From 0c0f3a3ce2e2c18848350ef5e550504c46e329bd Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Thu, 4 Jan 2024 09:12:08 +1100 Subject: [PATCH 1/3] Allow creating a body from `Array[Byte]` --- .../ServerInboundHandlerBenchmark.scala | 50 +++++++++++++++++-- zio-http/src/main/scala/zio/http/Body.scala | 36 +++++++++++++ .../zio/http/netty/NettyBodyWriter.scala | 3 ++ 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/ServerInboundHandlerBenchmark.scala b/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/ServerInboundHandlerBenchmark.scala index 753a685f5b..7ea889b71c 100644 --- a/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/ServerInboundHandlerBenchmark.scala +++ b/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/ServerInboundHandlerBenchmark.scala @@ -13,10 +13,39 @@ import sttp.client3.{HttpURLConnectionBackend, UriContext, basicRequest} @BenchmarkMode(Array(Mode.Throughput)) @OutputTimeUnit(TimeUnit.SECONDS) class ServerInboundHandlerBenchmark { + private val random = scala.util.Random + random.setSeed(42) + private val largeString = random.alphanumeric.take(100_000).mkString + + private val baseUrl = "http://localhost:8080" + private val headers = Headers(Header.ContentType(MediaType.text.`plain`).untyped) + + private val arrayEndpoint = "array" + private val arrayResponse = ZIO.succeed( + Response( + status = Status.Ok, + headers = headers, + body = Body.fromArray(largeString.getBytes), + ), + ) + private val arrayRoute = Route.route(Method.GET / arrayEndpoint)(handler(arrayResponse)) + private val arrayRequest = basicRequest.get(uri"$baseUrl/$arrayEndpoint") + + private val chunkEndpoint = "chunk" + private val chunkResponse = ZIO.succeed( + Response( + status = Status.Ok, + headers = headers, + body = Body.fromChunk(Chunk.fromArray(largeString.getBytes)), + ), + ) + private val chunkRoute = Route.route(Method.GET / chunkEndpoint)(handler(chunkResponse)) + private val chunkRequest = basicRequest.get(uri"$baseUrl/$chunkEndpoint") + private val testResponse = ZIO.succeed(Response.text("Hello World!")) private val testEndPoint = "test" private val testRoute = Route.route(Method.GET / testEndPoint)(handler(testResponse)) - private val testUrl = s"http://localhost:8080/$testEndPoint" + private val testUrl = s"$baseUrl/$testEndPoint" private val testRequest = basicRequest.get(uri"$testUrl") private val shutdownResponse = Response.text("shutting down") @@ -27,7 +56,8 @@ class ServerInboundHandlerBenchmark { private def shutdownRoute(shutdownSignal: Promise[Nothing, Unit]) = Route.route(Method.GET / shutdownEndpoint)(handler(shutdownSignal.succeed(()).as(shutdownResponse))) - private def http(shutdownSignal: Promise[Nothing, Unit]) = Routes(testRoute, shutdownRoute(shutdownSignal)).toHttpApp + private def http(shutdownSignal: Promise[Nothing, Unit]) = + Routes(testRoute, arrayRoute, chunkRoute, shutdownRoute(shutdownSignal)).toHttpApp @Setup(Level.Trial) def setup(): Unit = { @@ -58,7 +88,21 @@ class ServerInboundHandlerBenchmark { } @Benchmark - def benchmarkApp(): Unit = { + def benchmarkLargeArray(): Unit = { + val statusCode = arrayRequest.send(backend).code + if (!statusCode.isSuccess) + throw new RuntimeException(s"Received unexpected status code ${statusCode.code}") + } + + @Benchmark + def benchmarkLargeChunk(): Unit = { + val statusCode = chunkRequest.send(backend).code + if (!statusCode.isSuccess) + throw new RuntimeException(s"Received unexpected status code ${statusCode.code}") + } + + @Benchmark + def benchmarkSimple(): Unit = { val statusCode = testRequest.send(backend).code if (!statusCode.isSuccess) throw new RuntimeException(s"Received unexpected status code ${statusCode.code}") diff --git a/zio-http/src/main/scala/zio/http/Body.scala b/zio-http/src/main/scala/zio/http/Body.scala index cf64927a2a..084784cbc1 100644 --- a/zio-http/src/main/scala/zio/http/Body.scala +++ b/zio-http/src/main/scala/zio/http/Body.scala @@ -164,6 +164,13 @@ object Body { */ def fromChunk(data: Chunk[Byte]): Body = ChunkBody(data) + /** + * Constructs a [[zio.http.Body]] from an array of bytes. + * + * WARNING: The array must not be mutated after creating the body. + */ + def fromArray(data: Array[Byte]): Body = ArrayBody(data) + /** * Constructs a [[zio.http.Body]] from the contents of a file. */ @@ -293,6 +300,35 @@ object Body { copy(mediaType = Some(newMediaType), boundary = boundary.orElse(Some(newBoundary))) } + private[zio] final case class ArrayBody( + data: Array[Byte], + override val mediaType: Option[MediaType] = None, + override val boundary: Option[Boundary] = None, + ) extends Body + with UnsafeWriteable + with UnsafeBytes { self => + + override def asArray(implicit trace: Trace): Task[Array[Byte]] = ZIO.succeed(data) + + override def isComplete: Boolean = true + + override def isEmpty: Boolean = data.isEmpty + + override def asChunk(implicit trace: Trace): Task[Chunk[Byte]] = ZIO.succeed(Chunk.fromArray(data)) + + override def asStream(implicit trace: Trace): ZStream[Any, Throwable, Byte] = + ZStream.unwrap(asChunk.map(ZStream.fromChunk(_))) + + override def toString(): String = s"Body.fromArray($data)" + + override private[zio] def unsafeAsArray(implicit unsafe: Unsafe): Array[Byte] = data + + override def contentType(newMediaType: MediaType): Body = copy(mediaType = Some(newMediaType)) + + override def contentType(newMediaType: MediaType, newBoundary: Boundary): Body = + copy(mediaType = Some(newMediaType), boundary = boundary.orElse(Some(newBoundary))) + } + private[zio] final case class FileBody( val file: java.io.File, chunkSize: Int = 1024 * 4, diff --git a/zio-http/src/main/scala/zio/http/netty/NettyBodyWriter.scala b/zio-http/src/main/scala/zio/http/netty/NettyBodyWriter.scala index 1fd1b23501..55bef0ef49 100644 --- a/zio-http/src/main/scala/zio/http/netty/NettyBodyWriter.scala +++ b/zio-http/src/main/scala/zio/http/netty/NettyBodyWriter.scala @@ -108,6 +108,9 @@ object NettyBodyWriter { } }, ) + case ArrayBody(data, _, _) => + ctx.writeAndFlush(Unpooled.wrappedBuffer(data)) + None case ChunkBody(data, _, _) => ctx.write(Unpooled.wrappedBuffer(data.toArray)) ctx.flush() From bdc5395c3ad741151feba6b07bbf99fa16838188 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Thu, 4 Jan 2024 12:27:37 +1100 Subject: [PATCH 2/3] Add Body.fromArray to HttpGen --- zio-http/src/test/scala/zio/http/internal/HttpGen.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zio-http/src/test/scala/zio/http/internal/HttpGen.scala b/zio-http/src/test/scala/zio/http/internal/HttpGen.scala index 1ba2149702..70ca1ac3e9 100644 --- a/zio-http/src/test/scala/zio/http/internal/HttpGen.scala +++ b/zio-http/src/test/scala/zio/http/internal/HttpGen.scala @@ -102,6 +102,7 @@ object HttpGen { ), Body.fromString(list.mkString("")), Body.fromChunk(Chunk.fromArray(list.mkString("").getBytes())), + Body.fromArray(list.mkString("").getBytes()), Body.empty, ), ) @@ -139,6 +140,7 @@ object HttpGen { ), Body.fromString(list.mkString("")), Body.fromChunk(Chunk.fromArray(list.mkString("").getBytes())), + Body.fromArray(list.mkString("").getBytes()), ), ) } yield cnt From fc853187772399b044e62b903511201a3cc4d496 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Thu, 4 Jan 2024 13:40:23 +1100 Subject: [PATCH 3/3] Fix scala 2.12 compiling --- .../scala/zhttp.benchmarks/ServerInboundHandlerBenchmark.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/ServerInboundHandlerBenchmark.scala b/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/ServerInboundHandlerBenchmark.scala index 7ea889b71c..4a36791fb4 100644 --- a/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/ServerInboundHandlerBenchmark.scala +++ b/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/ServerInboundHandlerBenchmark.scala @@ -15,7 +15,7 @@ import sttp.client3.{HttpURLConnectionBackend, UriContext, basicRequest} class ServerInboundHandlerBenchmark { private val random = scala.util.Random random.setSeed(42) - private val largeString = random.alphanumeric.take(100_000).mkString + private val largeString = random.alphanumeric.take(100000).mkString private val baseUrl = "http://localhost:8080" private val headers = Headers(Header.ContentType(MediaType.text.`plain`).untyped)