Summary
The accept loop rebuilds a StackConfig and runs the full build_stack() tower composition (5–6 apply_* factories each calling boxed_clone()) per-bistream rather than per-connection or per-serve-call. Since all stack parameters are fixed for the lifetime of the serve loop, this is unnecessary allocation on the hot path.
Additionally, CompressionOptions is cloned twice per bistream (stack_compression_conn = cfg.stack_compression.clone() per-connection, then req_compression = stack_compression_conn.clone() per-bistream) when it never mutates.
Evidence
crates/iroh-http-core/src/http/server/accept.rs#L202-L237 — inside the per-bistream tokio::spawn:
let req_compression = stack_compression_conn.clone(); // clone per-bistream
// ... later in the spawn:
let stack_cfg = StackConfig {
timeout: ...,
max_request_body_wire_bytes,
max_request_body_decoded_bytes,
load_shed: load_shed_enabled,
compression: req_compression, // moved in
decompression: cfg_decompression,
};
super::pipeline::serve_bistream(io, svc, effective_header_limit, &stack_cfg).await;
serve_bistream then calls build_stack(svc, cfg) which runs apply_body_limit → apply_decompression → apply_compression → apply_timeout → apply_load_shed, each calling .boxed_clone().
In axum's reference shape, the full ServiceBuilder chain is composed once from the user's service before the accept loop. The per-connection task only wraps AddExtensionLayer (for RemoteNodeId) and hands the pre-built stack to serve_connection.
Impact
- Per-bistream: 5–6 heap allocations (
boxed_clone) + 1 CompressionOptions clone.
- Under high request throughput this is measurable allocator pressure. On a single peer opening many bistreams (multiplexed HTTP/1.1 pipelining over QUIC) the cost multiplies.
- Functionally correct — no bugs, just waste.
Remediation
- Build the
StackConfig once at the top of the connection task (outside the loop { accept_bi } loop). All fields come from AcceptConfig which is immutable.
- Wrap
CompressionOptions in Arc<Option<CompressionOptions>> — clone the Arc instead of the struct.
- Ideally, call
build_stack(conn_svc, &stack_cfg) once per connection and clone the resulting ServeService (which is already BoxCloneService) per-bistream. This mirrors the axum shape where the stack is pre-built and the per-request cost is a single Arc::clone.
Step 3 requires verifying that no per-bistream state leaks into the stack (currently none does — StackConfig fields are all stateless layer toggles).
Acceptance criteria
StackConfig is constructed once per connection task, not per bistream.
CompressionOptions is Arc-wrapped or built once — no Clone per bistream.
build_stack() is called at most once per connection (or once at serve-start if the RemoteNodeId extension can be injected differently).
- Existing tests pass; no behaviour change.
npm run ci green.
Summary
The accept loop rebuilds a
StackConfigand runs the fullbuild_stack()tower composition (5–6apply_*factories each callingboxed_clone()) per-bistream rather than per-connection or per-serve-call. Since all stack parameters are fixed for the lifetime of the serve loop, this is unnecessary allocation on the hot path.Additionally,
CompressionOptionsis cloned twice per bistream (stack_compression_conn = cfg.stack_compression.clone()per-connection, thenreq_compression = stack_compression_conn.clone()per-bistream) when it never mutates.Evidence
crates/iroh-http-core/src/http/server/accept.rs#L202-L237 — inside the per-bistream
tokio::spawn:serve_bistreamthen callsbuild_stack(svc, cfg)which runsapply_body_limit → apply_decompression → apply_compression → apply_timeout → apply_load_shed, each calling.boxed_clone().In axum's reference shape, the full
ServiceBuilderchain is composed once from the user's service before the accept loop. The per-connection task only wrapsAddExtensionLayer(forRemoteNodeId) and hands the pre-built stack toserve_connection.Impact
boxed_clone) + 1CompressionOptionsclone.Remediation
StackConfigonce at the top of the connection task (outside theloop { accept_bi }loop). All fields come fromAcceptConfigwhich is immutable.CompressionOptionsinArc<Option<CompressionOptions>>— clone theArcinstead of the struct.build_stack(conn_svc, &stack_cfg)once per connection and clone the resultingServeService(which is alreadyBoxCloneService) per-bistream. This mirrors the axum shape where the stack is pre-built and the per-request cost is a singleArc::clone.Step 3 requires verifying that no per-bistream state leaks into the stack (currently none does —
StackConfigfields are all stateless layer toggles).Acceptance criteria
StackConfigis constructed once per connection task, not per bistream.CompressionOptionsisArc-wrapped or built once — noCloneper bistream.build_stack()is called at most once per connection (or once at serve-start if theRemoteNodeIdextension can be injected differently).npm run cigreen.