-
Notifications
You must be signed in to change notification settings - Fork 374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Performance: Cookie decoder improvements #576
Conversation
821a10c
to
1cd3d23
Compare
Codecov Report
@@ Coverage Diff @@
## main #576 +/- ##
==========================================
- Coverage 57.36% 49.34% -8.03%
==========================================
Files 66 68 +2
Lines 1466 2061 +595
Branches 55 53 -2
==========================================
+ Hits 841 1017 +176
- Misses 625 1044 +419
Continue to review full report at Codecov.
|
), | ||
) | ||
} else { | ||
None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great, you have almost doubled the perf!
You can further reduce allocations by —
- Get rid of all the
Option
and usenull
instead. - Every
map
operation is creating a new collection (more memory), use a for/while loop if possible - Get rid of interim
Function
andTuple
also. - Don't allocate the
String
inside the method, pull them outside so that thy are allocated only once.
NOTE: You can have a method called - unsafeDecodeResponseCookie
that returns a null or a Cookie for performance and make it package private. Then also have a decodeResponseCookie
which calls unsafeDecodeResponseCookie
and wraps it in an Option
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made some changes to avoid string split and some allocations.
Marked internal functions parseDate and splitNameContent @inline functions, not sure if it could be a problem ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you update the benchmark numbers as before and after. I think we can make more changes for performance :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using None
use null
. This can cause null pointer exceptions but that should be fine because we will rename this method to unsafeDecodeResponseCookie
and make it package-private. And, we will have a separate method decodeResponseCookie
which calls unsafeDecodeResponseCookie
and wraps the output in an Option
. And that should be a public method.
val c = if (i <= 0) headerValue.substring(prev) else headerValue.substring(prev, i) | ||
val (n, ct) = splitNameContent(c) | ||
|
||
(n.toLowerCase, ct) match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unnecessary tuple allocation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tusharmath I have mode some changes to avoid string and tuple unnecessary allocations, inner loop is not the fanciest but I think that this way you get more performance ... what do you think ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you commit the Benchmark
code to the benchmarks project.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, added CookieDecodeBenchark to zio-http-benchmarks (very naive at the moment)
case _ => ("", None) | ||
@inline | ||
private def parseDate(v: String): Try[Instant] = | ||
Try(Instant.parse(v)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try will allocate additional memory. Instead, you could use a try { } catch {}
block.
Incase of failure return a null
@BenchmarkMode(Array(Mode.Throughput)) | ||
@OutputTimeUnit(TimeUnit.SECONDS) | ||
class CookieDecodeBenchmark { | ||
private val cookie = "SUID=123;httponly=true;expires=2007-12-03T10:15:30.00Z" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be generate a larger cookie. With a large payload and more key-value pairs.
} else { | ||
name = headerValue.substring(0, next) | ||
} | ||
} else if (headerValue.regionMatches(true, curr, "expires=", 0, 8)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The string literals will also allocate memory every time the method is called. You could move it into a value so that it's allocated only once.
} else if (headerValue.regionMatches(true, curr, "samesite=", 0, 9)) { | ||
val v = headerValue.substring(curr + 9, next).toLowerCase | ||
v match { | ||
case "lax" => sameSite = SameSite.Lax |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not really sure but "lax"
AFAIK will also allocate additional memory.
@tusharmath
|
zio-http-benchmarks/src/main/scala/zhttp.benchmarks/CookieDecodeBenchmark.scala
Show resolved
Hide resolved
Co-authored-by: Tushar Mathur <tusharmath@gmail.com>
Co-authored-by: Tushar Mathur <tusharmath@gmail.com>
try { Instant.parse(headerValue.substring(curr + 8, next)) } | ||
catch { case _: Throwable => null } | ||
} else if (headerValue.regionMatches(true, curr, fieldMaxAge, 0, fieldMaxAge.length)) { | ||
maxAge = Try(headerValue.substring(curr + 8, next).toLong).toOption |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can create a global try
catch
block for this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tusharmath , a couple of questions here:
-
would it be enough to wrap with a try each attempt to decode a field?
-
What should we do with the exception, discard it completely?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- We should have only 1 top level try catch block inside the
safe
implementation. - Incase of failure we should throw an exception in the
unsafe
implementation. The safe implementation captures it and then could do either of the two things —
a. Ignore the error and return aNone
.
b. Capture the error and return aLeft[Throwable]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, changed to catch the exception in decodeResponseCookie but keeping the Option[Cookie] signature
Really looking forward to this PR @jgoday :) |
zio-http-benchmarks/src/main/scala/zhttp.benchmarks/CookieDecodeBenchmark.scala
Outdated
Show resolved
Hide resolved
@jgoday Thanks! this was pretty amazing :) We make it 1.7x faster. It might not seem much right now, but it all adds up when write a proper e2e application. |
Thank you @tusharmath for all your help and support. |
See #571.
Reduce Cookie case class allocations in Cookie.decodeResponseCookie.
Right now, it allocates a new Cookie instance with each field (using Cookie.copy through several methods).
For example,
it allocates 3 instances for
"SUID=123;httponly=true;expires=2007-12-03T10:15:30.00Z"
.This change parses each field separately and then creates one single Cookie instance.
This simple jhm benchmark show the two approaches
Parsing and copying Cookie case class
Parsing fields with mutable vars