Skip to content

Add option to resize HPACK dynamic table - #12973

Open
petedmarsh wants to merge 3 commits into
grpc:masterfrom
petedmarsh:disable-dynamic-hpack
Open

petedmarsh wants to merge 3 commits into
grpc:masterfrom
petedmarsh:disable-dynamic-hpack

Conversation

@petedmarsh

Copy link
Copy Markdown
Contributor

Add disableHpackDynamicTable() to the Netty client and server builders.

When enabled, Netty uses a zero-sized HPACK dynamic table and advertises SETTINGS_HEADER_TABLE_SIZE = 0 to its peer. The default remains unchanged.

Also fix grpc-okhttp to apply a peer's header-table setting to its outbound encoder instead of its inbound decoder.

Compliant peers require no configuration changes. Older grpc-okhttp releases must be upgraded before enabling this option on their Netty peer.

OpenAI Codex (GPT-5) was used to implement all changes in this PR.

@linux-foundation-easycla

linux-foundation-easycla Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

@petedmarsh petedmarsh changed the title Disable dynamic hpack Add option to disable HPACK dynamic table Aug 6, 2026
@ejona86

ejona86 commented Aug 6, 2026

Copy link
Copy Markdown
Member

Can you describe what your goal is? It seems like it is to reduce idle memory usage?

The okhttp bug has been on our radar, but needed some further investigation (#12819 . #12818 is similar but I saw it didn't remove hpackReader.headerTableSizeSetting(), so it was highly suspect; it was also AI-generated, and we really want a human to look at it). We will definitely want to keep any changes there as a separate commit.

@petedmarsh

petedmarsh commented Aug 6, 2026 via email

Copy link
Copy Markdown
Contributor Author

@ejona86

ejona86 commented Aug 6, 2026 •

Copy link
Copy Markdown
Member

Can you share what function(s) is hot? Is it the key/value hashing?

Receiving indexed values in HPACK seems reasonably cheap. I expect it is just the encoder costing you.

One thing for us to consider is this approach looks to be incompatible with Http2FrameCodecBuilder. We really need to migrate off the old Netty API that we are using, at which point we'll be limited to what Http2FrameCodecBuilder offers. (So then we have to either drop this feature or upstream it to Netty.)

@petedmarsh

Copy link
Copy Markdown
Contributor Author

We have quite a significant amount of headers (a lot of individual headers, some with high cardinality, and some with high cardinality and size of value), we've observed via production profiling that this is actually casuing signficiant CPU usage.

Here's a benchmark:

https://github.com/petedmarsh/grpc-java/tree/disable-dynamic-hpack-benchmark

## Direct change when disabling the table

Positive throughput is better; negative bytes and CPU are better.

| Value profile | Throughput | Modeled bytes | Process CPU |
| --- | ---: | ---: | ---: |
| Low length | +98.275% | +102.576% | -49.069% |
| High length | +18.114% | +19.040% | -15.381% |

The direct encoder results show statistically resolved CPU and throughput improvements, accompanied
by statistically resolved increases in encoded bytes.

## End-to-end unary gRPC

| Value profile | Dynamic table | Throughput (RPC/s) | Client bytes/RPC | Process CPU (us/RPC) |
| --- | --- | ---: | ---: | ---: |
| Low length | 4 KiB | 24,807.936 +/- 4,357.322 | 350.793 +/- 0.004 | 36.032 +/- 7.189 |
| Low length | Disabled | 25,666.263 +/- 983.054 | 717.585 +/- 0.002 | 34.700 +/- 1.669 |
| High length | 4 KiB | 24,187.775 +/- 1,832.197 | 1,524.586 +/- 0.004 | 36.926 +/- 2.476 |
| High length | Disabled | 25,873.456 +/- 3,399.571 | 1,803.585 +/- 0.002 | 34.459 +/- 4.809 |

### End-to-end change when disabling the table

| Value profile | Throughput | Client bytes | Process CPU |
| --- | ---: | ---: | ---: |
| Low length | +3.460% | +104.561% | -3.697% |
| High length | +6.969% | +18.300% | -6.681% |

The end-to-end CPU and throughput confidence intervals overlap, so those observed changes are not
statistically resolved by this run. The client-byte increases are resolved.

(Benchmark also generated with Open AI / Codex / Sol 5)

@petedmarsh

Copy link
Copy Markdown
Contributor Author

I'm also happy to change this to make the HPACK dynamic table size configurable, rather than just disable-able, in some cases bigger tables might be better.

I also know that AI generated contributions are slightly suspect, but I am confident in the analysis from our production profiling and that the benchmark is at least somewhat representative. This PR itself I am not too concernred about but I would like this functionality in grpc-java (as well as the never index changes in my other pr #12976) if someone has to re-do by hand.

If AI generated code is not an issue then I am happy to adjut - cheers!

@teorosu

teorosu commented Aug 11, 2026

Copy link
Copy Markdown

( for context: i work with @petedmarsh )

hey @ejona86. i tried to correlate our findings with the encoder/decoder code flow.

Can you share what function(s) is hot?

the hot logic is in HpackEncoder.encodeHeader but also some in HpackDecoder (less though on decoder side)

Is it the key/value hashing?

the hashing is a meaningful chunk, but not the majority

  • 2 x AsciiString.hashCode(name/value) per header ( in encodeHeader )
  • ( i think there's more hashing later e.g. on eviction, but i assume you were referring to hashCode above )

the rest is always a miss walk:

  • getEntryInsensitive ( + equalsVariableTime under it) - lookup misses
  • ensureCapacity() -> remove() - > removeNameEntryMatchingCounter + removeNameValueEntry
  • encodeAndAddEntries() -> getEntry + addNameEntry / addNameValueEntry - the insert half

Is it just the encoder?

mostly. the decoder side is smaller, but it's not free:

  • caller sends literal-with-incremental-index => the receiver pays HpackDecoder.insertHeader ( + HpackDynamicTable.add inside )
  • eviction happens on decoder side as well

let me/us know if anything is missing. thank you!

@petedmarsh petedmarsh changed the title Add option to disable HPACK dynamic table Add option to resize HPACK dynamic table Aug 13, 2026
@petedmarsh

Copy link
Copy Markdown
Contributor Author

I have updated this to make it possible to change the size of the table, not just disable it, for two reasons:

  1. This is more generally flexible
  2. Once we take into account bytes transferred and networking fees even with thrashing it might be cheaper to have a bigger table

@ejona86

ejona86 commented Aug 14, 2026

Copy link
Copy Markdown
Member

Looking at the benchmark, I feel like it is making a case that this approach is not appropriate. It shows 100% increase with maybe 2 µs saved, but it is too noisy to really say. "A benchmark got 7%±14% faster". And it is using direct executor and pre-serializing headers, so that this is the best case you'll see and seems pretty far from reality. Also, it shows that HPACK is working, as the number of bytes sent doubles when disabling it (which is surprising to me, because the benchmark looks really pathological yet HPACK was still helping).

#12976 seems like it'd get you all you need and is easier to accept. If that's not sufficient it seems you really need changes to Netty.

... This had me look into other things (like other approaches, optimizations) and took me pretty far afield. Sending what I've written just so I won't lose it over the weekend.

@petedmarsh

Copy link
Copy Markdown
Contributor Author

Thanks for looking into it!

We also did some more benchmarking and came to similar conclusions (never indexing is better than disabling).

Increasing the size of the dynamic table also helped drive down the number of bytes sent for headers. I've changed this PR to allow for the table size to be set rather than just disabled.

@petedmarsh
petedmarsh force-pushed the disable-dynamic-hpack branch from cdc5634 to cea8729 Compare September 23, 2026 11:39
petedmarsh and others added 3 commits September 23, 2026 13:44
Add client and server builder controls that disable HPACK dynamic table use
in both directions. Advertise a zero header table size and keep the encoder
table pinned at zero when the peer changes its setting.

Add encoder, handler, builder, transport, and interoperability coverage.

AI assistance: OpenAI Codex (GPT-5) was used to review these HPACK changes
and strengthen the tests.

Co-authored-by: Codex <noreply@openai.com>
Apply SETTINGS_HEADER_TABLE_SIZE to the outbound HPACK writer before
acknowledging it, so the next header block emits the required dynamic table
size update. Stop applying the peer encoder setting to the inbound decoder.

Add framed unit coverage and bidirectional OkHttp-Netty regression tests.
The tests verify that repeated calls remain on one transport.

AI assistance: OpenAI Codex (GPT-5) was used to implement and test this
grpc-okhttp compatibility fix.
Replace the disable-only Netty client and server options with a byte-size configuration. Keep the default at 4096 bytes, allow zero to disable the dynamic table, and reject negative values. Cap the encoder at the configured size and advertise non-default sizes to the peer.

Co-authored-by: Codex <noreply@openai.com>
@petedmarsh
petedmarsh force-pushed the disable-dynamic-hpack branch from cea8729 to 4fbbd76 Compare September 23, 2026 11:45
@petedmarsh

Copy link
Copy Markdown
Contributor Author

Hi @ejona86!

After more examination, I am still interesting in having this option but to increase HPACK table size rather than set it to 0 (disable).

On that basis would this be more acceptable?

@ejona86

ejona86 commented Sep 23, 2026

Copy link
Copy Markdown
Member

We could increase the receive header table size, but since netty/netty@7e8b325 Netty is limited to 8 KiB for sending, and 4 KiB by default. So this code right now will max out at 4 KiB for sending.

@petedmarsh

Copy link
Copy Markdown
Contributor Author

In that case would something like this:

https://github.com/grpc/grpc-java/compare/master...petedmarsh:grpc-java:hpack-8k-default-master?expand=1

where advertised/accepted table sizes default to 8 kIb be possible to merge?

@ejona86

ejona86 commented Sep 24, 2026

Copy link
Copy Markdown
Member

where advertised/accepted table sizes default to 8 kIb be possible to merge?

Is your system mixed between languages? Because that would have no behavior changes when grpc-java clients and servers are communicating.

@petedmarsh

Copy link
Copy Markdown
Contributor Author

For us it's largely grpc-java servers/clients - perhaps I am mistaken but I thought the changes in https://github.com/grpc/grpc-java/compare/master...petedmarsh:grpc-java:hpack-8k-default-master?expand=1 would:

  1. make grpc servers advertise 8kib hpack tables by default
  2. let grpc clients accept and use 8kib hpack tables by default

(I could well be wrong, sorry if so)

Maybe that is a simpler change than allowing this to be configurable, but perhaps it's too drastic of a change to put out by default too

@ejona86

ejona86 commented Sep 24, 2026

Copy link
Copy Markdown
Member

(I could well be wrong, sorry if so)

Both sides would advertise 8 KiB. But both encoders would limit themselves to 4 KiB because of the maxHeaderTableSize = Math.min(maxHeaderTableSize, nameEntries.length * 64); in netty/netty@7e8b325 and the default nameEntries.length is 64.

... Which I see now is inaccurate, because gRPC actually uses a hash table size of 16 (on both client and server), so is now recently limited to 1 KiB. 16 used to be the old default, but was changed in netty/netty@0df408e .

@petedmarsh

Copy link
Copy Markdown
Contributor Author

Ah! That is probably quite bad for us if it's 1 KiB tables - I will raise a PR against netty (unless its easier/faster for you to do so).

@ejona86

ejona86 commented Sep 24, 2026

Copy link
Copy Markdown
Member

What would be in the Netty PR? I would have expected a gRPC PR to change the hard-coded 16

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants