From 49d4aa4decf8dbf10be22989a596b0572dd04f5a Mon Sep 17 00:00:00 2001 From: kyri-petrou <67301607+kyri-petrou@users.noreply.github.com> Date: Sun, 21 Jan 2024 08:45:59 +1100 Subject: [PATCH] Micro-optimizations and improved `IOException` error handling (#2638) * Micro-optimizations and fixes * Fix `CharSequenceExtensions.compare` method --- .../zhttp.benchmarks/UtilBenchmark.scala | 70 ++++++++ .../zio/http/netty/model/Conversions.scala | 154 ++---------------- .../netty/server/ServerInboundHandler.scala | 7 +- .../src/main/scala/zio/http/Status.scala | 2 +- .../internal/CharSequenceExtensions.scala | 32 ++-- 5 files changed, 107 insertions(+), 158 deletions(-) create mode 100644 zio-http-benchmarks/src/main/scala/zhttp.benchmarks/UtilBenchmark.scala diff --git a/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/UtilBenchmark.scala b/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/UtilBenchmark.scala new file mode 100644 index 0000000000..37e005e9f7 --- /dev/null +++ b/zio-http-benchmarks/src/main/scala/zhttp.benchmarks/UtilBenchmark.scala @@ -0,0 +1,70 @@ +package zio.http.netty.benchmarks + +import java.util.concurrent.TimeUnit + +import zio.http.internal.{CaseMode, CharSequenceExtensions} +import zio.http.netty.model.Conversions + +import io.netty.handler.codec.http.DefaultHttpHeaders +import org.openjdk.jmh.annotations._ + +@State(Scope.Thread) +@BenchmarkMode(Array(Mode.AverageTime)) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(1) +@Warmup(iterations = 3, time = 3) +@Measurement(iterations = 3, time = 3) +class UtilBenchmark { + + private val nettyHeaders = + new DefaultHttpHeaders() + .add("Content-Type", "application/json; charset=utf-8") + .add("Content-Length", "100") + .add("Content-Encoding", "gzip") + .add("Accept", "application/json") + .add("Accept-Encoding", "gzip, deflate, br") + .add("Accept-Language", "en-US,en;q=0.9") + .add("Connection", "keep-alive") + .add("Host", "localhost:8080") + .add("Origin", "http://localhost:8080") + .add("Referer", "http://localhost:8080/") + .add("Sec-Fetch-Dest", "empty") + .add("Sec-Fetch-Mode", "cors") + .add("Sec-Fetch-Site", "same-origin") + .add( + "User-Agent", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/88.0.4324.96 Chrome/88.0.4324.96 Safari/537.36", + ) + + private val headers = Conversions.headersFromNetty(nettyHeaders) + + @Benchmark + def benchmarkEqualsInsensitive(): Unit = { + CharSequenceExtensions.equals( + "application/json; charset=utf-8", + "Application/json; charset=utf-8", + caseMode = CaseMode.Insensitive, + ) + () + } + + @Benchmark + // For comparison with benchmarkEqualsInsensitive + def benchmarkEqualsInsensitiveJava(): Unit = { + val _ = "application/json; charset=utf-8".equalsIgnoreCase("application/json; Charset=utf-8") + () + } + + @Benchmark + def benchmarkHeaderGetUnsafe(): Unit = { + headers.getUnsafe("sec-fetch-site") + () + } + + @Benchmark + def benchmarkStatusToNetty(): Unit = { + Conversions.statusToNetty(zio.http.Status.InternalServerError) + () + } + +} diff --git a/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala b/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala index 5f3beec7c7..e8e5268fde 100644 --- a/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala +++ b/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala @@ -22,7 +22,6 @@ import zio.stacktracer.TracingImplicits.disableAutoTrace import zio.http.Server.Config.CompressionOptions import zio.http._ -import zio.http.internal.{CaseMode, CharSequenceExtensions} import io.netty.handler.codec.compression.{DeflateOptions, StandardCompressionOptions} import io.netty.handler.codec.http._ @@ -60,9 +59,9 @@ private[netty] object Conversions { def headersToNetty(headers: Headers): HttpHeaders = headers match { - case Headers.FromIterable(_) => encodeHeaderListToNetty(headers.toList) + case Headers.FromIterable(_) => encodeHeaderListToNetty(headers) case Headers.Native(value, _, _) => value.asInstanceOf[HttpHeaders] - case Headers.Concat(_, _) => encodeHeaderListToNetty(headers.toList) + case Headers.Concat(_, _) => encodeHeaderListToNetty(headers) case Headers.Empty => new DefaultHttpHeaders() } @@ -82,152 +81,31 @@ private[netty] object Conversions { Headers.Native( headers, (headers: HttpHeaders) => nettyHeadersIterator(headers), - (headers: HttpHeaders, key: CharSequence) => { - val iterator = headers.iteratorCharSequence() - var result: String = null - while (iterator.hasNext && (result eq null)) { - val entry = iterator.next() - if (CharSequenceExtensions.equals(entry.getKey, key, CaseMode.Insensitive)) { - result = entry.getValue.toString - } - } - - result - }, + // NOTE: Netty's headers.get is case-insensitive + (headers: HttpHeaders, key: CharSequence) => headers.get(key), ) private def encodeHeaderListToNetty(headers: Iterable[Header]): HttpHeaders = { - val nettyHeaders = new DefaultHttpHeaders(true) - for (header <- headers) { - if (header.headerName == Header.SetCookie.name) { - nettyHeaders.add(header.headerName, header.renderedValueAsCharSequence) + val nettyHeaders = new DefaultHttpHeaders(true) + val setCookieName = Header.SetCookie.name + val iter = headers.iterator + while (iter.hasNext) { + val header = iter.next() + val name = header.headerName + if (name == setCookieName) { + nettyHeaders.add(name, header.renderedValueAsCharSequence) } else { - nettyHeaders.set(header.headerName, header.renderedValueAsCharSequence) + nettyHeaders.set(name, header.renderedValueAsCharSequence) } } nettyHeaders } def statusToNetty(status: Status): HttpResponseStatus = - status match { - case Status.Continue => HttpResponseStatus.CONTINUE // 100 - case Status.SwitchingProtocols => HttpResponseStatus.SWITCHING_PROTOCOLS // 101 - case Status.Processing => HttpResponseStatus.PROCESSING // 102 - case Status.Ok => HttpResponseStatus.OK // 200 - case Status.Created => HttpResponseStatus.CREATED // 201 - case Status.Accepted => HttpResponseStatus.ACCEPTED // 202 - case Status.NonAuthoritativeInformation => HttpResponseStatus.NON_AUTHORITATIVE_INFORMATION // 203 - case Status.NoContent => HttpResponseStatus.NO_CONTENT // 204 - case Status.ResetContent => HttpResponseStatus.RESET_CONTENT // 205 - case Status.PartialContent => HttpResponseStatus.PARTIAL_CONTENT // 206 - case Status.MultiStatus => HttpResponseStatus.MULTI_STATUS // 207 - case Status.MultipleChoices => HttpResponseStatus.MULTIPLE_CHOICES // 300 - case Status.MovedPermanently => HttpResponseStatus.MOVED_PERMANENTLY // 301 - case Status.Found => HttpResponseStatus.FOUND // 302 - case Status.SeeOther => HttpResponseStatus.SEE_OTHER // 303 - case Status.NotModified => HttpResponseStatus.NOT_MODIFIED // 304 - case Status.UseProxy => HttpResponseStatus.USE_PROXY // 305 - case Status.TemporaryRedirect => HttpResponseStatus.TEMPORARY_REDIRECT // 307 - case Status.PermanentRedirect => HttpResponseStatus.PERMANENT_REDIRECT // 308 - case Status.BadRequest => HttpResponseStatus.BAD_REQUEST // 400 - case Status.Unauthorized => HttpResponseStatus.UNAUTHORIZED // 401 - case Status.PaymentRequired => HttpResponseStatus.PAYMENT_REQUIRED // 402 - case Status.Forbidden => HttpResponseStatus.FORBIDDEN // 403 - case Status.NotFound => HttpResponseStatus.NOT_FOUND // 404 - case Status.MethodNotAllowed => HttpResponseStatus.METHOD_NOT_ALLOWED // 405 - case Status.NotAcceptable => HttpResponseStatus.NOT_ACCEPTABLE // 406 - case Status.ProxyAuthenticationRequired => HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED // 407 - case Status.RequestTimeout => HttpResponseStatus.REQUEST_TIMEOUT // 408 - case Status.Conflict => HttpResponseStatus.CONFLICT // 409 - case Status.Gone => HttpResponseStatus.GONE // 410 - case Status.LengthRequired => HttpResponseStatus.LENGTH_REQUIRED // 411 - case Status.PreconditionFailed => HttpResponseStatus.PRECONDITION_FAILED // 412 - case Status.RequestEntityTooLarge => HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE // 413 - case Status.RequestUriTooLong => HttpResponseStatus.REQUEST_URI_TOO_LONG // 414 - case Status.UnsupportedMediaType => HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE // 415 - case Status.RequestedRangeNotSatisfiable => HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE // 416 - case Status.ExpectationFailed => HttpResponseStatus.EXPECTATION_FAILED // 417 - case Status.MisdirectedRequest => HttpResponseStatus.MISDIRECTED_REQUEST // 421 - case Status.UnprocessableEntity => HttpResponseStatus.UNPROCESSABLE_ENTITY // 422 - case Status.Locked => HttpResponseStatus.LOCKED // 423 - case Status.FailedDependency => HttpResponseStatus.FAILED_DEPENDENCY // 424 - case Status.UnorderedCollection => HttpResponseStatus.UNORDERED_COLLECTION // 425 - case Status.UpgradeRequired => HttpResponseStatus.UPGRADE_REQUIRED // 426 - case Status.PreconditionRequired => HttpResponseStatus.PRECONDITION_REQUIRED // 428 - case Status.TooManyRequests => HttpResponseStatus.TOO_MANY_REQUESTS // 429 - case Status.RequestHeaderFieldsTooLarge => HttpResponseStatus.REQUEST_HEADER_FIELDS_TOO_LARGE // 431 - case Status.InternalServerError => HttpResponseStatus.INTERNAL_SERVER_ERROR // 500 - case Status.NotImplemented => HttpResponseStatus.NOT_IMPLEMENTED // 501 - case Status.BadGateway => HttpResponseStatus.BAD_GATEWAY // 502 - case Status.ServiceUnavailable => HttpResponseStatus.SERVICE_UNAVAILABLE // 503 - case Status.GatewayTimeout => HttpResponseStatus.GATEWAY_TIMEOUT // 504 - case Status.HttpVersionNotSupported => HttpResponseStatus.HTTP_VERSION_NOT_SUPPORTED // 505 - case Status.VariantAlsoNegotiates => HttpResponseStatus.VARIANT_ALSO_NEGOTIATES // 506 - case Status.InsufficientStorage => HttpResponseStatus.INSUFFICIENT_STORAGE // 507 - case Status.NotExtended => HttpResponseStatus.NOT_EXTENDED // 510 - case Status.NetworkAuthenticationRequired => HttpResponseStatus.NETWORK_AUTHENTICATION_REQUIRED // 511 - case Status.Custom(code) => HttpResponseStatus.valueOf(code) - } + HttpResponseStatus.valueOf(status.code) - def statusFromNetty(status: HttpResponseStatus): Status = (status: @unchecked) match { - case HttpResponseStatus.CONTINUE => Status.Continue - case HttpResponseStatus.SWITCHING_PROTOCOLS => Status.SwitchingProtocols - case HttpResponseStatus.PROCESSING => Status.Processing - case HttpResponseStatus.OK => Status.Ok - case HttpResponseStatus.CREATED => Status.Created - case HttpResponseStatus.ACCEPTED => Status.Accepted - case HttpResponseStatus.NON_AUTHORITATIVE_INFORMATION => Status.NonAuthoritativeInformation - case HttpResponseStatus.NO_CONTENT => Status.NoContent - case HttpResponseStatus.RESET_CONTENT => Status.ResetContent - case HttpResponseStatus.PARTIAL_CONTENT => Status.PartialContent - case HttpResponseStatus.MULTI_STATUS => Status.MultiStatus - case HttpResponseStatus.MULTIPLE_CHOICES => Status.MultipleChoices - case HttpResponseStatus.MOVED_PERMANENTLY => Status.MovedPermanently - case HttpResponseStatus.FOUND => Status.Found - case HttpResponseStatus.SEE_OTHER => Status.SeeOther - case HttpResponseStatus.NOT_MODIFIED => Status.NotModified - case HttpResponseStatus.USE_PROXY => Status.UseProxy - case HttpResponseStatus.TEMPORARY_REDIRECT => Status.TemporaryRedirect - case HttpResponseStatus.PERMANENT_REDIRECT => Status.PermanentRedirect - case HttpResponseStatus.BAD_REQUEST => Status.BadRequest - case HttpResponseStatus.UNAUTHORIZED => Status.Unauthorized - case HttpResponseStatus.PAYMENT_REQUIRED => Status.PaymentRequired - case HttpResponseStatus.FORBIDDEN => Status.Forbidden - case HttpResponseStatus.NOT_FOUND => Status.NotFound - case HttpResponseStatus.METHOD_NOT_ALLOWED => Status.MethodNotAllowed - case HttpResponseStatus.NOT_ACCEPTABLE => Status.NotAcceptable - case HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED => Status.ProxyAuthenticationRequired - case HttpResponseStatus.REQUEST_TIMEOUT => Status.RequestTimeout - case HttpResponseStatus.CONFLICT => Status.Conflict - case HttpResponseStatus.GONE => Status.Gone - case HttpResponseStatus.LENGTH_REQUIRED => Status.LengthRequired - case HttpResponseStatus.PRECONDITION_FAILED => Status.PreconditionFailed - case HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE => Status.RequestEntityTooLarge - case HttpResponseStatus.REQUEST_URI_TOO_LONG => Status.RequestUriTooLong - case HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE => Status.UnsupportedMediaType - case HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE => Status.RequestedRangeNotSatisfiable - case HttpResponseStatus.EXPECTATION_FAILED => Status.ExpectationFailed - case HttpResponseStatus.MISDIRECTED_REQUEST => Status.MisdirectedRequest - case HttpResponseStatus.UNPROCESSABLE_ENTITY => Status.UnprocessableEntity - case HttpResponseStatus.LOCKED => Status.Locked - case HttpResponseStatus.FAILED_DEPENDENCY => Status.FailedDependency - case HttpResponseStatus.UNORDERED_COLLECTION => Status.UnorderedCollection - case HttpResponseStatus.UPGRADE_REQUIRED => Status.UpgradeRequired - case HttpResponseStatus.PRECONDITION_REQUIRED => Status.PreconditionRequired - case HttpResponseStatus.TOO_MANY_REQUESTS => Status.TooManyRequests - case HttpResponseStatus.REQUEST_HEADER_FIELDS_TOO_LARGE => Status.RequestHeaderFieldsTooLarge - case HttpResponseStatus.INTERNAL_SERVER_ERROR => Status.InternalServerError - case HttpResponseStatus.NOT_IMPLEMENTED => Status.NotImplemented - case HttpResponseStatus.BAD_GATEWAY => Status.BadGateway - case HttpResponseStatus.SERVICE_UNAVAILABLE => Status.ServiceUnavailable - case HttpResponseStatus.GATEWAY_TIMEOUT => Status.GatewayTimeout - case HttpResponseStatus.HTTP_VERSION_NOT_SUPPORTED => Status.HttpVersionNotSupported - case HttpResponseStatus.VARIANT_ALSO_NEGOTIATES => Status.VariantAlsoNegotiates - case HttpResponseStatus.INSUFFICIENT_STORAGE => Status.InsufficientStorage - case HttpResponseStatus.NOT_EXTENDED => Status.NotExtended - case HttpResponseStatus.NETWORK_AUTHENTICATION_REQUIRED => Status.NetworkAuthenticationRequired - case status: HttpResponseStatus => Status.Custom(status.code) - } + def statusFromNetty(status: HttpResponseStatus): Status = + Status.fromInt(status.code) def schemeToNetty(scheme: Scheme): Option[HttpScheme] = scheme match { case Scheme.HTTP => Option(HttpScheme.HTTP) diff --git a/zio-http/jvm/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala b/zio-http/jvm/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala index 311ba6afee..e554caa649 100644 --- a/zio-http/jvm/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala +++ b/zio-http/jvm/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala @@ -109,8 +109,11 @@ private[zio] final case class ServerInboundHandler( override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit = cause match { - case ioe: IOException if ioe.getMessage.startsWith("Connection reset") => - case t => + case ioe: IOException if { + val msg = ioe.getMessage + (msg ne null) && msg.contains("Connection reset") + } => + case t => if (app ne null) { runtime.run(ctx, () => {}) { // We cannot return the generated response from here, but still calling the handler for its side effect diff --git a/zio-http/shared/src/main/scala/zio/http/Status.scala b/zio-http/shared/src/main/scala/zio/http/Status.scala index 96fd397a9a..089af773b9 100644 --- a/zio-http/shared/src/main/scala/zio/http/Status.scala +++ b/zio-http/shared/src/main/scala/zio/http/Status.scala @@ -176,7 +176,7 @@ object Status { Try(code.toInt).toOption.map(fromInt) def fromInt(code: Int): Status = { - code match { + (code: @annotation.switch) match { case 100 => Status.Continue case 101 => Status.SwitchingProtocols case 102 => Status.Processing diff --git a/zio-http/shared/src/main/scala/zio/http/internal/CharSequenceExtensions.scala b/zio-http/shared/src/main/scala/zio/http/internal/CharSequenceExtensions.scala index 5c081ef5d2..7008df4e5a 100644 --- a/zio-http/shared/src/main/scala/zio/http/internal/CharSequenceExtensions.scala +++ b/zio-http/shared/src/main/scala/zio/http/internal/CharSequenceExtensions.scala @@ -19,7 +19,7 @@ package zio.http.internal private[http] object CharSequenceExtensions { def equals(left: CharSequence, right: CharSequence, caseMode: CaseMode = CaseMode.Sensitive): Boolean = - if (left eq right) true else compare(left, right, caseMode) == 0 + left.length == right.length && compare(left, right, caseMode) == 0 /** * Lexicographically compares two `CharSequence`s. @@ -35,37 +35,35 @@ private[http] object CharSequenceExtensions { } else { val leftLength = left.length val rightLength = right.length - var result: Int = 0 + caseMode match { case CaseMode.Sensitive => var i = 0 - while (i < leftLength && i < leftLength && i < rightLength) { + while (i < leftLength && i < rightLength) { val leftChar = left.charAt(i) val rightChar = right.charAt(i) if (leftChar != rightChar) { - result = leftChar - rightChar - i = leftLength - } else { - i += 1 + return leftChar - rightChar } + i += 1 } case CaseMode.Insensitive => var i = 0 - while (i < leftLength && i < leftLength && i < rightLength) { - val leftChar = left.charAt(i).toLower - val rightChar = right.charAt(i).toLower + while (i < leftLength && i < rightLength) { + val leftChar = left.charAt(i) + val rightChar = right.charAt(i) if (leftChar != rightChar) { - result = leftChar - rightChar - i = leftLength - } else { - i += 1 + val lLower = leftChar.toLower + val rLower = rightChar.toLower + if (lLower != rLower) { + return lLower - rLower + } } + i += 1 } } - - if (result != 0) result else leftLength.compare(rightLength) + leftLength.compare(rightLength) } - } def hashCode(value: CharSequence): Int = {