Skip to content

Reduce allocations on DefaultHeaders::containsValue#15843

Merged
chrisvest merged 4 commits into
netty:4.2from
franz1981:faster_is_keep_alive
Feb 3, 2026
Merged

Reduce allocations on DefaultHeaders::containsValue#15843
chrisvest merged 4 commits into
netty:4.2from
franz1981:faster_is_keep_alive

Conversation

@franz1981

Copy link
Copy Markdown
Contributor

Motivation:
DefaultHeaders::containsValue is used heavily to detect if a request contains close or keep-alive header values, but the default implementations always allocates iterators

Modification:
Implements a specialized garbage-free method using optimized predicates which don't need to allocate any iterator

Result:
cheaper HTTP keep alive checks

@franz1981

franz1981 commented Nov 12, 2025

Copy link
Copy Markdown
Contributor Author

And I believe that containsCommaSeparatedTrimmed could be specialized too, to use regionMatches

https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#regionMatches-boolean-int-java.lang.String-int-int-

or

public boolean regionMatches(boolean ignoreCase, int thisStart, CharSequence string, int start, int length) {

based on the type of CharSequence values.

Another GC pain point is re trimming which could just skip whitespaces;

to be fair I hope this won't be a problem because I expect "old" browsers to just provide

Connection: keep-alive

which should not allocate any trimmed value or subsequence - and that's why i didn't implemented it here.

@franz1981 franz1981 force-pushed the faster_is_keep_alive branch from 5431f24 to 48bd450 Compare November 12, 2025 10:05
@franz1981

Copy link
Copy Markdown
Contributor Author

FYI @isaacrivriv ^^

@franz1981 franz1981 force-pushed the faster_is_keep_alive branch from 48bd450 to 691bf6d Compare November 13, 2025 03:07
* Default implementation of {@link HttpHeaders}.
*/
public class DefaultHttpHeaders extends HttpHeaders {
private static final BiPredicate<CharSequence, CharSequence> CASE_INSENSITIVE_CONTAINS =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is bimorphic on purpose, but clearly...it can become problematic if the containsAny method become too big to be inlined - and the predicate call site bloated of types.
But to be fair it shouldn't happen: this method here is used mostly with a single lambda type.

* @param predicateArg argument passed as the second parameter to {@code valuePredicate} (may be {@code null})
* @param valuePredicate predicate used to test stored header values (must not be {@code null})
*/
public boolean containsAny(K name, V predicateArg, BiPredicate<? super V, ? super V> valuePredicate) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern comes from the old Disruptor times, to avoid allocations and defining different capturing lambda types based on the argument

@franz1981

Copy link
Copy Markdown
Contributor Author

I've added 2 benchmarks for existent values and not existent ones, getting mixed results vs before...
This PR:

Benchmark                                     (exampleHeader)  Mode  Cnt    Score    Error  Units
HeadersBenchmark.httpContainsValueIgnoreCase            THREE  avgt   10   25.379 ±  0.032  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase             FIVE  avgt   10  100.208 ±  3.906  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase              SIX  avgt   10   59.749 ±  0.086  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase            EIGHT  avgt   10   74.956 ±  1.664  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase           ELEVEN  avgt   10  226.583 ±  9.015  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase        TWENTYTWO  avgt   10  385.847 ± 15.629  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase           THIRTY  avgt   10  570.298 ± 20.580  ns/op

Benchmark                                                (exampleHeader)  Mode  Cnt    Score    Error  Units
HeadersBenchmark.httpContainsNotExistentValueIgnoreCase            THREE  avgt   10   27.072 ±  0.067  ns/op
HeadersBenchmark.httpContainsNotExistentValueIgnoreCase             FIVE  avgt   10   88.509 ±  3.460  ns/op
HeadersBenchmark.httpContainsNotExistentValueIgnoreCase              SIX  avgt   10   60.892 ±  0.064  ns/op
HeadersBenchmark.httpContainsNotExistentValueIgnoreCase            EIGHT  avgt   10   81.440 ±  0.239  ns/op
HeadersBenchmark.httpContainsNotExistentValueIgnoreCase           ELEVEN  avgt   10  234.479 ±  9.173  ns/op
HeadersBenchmark.httpContainsNotExistentValueIgnoreCase        TWENTYTWO  avgt   10  405.157 ± 16.898  ns/op
HeadersBenchmark.httpContainsNotExistentValueIgnoreCase           THIRTY  avgt   10  542.474 ± 18.648  ns/op

vs before:

Benchmark                                     (exampleHeader)  Mode  Cnt    Score    Error  Units
HeadersBenchmark.httpContainsValueIgnoreCase            THREE  avgt   10   25.902 ±  0.018  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase             FIVE  avgt   10   91.040 ±  3.477  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase              SIX  avgt   10   59.999 ±  0.117  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase            EIGHT  avgt   10   79.694 ±  0.043  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase           ELEVEN  avgt   10  238.261 ±  9.468  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase        TWENTYTWO  avgt   10  405.410 ± 15.945  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase           THIRTY  avgt   10  500.464 ± 20.621  ns/op

Benchmark                                                (exampleHeader)  Mode  Cnt    Score    Error  Units
HeadersBenchmark.httpContainsNotExistentValueIgnoreCase            THREE  avgt   10   28.717 ±  0.028  ns/op
HeadersBenchmark.httpContainsNotExistentValueIgnoreCase             FIVE  avgt   10   94.704 ±  3.713  ns/op
HeadersBenchmark.httpContainsNotExistentValueIgnoreCase              SIX  avgt   10   67.872 ±  0.049  ns/op
HeadersBenchmark.httpContainsNotExistentValueIgnoreCase            EIGHT  avgt   10  104.794 ±  0.126  ns/op
HeadersBenchmark.httpContainsNotExistentValueIgnoreCase           ELEVEN  avgt   10  243.170 ±  7.615  ns/op
HeadersBenchmark.httpContainsNotExistentValueIgnoreCase        TWENTYTWO  avgt   10  410.704 ± 16.797  ns/op
HeadersBenchmark.httpContainsNotExistentValueIgnoreCase           THIRTY  avgt   10  527.890 ± 22.066  ns/op

The THIRTY case in particular, shows a degradation regardless, so need investigation.

@franz1981

Copy link
Copy Markdown
Contributor Author

The reason for the perf regression seems related to 2 methods not inlined, see

image

whilst, before:

image

Now investigating if it's related the benchmark itself or not

@franz1981

franz1981 commented Nov 13, 2025

Copy link
Copy Markdown
Contributor Author

I've fixed the benchmark, because the lambda wasn't inlined because of the inlining depth (DOH!), and numbers now look more similar with some small win and loss

This PR:

Benchmark                                     (exampleHeader)  Mode  Cnt    Score    Error  Units
HeadersBenchmark.httpContainsValueIgnoreCase            THREE  avgt   10   23.182 ±  0.410  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase             FIVE  avgt   10   91.740 ±  3.598  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase              SIX  avgt   10   58.874 ±  0.479  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase            EIGHT  avgt   10   83.743 ±  0.391  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase           ELEVEN  avgt   10  220.086 ±  8.467  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase        TWENTYTWO  avgt   10  366.428 ± 13.781  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase           THIRTY  avgt   10  530.943 ± 16.282  ns/op

Benchmark                                                (exampleHeader)  Mode  Cnt    Score    Error  Units
HeadersBenchmark.httpContainsValueIgnoreCaseNonExisting            THREE  avgt   10   26.735 ±  0.061  ns/op
HeadersBenchmark.httpContainsValueIgnoreCaseNonExisting             FIVE  avgt   10   98.547 ±  3.700  ns/op
HeadersBenchmark.httpContainsValueIgnoreCaseNonExisting              SIX  avgt   10   65.571 ±  0.052  ns/op
HeadersBenchmark.httpContainsValueIgnoreCaseNonExisting            EIGHT  avgt   10   92.841 ±  0.074  ns/op
HeadersBenchmark.httpContainsValueIgnoreCaseNonExisting           ELEVEN  avgt   10  235.793 ±  9.280  ns/op
HeadersBenchmark.httpContainsValueIgnoreCaseNonExisting        TWENTYTWO  avgt   10  394.907 ± 15.574  ns/op
HeadersBenchmark.httpContainsValueIgnoreCaseNonExisting           THIRTY  avgt   10  563.534 ± 22.980  ns/op

whilst, before:

Benchmark                                     (exampleHeader)  Mode  Cnt    Score    Error  Units
HeadersBenchmark.httpContainsValueIgnoreCase            THREE  avgt   10   25.883 ±  0.026  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase             FIVE  avgt   10   90.089 ±  3.190  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase              SIX  avgt   10   63.379 ±  0.157  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase            EIGHT  avgt   10   91.922 ±  0.199  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase           ELEVEN  avgt   10  242.027 ±  9.806  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase        TWENTYTWO  avgt   10  410.833 ± 17.691  ns/op
HeadersBenchmark.httpContainsValueIgnoreCase           THIRTY  avgt   10  569.019 ± 19.246  ns/op

Benchmark                                                (exampleHeader)  Mode  Cnt    Score    Error  Units
HeadersBenchmark.httpContainsValueIgnoreCaseNonExisting            THREE  avgt   10   27.074 ±  0.051  ns/op
HeadersBenchmark.httpContainsValueIgnoreCaseNonExisting             FIVE  avgt   10   90.094 ±  3.465  ns/op
HeadersBenchmark.httpContainsValueIgnoreCaseNonExisting              SIX  avgt   10   68.341 ±  0.101  ns/op
HeadersBenchmark.httpContainsValueIgnoreCaseNonExisting            EIGHT  avgt   10   97.504 ±  0.105  ns/op
HeadersBenchmark.httpContainsValueIgnoreCaseNonExisting           ELEVEN  avgt   10  243.146 ±  9.524  ns/op
HeadersBenchmark.httpContainsValueIgnoreCaseNonExisting        TWENTYTWO  avgt   10  404.841 ± 15.793  ns/op
HeadersBenchmark.httpContainsValueIgnoreCaseNonExisting           THIRTY  avgt   10  538.118 ± 19.718  ns/op

In short, the performance looks here as expected, mostly unchanged (we reduced the allocation rate, a bit).
Which means that to show real benefit we should have the case mentioned, to be stressed as well.
Writing now a microbench specific to this as well.

@franz1981

Copy link
Copy Markdown
Contributor Author

I see a test failure, so handling it i.e.

Error:  Tests run: 32, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.044 s <<< FAILURE! -- in io.netty.handler.codec.http.CombinedHttpHeadersTest
Error:  io.netty.handler.codec.http.CombinedHttpHeadersTest.owsTrimming -- Time elapsed: 0.017 s <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:31)
	at org.junit.jupiter.api.Assertions.assertTrue(Assertions.java:183)
	at io.netty.handler.codec.http.CombinedHttpHeadersTest.owsTrimming(CombinedHttpHeadersTest.java:332)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
	at java.base/java.util.concurrent.ForkJoinTask.doJoin(ForkJoinTask.java:396)
	at java.base/java.util.concurrent.ForkJoinTask.join(ForkJoinTask.java:721)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
	at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
	at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
	at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
	at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:183)

@franz1981

franz1981 commented Nov 13, 2025

Copy link
Copy Markdown
Contributor Author

I've fixed the test failure: combiner http headers was relying on containsValue which, previously, was based on the value iterations, which is overridden in the combined case.
I've implemented the containsAny correctly in such case as well (sadly, similarly as before, to preserve the new value iteration semantic).

This is a case where OOP failed me: there's no way to remember to child impls of DefaultHeaders to re-implement the new provided method containsAny to respect the value iteration order, now used in DefaultHttpHeaders.
The same problem existed for getAll to be fair....

Performance wise, I have added a case which shows what happen (in some artificial way, I know) if the value iterator would escape and get allocated.

This PR:

Benchmark                           (close)  (warmup)  Mode  Cnt   Score   Error  Units
IsKeepAliveBenchmark.isKeepAlive  KeepAlive     false  avgt   20   8.762 ± 0.127  ns/op
IsKeepAliveBenchmark.isKeepAlive  KeepAlive      true  avgt   20   9.040 ± 0.091  ns/op
IsKeepAliveBenchmark.isKeepAlive      Close     false  avgt   20  10.961 ± 0.080  ns/op
IsKeepAliveBenchmark.isKeepAlive      Close      true  avgt   20  10.436 ± 0.023  ns/op
IsKeepAliveBenchmark.isKeepAlive       None     false  avgt   20   1.498 ± 0.014  ns/op
IsKeepAliveBenchmark.isKeepAlive       None      true  avgt   20   2.283 ± 0.008  ns/op

whilst before:

Benchmark                                                          (close)  (warmup)  Mode  Cnt      Score     Error   Units
IsKeepAliveBenchmark.isKeepAlive                                 KeepAlive     false  avgt   20      9.031 ±   0.063   ns/op
IsKeepAliveBenchmark.isKeepAlive                                 KeepAlive      true  avgt   20      8.738 ±   0.095   ns/op
IsKeepAliveBenchmark.isKeepAlive                                     Close     false  avgt   20     10.837 ±   0.356   ns/op
IsKeepAliveBenchmark.isKeepAlive                                     Close      true  avgt   20     10.915 ±   0.066   ns/op
IsKeepAliveBenchmark.isKeepAlive                                      None     false  avgt   20      1.483 ±   0.009   ns/op
IsKeepAliveBenchmark.isKeepAlive                                      None      true  avgt   20      2.281 ±   0.017   ns/op
IsKeepAliveBenchmark.isKeepAliveWithGarbage                      KeepAlive     false  avgt   20     18.613 ±   0.856   ns/op
IsKeepAliveBenchmark.isKeepAliveWithGarbage                      KeepAlive      true  avgt   20     17.943 ±   0.395   ns/op
IsKeepAliveBenchmark.isKeepAliveWithGarbage                          Close     false  avgt   20     20.365 ±   0.405   ns/op
IsKeepAliveBenchmark.isKeepAliveWithGarbage                          Close      true  avgt   20     21.805 ±   0.451   ns/op
IsKeepAliveBenchmark.isKeepAliveWithGarbage                           None     false  avgt   20      3.819 ±   0.199   ns/op
IsKeepAliveBenchmark.isKeepAliveWithGarbage                           None      true  avgt   20      5.733 ±   0.434   ns/op

The results of this PR with garbage are the same, and I didn't reported them for brevity.
The alloc.norm for before is 40 bytes/op (which is the size of the iterator on the heap).

@franz1981 franz1981 force-pushed the faster_is_keep_alive branch 2 times, most recently from c0bbbbd to a94b4c2 Compare November 13, 2025 06:37
@normanmaurer

Copy link
Copy Markdown
Member

@franz1981 please fix check style.

Motivation:
DefaultHeaders::containsValue is used heavily to detect if a request contains close or keep-alive header values, but the default implementations always allocates iterators

Modification:
Implements a specialized garbage-free method using optimized predicates which don't need to allocate any iterator

Result:
cheaper HTTP keep alive checks
@franz1981 franz1981 force-pushed the faster_is_keep_alive branch from a94b4c2 to 164e7f1 Compare November 17, 2025 14:38
@normanmaurer

Copy link
Copy Markdown
Member

@franz1981 is this ready to go ?

@normanmaurer normanmaurer added this to the 4.2.10.Final milestone Jan 21, 2026

@franz1981 franz1981 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yesss ❤️

@chrisvest chrisvest left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed one more interesting test case we could add. Otherwise looks good.

@chrisvest chrisvest enabled auto-merge (squash) February 3, 2026 18:58
@chrisvest chrisvest added the needs-cherry-pick-5.0 This PR should be cherry-picked to 5.0 once merged. label Feb 3, 2026
@chrisvest chrisvest merged commit 874c995 into netty:4.2 Feb 3, 2026
33 of 35 checks passed
chrisvest added a commit to chrisvest/netty that referenced this pull request Feb 3, 2026
Motivation:
DefaultHeaders::containsValue is used heavily to detect if a request
contains close or keep-alive header values, but the default
implementations always allocates iterators

Modification:
Implements a specialized garbage-free method using optimized predicates
which don't need to allocate any iterator

Result:
cheaper HTTP keep alive checks

---------

Co-authored-by: Norman Maurer <norman_maurer@apple.com>
Co-authored-by: Chris Vest <mr.chrisvest@gmail.com>
Co-authored-by: Chris Vest <christianvest_hansen@apple.com>
(cherry picked from commit 874c995)
@chrisvest

Copy link
Copy Markdown
Member

Thanks, @franz1981 !

chrisvest added a commit that referenced this pull request Feb 4, 2026
Motivation:
DefaultHeaders::containsValue is used heavily to detect if a request
contains close or keep-alive header values, but the default
implementations always allocates iterators

Modification:
Implements a specialized garbage-free method using optimized predicates
which don't need to allocate any iterator

Result:
cheaper HTTP keep alive checks

(cherry picked from commit 874c995)

Co-authored-by: Francesco Nigro <nigro.fra@gmail.com>
Co-authored-by: Norman Maurer <norman_maurer@apple.com>
andyhedges pushed a commit to andyhedges/netty that referenced this pull request Feb 8, 2026
Motivation:
DefaultHeaders::containsValue is used heavily to detect if a request
contains close or keep-alive header values, but the default
implementations always allocates iterators

Modification:
Implements a specialized garbage-free method using optimized predicates
which don't need to allocate any iterator

Result:
cheaper HTTP keep alive checks

---------

Co-authored-by: Norman Maurer <norman_maurer@apple.com>
Co-authored-by: Chris Vest <mr.chrisvest@gmail.com>
Co-authored-by: Chris Vest <christianvest_hansen@apple.com>
@chrisvest chrisvest removed the needs-cherry-pick-5.0 This PR should be cherry-picked to 5.0 once merged. label Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants