Files
Flash5/flash/docs/http2/PERFORMANCE.md
T

6.0 KiB
Raw Blame History

HTTP/2 performance

Method

Measurements were taken on 2026-08-13 under Linux 6.12/KVM with six exposed AMD Ryzen 7 1700X cores, Temurin 21.0.11, JMH 1.37 and nghttp2 1.59.0. JMH component benchmarks use prepared, reusable protocol state and forked JVMs. h2load exercises the real cleartext server on loopback; Flash and nghttpd run on the same host in alternating order. Results are snapshots, not promises for different hardware.

No "unmatched throughput" claim is supported. nghttpd is normally faster in this matrix; Flash's numbers include framework routing, request-model assembly and handler dispatch that the static reference server does not.

Component results

The CI-controlled allocation and percentile numbers are in BASELINES.md. Additional average time measurements from the same run were:

Scenario Result
h2 responses across 1 live stream 74.405 ns
h2 responses across 8 live streams 705.368 ns
h2 responses across 64 live streams 5,591.368 ns
h2 responses across 256 live streams 28,961.681 ns
h2 POST lifecycle with 1 KiB DATA 741.031 ns, 0.005 B/op
1 MiB streaming response 78,681.815 ns

The multiplexing benchmark reports one complete response-encoding pass across all live streams, not per-stream time. Http2BodyBenchmark separately covers the 1 KiB request-body shape and the 1 MiB response shape. FrameWriterBenchmark retains the Phase 3 contention matrix and its per-write latency distribution.

End-to-end h2load comparison

Each row uses at least 1,000 requests. Requested stream concurrency is capped first by Flash's advertised 64-stream setting and then to 4,096 aggregate active streams so the 1,000-connection rows remain bounded. Both requested and effective values are shown.

Connections Requested/effective streams Flash req/s nghttpd req/s
1 1 / 1 2,136.18 12,786.42
1 10 / 10 18,396.56 83,521.26
1 100 / 64 17,039.55 66,746.76
10 1 / 1 11,247.08 37,838.66
10 10 / 10 25,055.12 104,964.84
10 100 / 64 3,878.28 67,303.81
100 1 / 1 5,517.94 42,319.09
100 10 / 10 1,818.52 26,732.25
100 100 / 40 7,042.85 136,585.90
1,000 1 / 1 1,393.17 3,877.62
1,000 10 / 4 10,374.83 40,976.05
1,000 100 / 4 49,622.10 85,344.40

The matrix found a correctness issue before it produced these final numbers: closed streams still occupied live admission slots while their final write callback was pending. The bounded detach fix is recorded as EX-57 and covered by regression tests.

Tuning decisions

Knob Measurement Decision
16 KiB / 64 KiB / 1 MiB response frame 1 MiB stream: 104,071 / 97,996 / 98,769 ns in the non-Huffman sweep Keep 16 KiB. The roughly 6% gain at 64 KiB does not justify 4x per-connection buffer exposure on this noisy host.
1 MiB initial receive window 100 MiB Phase 11 transfer and the load matrix complete without flow stalls Keep; it matches bounded receive capacity and changing it independently would not isolate a throughput claim.
half-window WINDOW_UPDATE hysteresis 1 MiB streaming and 100 MiB transfer complete with steady pooled reads Keep; no per-frame update traffic and no demonstrated reason to weaken backpressure.
64 KiB inline body 1 KiB inline materialization is one 1,040 B allocation; streaming steady state is ≈0 B/op Keep the explicit one-array small-body tradeoff and stream larger bodies.
64 × 16 KiB DATA buffers 1 MiB streaming is 78,682 ns with ≈0 B/op; h2load stays bounded Keep; larger chunks did not produce a clear win beyond the frame-size sweep.
ScratchPool bound 64 objects per exposed CPU, capped at 4,096; full 1,000-connection matrix completes Keep the capacity bound; it affects retained burst memory, not steady-state request instructions.
word-at-a-time route compare 14.289 ns versus 22.313 ns bytewise, 36.0% faster Keep.
SWAR header-end scan 89.919 ns versus 128.460 ns scalar, 30.0% faster Keep.
SlicePool size 4 Header/path/query view benchmarks remain allocation-free Keep; size changes lifetime capacity, not lookup work, and four simultaneous borrowed views match the documented contract.
runtime-value Huffman representative response headers: 375.761 ns versus 180.129 ns at 16 KiB Keep disabled by default; this header set is 109% slower to encode.

The frame-size/Huffman factorial produced counterintuitive variation in the body-only rows, so it was not used to claim a Huffman body effect: Huffman only prepares headers. This is treated as host noise rather than reverse-engineered into a preferred result.

Profiling

async-profiler 4.4 was run against the representative browser HPACK decode. The top CPU leaves were Huffman.decode (72.67%), HpackHeaderBlock.accept (7.00%), HpackDecoder.decode (5.00%), JVM byte-array copy (4.00%), HpackHeaderBlock.copy (4.00%), HpackStaticTable.name (2.00%), PooledSlice.reset (1.00%), PooledSlice.array (0.67%), JVM byte-arraycopy (0.67%), and HpackDecoder.decodeString (0.67%). Each belongs to decoding, bounded arena ownership, or the copy that makes header lifetime independent of dynamic-table eviction; none is incidental locking or logging.

The allocation profile produced no samples on the gated decode path. The realistic eight-writer lock profile produced no sampled contended locks; the writer benchmark measured 185.605 bursts/s, p50 1.2 µs, p99 5.3 µs and p999 41.6 µs. CPU, allocation and lock artifacts were generated under /tmp/phase17-* and are intentionally not committed.

CI runs the complete suite with -Djdk.tracePinnedThreads=full. The forked allocation and p99 gates run only under the Maven jmh profile; the h2load comparison remains informational and conditional because cross-runner throughput is not a stable correctness gate.

The reconstructed Phase-0 HTTP/1 comparison is documented in BASELINES.md. Its confidence interval overlaps the Phase-17 result, while normalized allocation falls from 224.007 B/op to 0.007 B/op.