Skip to content

perf: bound streaming memory and offload command processing - #527

Draft
Mohamed Mansour (mohamedmansour) wants to merge 2 commits into
mainfrom
mohamedmansour/bounded-stream-processing
Draft

perf: bound streaming memory and offload command processing#527
Mohamed Mansour (mohamedmansour) wants to merge 2 commits into
mainfrom
mohamedmansour/bounded-stream-processing

Conversation

@mohamedmansour

@mohamedmansour Mohamed Mansour (mohamedmansour) commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Draft: do not merge this combined change

The measurements justify splitting this work rather than merging the combined
patch. The independently reviewable CLI improvement is #529, and bounded
watcher hashing is #530. The isolated pool-retention fix is #532, now with
completed allocation/CPU measurements and explicit tradeoffs: recurring
1 MiB attributes are about 50% slower, and paced 1 MiB raw responses use
about 38% more combined producer/consumer CPU despite unchanged rate-limited
completion time.

The 4/16/64 KiB transport experiments are complete. None is recommended as
a new general default.
Hard 64 KiB is the least costly bounded option, but
large pooled values still regress about 2-3 times in transport. Equal-target
soft controls obtain most of the mixed/SSR benefit without that hard-cap
penalty. The corrected local HTTP results do not establish a universal winner.
The follow-up harness commit is benchmark-only; it does not select a new
production policy. This PR still contains the original held hard-4-KiB proposal.

Completed transport/pool report: all 120 timed cells, 76 allocation-accounting
rows, controls, output hashes, and limitations
.

The reproducible harness is published in benchmark-only commit
e337eec9.
Initial Nagle/delayed-ACK-confounded HTTP rows are retained but excluded from
the recommendation; corrected HTTP explicitly enables and verifies TCP_NODELAY.

The hard-4-KiB transport change remains experimental because the recorded
large-single-write workload regresses approximately 34-45 times. This draft
retains the original code and complete measurement history; neither the
external SSR results nor the CLI-only measurements establish a net benefit
for the hard-chunking implementation.

Original combined summary

Apply the relevant lessons from astral-sh/uv#21372: keep synchronous processing together on an existing blocking worker, transfer ownership instead of copying large values, bound the bytes between producer and consumer, and reuse bounded scratch storage.

  1. Move freshly decoded start/resume state into the CLI streaming continuation instead of passing borrowed values that require cloning.
  2. Make StreamingWriter::with_chunk_size a hard byte limit for raw and attribute output, reject oversized idle pool returns, and retain consumer-owned recycling. Keep the tiny-write fast path and try a nonblocking send before waiting.
  3. Send owned, size-checked NDJSON records through the existing capacity-one channel. Deserialize, validate command order, and prepare defaults on the existing blocking renderer, without spawning a worker per command.
  4. Hash watched files incrementally with one reusable 8 KiB buffer per watcher, rather than allocating a whole-file content buffer for each event.

Update production pool construction, examples, the living specification, public Rust integration/performance documentation, and benchmark documentation. Add bounded transport/tiny-write and watcher-hashing benchmarks, and register watcher hashing in cargo xtask bench all.

This is not a uniform throughput improvement. The isolated CLI processing benchmark improves substantially for large state, but enforcing 4 KiB transport chunks makes a single 1 MiB write roughly 34-45 times slower in the bounded concurrent microbenchmark because one oversized send becomes 256-257 sends. Watcher hashing also regresses for the 1 MiB and small-file-burst cases. These tradeoffs are included below, not hidden behind the faster cases. No whole-server speedup or process-RSS improvement is claimed.

External canonical suite follow-up

The requested local-source run of microsoft/webui-benchmarks is reported in full here: all 16 SSR cases in both modes, latency/RPS/peak RSS, overload counts, source/runtime provenance, and browser coverage. The 160-round headed browser run had 11 failures (ten Lit correctness failures and one Deno display failure), so no official browser numeric report or homepage summary was produced. A necessary one-line benchmark-adapter API alignment remains uncommitted and is disclosed in the measured source metadata. These current-checkout integration results are not a before/after comparison.

Behavioral and memory contracts

Surface Contract
Transport Every emitted chunk is at most the configured byte limit: default 4,096 bytes, minimum 64. Concatenated output and semantic checkpoint flush positions are unchanged.
Default queue Four slots hold at most 16,384 bytes of queued payload. This is not a total-response-memory bound: producer buffers, pending sends, consumer-held chunks, spare capacity, state, serialization scratch, HTTP buffers, and channel/ownership metadata are additional.
Pool Idle summed Vec capacity is bounded by max_pool.max(1) * chunk_size. Oversized returns are dropped, not retained or shrunk. A buffer returns only after the final Bytes consumer reference is released. Bytes::from_owner still allocates ownership metadata.
UTF-8 A transport chunk can split a code point. Forward bytes, concatenate them, or decode incrementally; do not independently UTF-8-decode each chunk.
Owned session StreamingSession still returns a complete semantic-step Vec, not fixed-size transport chunks.
CLI Keep protocol version 2, the 2,000,000-byte record cap, 4,000,000-byte precommit cap, cancellation, truncation, error precedence, and backpressure. Initial invalid commands remain HTTP 502; initial rendering failures remain HTTP 500.
Watcher Preserve the digest, unchanged-save suppression, and retry/invalidation behavior. Validate file metadata, handle short and interrupted reads, and read at most 8 MiB plus one overflow-probe byte. Failed or oversized reads never cache a partial digest.

No public API, protocol field, or production dependency is added. Criterion is an existing workspace dependency added only to the dev-server's dev-dependencies.

Benchmark environment and scope

Baseline: f43db65067d48581ed7f99acc4b8d68f201c8bb9.

Recorded host: Intel Core Ultra 9 285K, x86_64 Linux 6.6.114.1-microsoft-standard-WSL2; Rust/Cargo 1.98.0; optimized release/bench builds with thin LTO. These are local measurements, not cross-platform guarantees. The Criterion measurements ran in a shared environment and are sensitive to load, cache state, and CPU frequency.

All measured cases are reported below, including the repeated tiny-write run and the unchanged-handler comparison that originally motivated the ownership change. CLI P50/P95, transport Criterion estimates, and watcher Criterion medians are different statistics and are labeled accordingly.

1. Real CLI streaming pipeline: owned state plus worker-side parsing

Method

Measure actual render() through to_bytes() using prebuilt NDJSON and an in-memory backend stream with 16 KiB chunks. This is the CLI response pipeline, not TCP or a full-server benchmark. Start/resume each retain a large rows subtree; resume changes that subtree. Components projection keeps rendered output small to isolate state processing.

Three sequential before/after pairs were pinned to CPUs 8 and 9. Each run/case used 40 warmups and 180 timed latency responses: 540 timed responses per variant/case. A separate responsiveness phase used 40 warmups and 90 timed responses per run: 270 timed responses per variant/case.

Only the CLI owned-state/worker-processing patch differs between the measured source snapshots. Both use the original transport, dependencies, and watcher implementation. The later test-helper pool-size alignment was excluded. The two optimizations were not independently ablated.

Input serialization, configuration construction, stream-chunk clones, output-byte comparison, and CSV writing are outside the timing window. Process and current-event-thread CPU use Linux CLOCK_PROCESS_CPUTIME_ID and CLOCK_THREAD_CPUTIME_ID.

Case Total input bytes, two records Output bytes
Small 471 487
Approximately 64 KiB state per record 131,255 487
Approximately 1 MiB state per record 2,097,303 487

Each individual record is below the 2 MB cap. All output bodies and checkpoints matched byte-for-byte across every case, variant, warmup, and measured response.

Latency and CPU

All entries are microseconds, median / P95, pooled across the three runs. P50 is the sample median; P95 is sorted sample index floor((n - 1) * 0.95).

State Metric Before After
Small Wall 84.056 / 180.574 76.818 / 131.396
Small Process CPU 36.426 / 84.898 29.0185 / 60.674
Small Event-thread CPU 18.4235 / 42.386 10.699 / 23.382
64 KiB Wall 1468.1425 / 2108.629 996.589 / 1440.722
64 KiB Process CPU 1404.395 / 1980.192 914.825 / 1319.581
64 KiB Event-thread CPU 853.834 / 1275.773 41.839 / 81.282
1 MiB Wall 39601.5925 / 48493.216 21582.768 / 26279.345
1 MiB Process CPU 39481.3355 / 48401.554 21478.9435 / 26331.253
1 MiB Event-thread CPU 11949.8735 / 17647.320 379.340 / 473.388

For the 1 MiB case, pooled median wall latency falls from about 39.60 ms to 21.58 ms, and median event-thread CPU falls from about 11.95 ms to 0.379 ms. This supports the combined ownership/offloading change for this workload, not a general end-to-end server throughput claim.

Cooperative event-loop responsiveness

The separate ticker phase runs a continuously yielding task on the current thread. Each response records its maximum inter-tick gap. Entries below are microseconds, median / P95 of those per-response maxima; they are not percentiles of all individual ticks.

State Before After
Small 5.044 / 16.024 2.8065 / 5.679
64 KiB 412.1565 / 487.106 10.717 / 63.593
1 MiB 6147.572 / 8683.731 167.349 / 313.909

Per-pair median changes provide an additional view of run variability. Negative values mean lower latency or shorter gaps.

State Wall latency: pair 1 / 2 / 3 Maximum ticker gap: pair 1 / 2 / 3
Small -14.38% / -22.48% / -9.88% -46.20% / -43.72% / -43.00%
64 KiB -30.98% / -35.93% / -30.95% -96.57% / -96.97% / -98.13%
1 MiB -46.09% / -49.43% / -38.66% -97.14% / -98.04% / -97.00%
Full ticker-phase wall/CPU measurements (spinner-affected; not throughput evidence)

The spinner changes scheduling and contributes CPU usage. Do not substitute this table for the no-spinner latency/CPU table above. All entries are microseconds, median / P95.

State Metric Before After
Small Wall 49.8725 / 101.093 47.909 / 74.364
Small Process CPU 61.7485 / 124.655 55.113 / 91.069
Small Event-thread CPU 50.1895 / 100.583 47.7805 / 74.518
64 KiB Wall 1443.5595 / 1913.863 939.9415 / 1315.568
64 KiB Process CPU 1692.303 / 2432.949 1330.961 / 2239.485
64 KiB Event-thread CPU 1447.104 / 1923.676 936.638 / 1312.748
1 MiB Wall 40166.8755 / 46330.087 21080.9945 / 29441.506
1 MiB Process CPU 65661.102 / 74253.069 40255.348 / 56726.748
1 MiB Event-thread CPU 40210.6795 / 46270.554 21180.674 / 29391.633

RSS observations: inconclusive

Whole test-process high-water RSS, KiB Run 1 Run 2 Run 3
Before 54,372 54,136 55,336
After 50,736 61,040 82,960

These high-water marks include setup, warmups, the ticker phase, and allocator retention. They establish neither an improvement nor a regression in request memory. No CLI allocation-count measurement was collected.

Reproduction and artifact identity

The Linux-only measurement harness and reproduction steps are provided in the reproduction appendix so the transient CLI benchmark is reproducible without adding a production benchmark API.

Build isolated baseline snapshots, apply only the CLI patch to the after snapshot, and include the identical harness inside both existing streaming_api::tests modules. Build optimized CLI test executables with cargo test --release -p microsoft-webui-cli --no-run.

When sharing a Cargo target across archived snapshots, force a CLI rebuild with cargo clean --release -p microsoft-webui-cli before building the second variant: same-mtime snapshots can otherwise be treated as fresh. Separate target directories also avoid this hazard. Distinct executable hashes confirmed the measured variants.

WEBUI_BENCH_ITERATIONS=180 WEBUI_BENCH_OUTPUT=before-1 \
  taskset -c 8,9 ./before-test measured_cli_render_pipeline --nocapture --test-threads=1
WEBUI_BENCH_ITERATIONS=180 WEBUI_BENCH_OUTPUT=after-1 \
  taskset -c 8,9 ./after-test measured_cli_render_pipeline --nocapture --test-threads=1

Repeat for pairs 2 and 3. Pool raw per-response CSV samples by phase/case/variant using the percentile definition above; compute per-pair medians separately.

Artifact Recorded SHA-256
Baseline executable 8e492c406cd76e9af07a03a1b6bd9ad1a2685190646975a1052e0896665e7cf5
CLI-only after executable 47348210a1432b33dc2d856f1d237a93bbaa1e98b581fef95c66f3b3dc6836e2
Identical 487-byte HTML output, all cases 263e3ada3df1757302f55546644a1e8864a7513310037e1e090a25e3b58df260

Executable hashes identify the measured binaries; they are not a promise of reproducible binary hashes on another toolchain/path.

2. Bounded streaming transport

Method

Extend the existing Criterion harness before implementing the transport changes, then run the same cases before/after. Optimized builds; 20 samples, 500 ms warmup, 1 s target measurement.

transport uses a persistent concurrent consumer and a four-slot channel. Timing includes backpressure and acknowledgment of final consumption, but excludes thread startup. transport_tiny removes cross-thread scheduling to isolate small writes.

These are Criterion point estimates with their printed confidence intervals, not extracted P50 values or measured CPU time. Short runs show shared-host/frequency variability, so both after tiny-write runs are retained.

Concurrent transport

Entries are microseconds, point estimate [lower, upper].

Case Before After
1 MiB raw, unpooled 62.748 [61.327, 64.639] 2848.8 [2764.6, 2928.4]
1 MiB raw, pooled 68.028 [64.630, 71.077] 2791.7 [2657.3, 2908.6]
1 MiB attribute, unpooled 91.506 [87.636, 95.712] 3129.3 [2751.7, 3499.3]
1 MiB attribute, pooled 69.894 [66.708, 73.383] 2462.1 [2389.4, 2548.7]
16 KiB many small writes, unpooled 36.945 [35.227, 38.879] 40.940 [37.443, 43.838]
16 KiB many small writes, pooled 40.312 [38.264, 42.189] 42.233 [40.250, 45.468]

Tiny writes

Entries are nanoseconds, point estimate [lower, upper].

Case Before After After repeat
Raw, unpooled 344.05 [342.13, 346.06] 310.09 [306.38, 314.08] 245.15 [243.74, 247.03]
Raw, pooled 339.36 [334.91, 344.52] 314.91 [311.32, 318.74] 270.36 [267.38, 273.75]
Attributes, unpooled 480.39 [474.81, 485.03] 482.94 [457.43, 521.21] 419.99 [411.21, 427.58]
Attributes, pooled 475.35 [469.18, 482.08] 517.11 [491.38, 557.27] 409.39 [402.65, 414.70]

Interpretation and memory bounds

The old 1 MiB raw write emitted one giant chunk; the new hard default maximum emits 256 chunks, or 257 for the quoted-attribute case. The large-single-value slowdown is a real bounded-memory/backpressure tradeoff. Hosts can choose a larger bounded maximum through the existing with_chunk_size setting when throughput matters more than a small queued-byte ceiling.

The default unpooled active-buffer capacity request falls from 5,120 to 4,096 bytes (-20%). The queue payload is now bounded by 16 KiB. A 1 MiB-capacity return to a 4 KiB pool is rejected instead of retained; idle capacity obeys the formula above. These are source-derived buffer/queue bounds, not measured RSS or complete allocation elimination.

The older writer_paths group and resource example buffer an entire response before draining their channels. They are not evidence of slow-consumer memory bounds.

Reproduction

Use the same new benchmark harness against baseline and changed implementations, retaining identical benchmark settings.

# Baseline implementation with the new transport benchmark cases:
cargo bench -p microsoft-webui --bench streaming_bench -- \
  transport --sample-size 20 --warm-up-time 0.5 --measurement-time 1 \
  --save-baseline uv-before

# Changed implementation:
cargo bench -p microsoft-webui --bench streaming_bench -- \
  transport --sample-size 20 --warm-up-time 0.5 --measurement-time 1 \
  --baseline uv-before

# Retained after repeat, without cross-thread scheduling:
cargo bench -p microsoft-webui --bench streaming_bench -- \
  transport_tiny --sample-size 20 --warm-up-time 0.5 --measurement-time 1

3. Watcher hashing

Method

Optimized Criterion builds, 50 samples, 1 s warmup, 3 s target measurement. Baseline runs the original metadata + fs::read + DefaultHasher::write implementation through the same private module/harness. After runs the incremental 8 KiB implementation.

File opening and metadata are timed; fixture creation is not. These are hot-page-cache microbenchmarks in a shared environment, not end-to-end rebuild measurements. The table uses extracted median/P50 estimates and median 95% confidence intervals, not Criterion's printed regression/mean estimates.

Case Before median, us [95% CI] After median, us [95% CI] Median change
1 KiB file 2.8852 [2.7815, 3.0209] 2.7888 [2.7253, 2.9230] -3.34%
1 MiB file 189.3235 [187.3793, 194.0550] 215.5880 [209.4294, 223.4490] +13.87%
8 MiB file 3261.7713 [2892.7735, 3534.1622] 1992.3002 [1961.0281, 2176.1765] -38.92%
32 x 1 KiB burst 101.7087 [99.9075, 109.5873] 122.4937 [113.3522, 131.4069] +20.44%
32 x 256 KiB burst 2327.7129 [2198.5900, 2433.0210] 1983.9213 [1936.6086, 2088.3039] -14.77%

The 1 MiB file and small-file burst regress in this run. Additional bounded read syscalls are a real potential CPU/latency cost; this is not a claim of uniformly faster hashing.

Allocation/storage change

These are source-derived, not allocator/RSS measurements.

Property Before After
Normal nonempty-file content buffer One file-sized Vec allocation per hash No per-file content-buffer heap allocation
Content working storage at 8 MiB cap 8 MiB One 8,192-byte scratch buffer; 1024 times smaller
32 x 256 KiB burst 32 content allocations, 8 MiB cumulative content allocation Reuse the same 8 KiB scratch
Live content scratch between events No live file-content buffer 8 KiB retained per watcher

Path, event, and hash-map allocations remain. Allocators can retain freed buffers, so these bounds must not be presented as process-RSS reductions.

Reproduction

Use identical fixtures and the private hashing-module benchmark entry point for both implementations.

cargo bench -p microsoft-webui-dev-server --bench watch_hash_bench -- \
  --sample-size 50 --warm-up-time 1 --measurement-time 3 --save-baseline uv-before
cargo bench -p microsoft-webui-dev-server --bench watch_hash_bench -- \
  --sample-size 50 --warm-up-time 1 --measurement-time 3 --baseline uv-before

Read the median.point_estimate and median.confidence_interval values from each case's Criterion estimates.json; convert nanoseconds to microseconds. The PR adds this benchmark to cargo xtask bench all.

4. Existing handler API comparison: context only

This benchmark ran on the unchanged baseline before the CLI modifications. It compares existing APIs; it is not a before/after result for this patch, and the owned-state and parsing-placement effects are not independently isolated by it.

128 rows, eight boundaries; 100 Criterion samples, 3 s warmup, 5 s target measurement. Buffered and streaming output sizes differ. The three streaming modes share the streaming output shape.

Existing API Output bytes Time, us: point [interval] Throughput, MiB/s: point [interval]
Buffered 9,166 11.248 [11.119, 11.394] 777.17 [767.22, 786.13]
Streaming fused 11,199 15.207 [15.114, 15.305] 702.31 [697.82, 706.65]
Streaming split, borrowed 11,199 53.675 [52.724, 54.843] 198.98 [194.74, 202.57]
Streaming split, owned 11,199 40.589 [39.434, 41.620] 263.13 [256.61, 270.84]
cargo bench -p microsoft-webui-handler --bench streaming_hydration_bench -- \
  streaming_large_state_compare --save-baseline uv-before

Coverage and validation

Regression coverage includes raw-record ownership and channel backpressure; worker-side defaults and initial command errors; parse failure while a backend stalls; exact transport byte caps and UTF-8 splits; attributes across chunk boundaries; timeout/disconnect propagation and cached flush errors; pool capacity and final-consumer lifetime; slow consumers; whole-file digest equivalence; arbitrary short/interrupted reads; exact-cap and growing files; and retry/recreation after failed reads.

Check Result
CLI focused streaming selection 24 passed
Streaming writer tests 28 passed in debug and 28 in release
Dev-server tests 49 passed
xtask benchmark registry tests 8 passed
Streaming library / dev-server all-target Clippy Passed
Scoped framework reviews No significant findings
Final precommit cargo xtask check Passed, 103.3 s

The complete gate included license headers, formatting, production Clippy, protobuf drift, dependency audit, workspace tests/builds, WASM, examples, benchmark smoke, and docs. A session-local official protoc was supplied through PROTOC because the system executable was absent; no repository toolchain workaround was committed.

Exploratory CLI test-target Clippy also exposed pre-existing unwrap/expect lint debt outside the changed streaming module. That stricter invocation is not claimed to pass; the repository's standard production-Clippy and workspace-test gate passes.

Move owned CLI start/resume state and JSON command preparation onto the existing blocking renderer. Enforce transport chunk and idle pool capacity limits, and reuse bounded scratch for watcher hashing. Add regression tests, before/after benchmark cases, and documentation for the memory and throughput tradeoffs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mohamedmansour

Copy link
Copy Markdown
Contributor Author

CLI benchmark reproduction appendix

This is the transient, Linux x86_64 measurement harness used for the CLI tables in the PR description. It uses the real private response pipeline and existing test fixtures; it is not a new public benchmark API or part of the product build.

  1. Create two isolated source snapshots of baseline f43db65067d48581ed7f99acc4b8d68f201c8bb9.
  2. In the after snapshot only, apply this PR's changes to crates/webui-cli/src/commands/serve/streaming_api.rs. Leave every other file on the baseline implementation. Keep the test helper's pool capacity at the original StreamingWriter::CHUNK_TARGET + 1024 in both snapshots; the later pool-alignment change was not part of the measured CLI-only patch.
  3. Save the source below as an absolute-path harness.rs outside both snapshots. Add include!("/absolute/path/to/harness.rs"); inside each snapshot's existing streaming_api::tests module.
  4. Run cargo test --release -p microsoft-webui-cli --no-run for each snapshot and copy the emitted CLI unit-test executable to before-test or after-test. Use separate target directories, or run cargo clean --release -p microsoft-webui-cli in the isolated target before rebuilding the second variant. The latter was necessary during measurement because same-mtime archived sources could otherwise reuse the first artifact.
  5. Run the two commands below sequentially for suffixes 1, 2, and 3, not concurrently. Preserve the emitted CSVs and HTML, and compare HTML across variants.
WEBUI_BENCH_ITERATIONS=180 WEBUI_BENCH_OUTPUT=before-1 \
  taskset -c 8,9 ./before-test measured_cli_render_pipeline --nocapture --test-threads=1
WEBUI_BENCH_ITERATIONS=180 WEBUI_BENCH_OUTPUT=after-1 \
  taskset -c 8,9 ./after-test measured_cli_render_pipeline --nocapture --test-threads=1

The CSVs contain every timed response. For the PR's aggregate tables, pool all three files per variant, group by phase and case, use the ordinary sample median for P50, and use sorted index floor((n - 1) * 0.95) for P95. Compute per-pair changes from each run's median separately. The harness's per-run console percentile helper selects a sorted sample rather than averaging the two middle samples; the PR tables use pooled CSV medians, not those rounded console lines.

The Linux clock IDs are 2 for process CPU and 3 for current-thread CPU. The ticker phase is deliberately separate because its yielding spinner changes CPU consumption and scheduling. No TCP, full server, allocator counter, or timing assertion is involved.

Full measurement harness
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#[allow(unsafe_code)]
mod pipeline_benchmark_clock {
    #[repr(C)]
    struct Timespec {
        seconds: std::os::raw::c_long,
        nanos: std::os::raw::c_long,
    }

    unsafe extern "C" {
        fn clock_gettime(clock: std::os::raw::c_int, time: *mut Timespec) -> std::os::raw::c_int;
    }

    pub(super) fn nanos(clock: i32) -> u64 {
        let mut time = Timespec { seconds: 0, nanos: 0 };
        // SAFETY: Linux clock_gettime writes a valid repr(C) timespec to this live pointer.
        assert_eq!(unsafe { clock_gettime(clock, &mut time) }, 0);
        (time.seconds as u64) * 1_000_000_000 + time.nanos as u64
    }
}

fn pipeline_benchmark_config() -> RenderConfig {
    let mut fragments = vec![
        WebUIFragment::raw("<html><head>"),
        structural("head_start"),
        structural("head_end"),
        WebUIFragment::raw("</head><body>"),
        structural("body_start"),
        WebUIFragment::raw("<pre>"),
        WebUIFragment::signal("rows.0.name", false),
        WebUIFragment::raw("</pre>"),
        WebUIFragment::boundary(0, "index.html", "content", None),
        WebUIFragment::raw("<main>"),
        WebUIFragment::signal("rows.0.name", false),
        WebUIFragment::raw("</main>"),
        WebUIFragment::boundary_end(0),
        WebUIFragment::raw("<footer>"),
        WebUIFragment::signal("rows.0.name", false),
        WebUIFragment::raw("</footer>"),
        structural("body_end"),
        WebUIFragment::raw("</body></html>"),
    ];
    fragments.shrink_to_fit();
    let mut document = WebUIProtocol::new(HashMap::from([(
        "index.html".to_owned(),
        FragmentList { fragments, contains_boundary: true },
    )]));
    document.initial_state_strategy = webui_protocol::InitialStateStrategy::Components as i32;
    render_config(document)
}

fn pipeline_benchmark_config_copy(source: &RenderConfig) -> RenderConfig {
    RenderConfig {
        protocol: Arc::clone(&source.protocol),
        entry: source.entry.clone(),
        route_path: source.route_path.clone(),
        plugin: None,
        body_inject: None,
        chunk_pool: Arc::clone(&source.chunk_pool),
    }
}

fn pipeline_benchmark_input(target_bytes: usize) -> (Vec<Bytes>, usize, usize) {
    let row = serde_json::json!({
        "name": "row",
        "payload": "x".repeat(64),
        "nested": { "enabled": true, "ids": [1, 2, 3, 4] }
    });
    let row_bytes = serde_json::to_vec(&row).unwrap().len() + 1;
    let count = (target_bytes / row_bytes).max(1);
    let mut state = serde_json::json!({
        "rows": { "0": { "name": "start" }, "items": vec![row; count] }
    });
    let mut start = serde_json::to_vec(&serde_json::json!({
        "type": "start", "version": VERSION, "state": state
    })).unwrap();
    state["rows"]["0"]["name"] = serde_json::json!("resume");
    state["rows"]["items"][0]["name"] = serde_json::json!("changed");
    let resume = serde_json::to_vec(&serde_json::json!({
        "type": "resume",
        "boundary": { "owner": "index.html", "name": "content" },
        "state": state
    })).unwrap();
    assert!(start.len() < MAX_RECORD_BYTES && resume.len() < MAX_RECORD_BYTES);
    start.push(b'\n');
    start.extend_from_slice(&resume);
    start.push(b'\n');
    let total_bytes = start.len();
    let chunks = start.chunks(16 * 1024).map(Bytes::copy_from_slice).collect();
    (chunks, count, total_bytes)
}

fn pipeline_benchmark_percentile(values: impl Iterator<Item = f64>, percentile: usize) -> f64 {
    let mut values: Vec<_> = values.collect();
    values.sort_by(f64::total_cmp);
    values[(values.len() - 1) * percentile / 100]
}

#[actix_web::test]
async fn measured_cli_render_pipeline() {
    use std::cell::Cell;
    use std::fmt::Write as _;
    use std::rc::Rc;
    use std::time::Instant;

    let output_prefix = std::env::var("WEBUI_BENCH_OUTPUT").unwrap();
    let measure_count = std::env::var("WEBUI_BENCH_ITERATIONS")
        .ok().map(|value| value.parse::<usize>().unwrap()).unwrap_or(400);
    let ticker_count = (measure_count / 2).max(1);
    let mut raw = String::from(
        "phase,case,iteration,wall_us,process_cpu_us,event_cpu_us,max_tick_gap_us,tick_count\n"
    );
    let config = pipeline_benchmark_config();
    for (name, target) in [("small", 256), ("64k", 65_536), ("1m", 1_048_576)] {
        let (chunks, rows, bytes) = pipeline_benchmark_input(target);
        let response = render(
            tokio_stream::iter(chunks.iter().cloned().map(Ok::<_, String>).collect::<Vec<_>>()),
            pipeline_benchmark_config_copy(&config),
            defaults(),
        ).await;
        assert_eq!(response.status(), actix_web::http::StatusCode::OK);
        let expected = to_bytes(response.into_body()).await.unwrap();
        let html = std::str::from_utf8(&expected).unwrap();
        assert!(html.contains("<pre>start</pre>"), "{html}");
        assert!(html.contains("<main>resume</main>"), "{html}");
        assert!(html.contains(",4,0,{}]"), "{html}");
        assert!(expected.len() < 4096, "output must remain small");
        std::fs::write(format!("{output_prefix}-{name}.html"), &expected).unwrap();
        println!("CASE name={name} rows={rows} input_bytes={bytes} output_bytes={}", expected.len());

        for phase in ["latency", "ticker"] {
            let stop = Rc::new(Cell::new(false));
            let max_gap = Rc::new(Cell::new(0_u64));
            let ticks = Rc::new(Cell::new(0_u64));
            let ticker = if phase == "ticker" {
                let stop = Rc::clone(&stop);
                let gap = Rc::clone(&max_gap);
                let ticks = Rc::clone(&ticks);
                Some(actix_web::rt::spawn(async move {
                    let mut last = Instant::now();
                    while !stop.get() {
                        tokio::task::yield_now().await;
                        let now = Instant::now();
                        gap.set(gap.get().max(now.duration_since(last).as_nanos() as u64));
                        ticks.set(ticks.get() + 1);
                        last = now;
                    }
                }))
            } else {
                None
            };
            let count = if phase == "latency" { measure_count } else { ticker_count };
            let warmups = if measure_count > 10 { 40 } else { 2 };
            let mut samples = Vec::with_capacity(count);
            for iteration in 0..warmups + count {
                let stream = tokio_stream::iter(
                    chunks.iter().cloned().map(Ok::<_, String>).collect::<Vec<_>>()
                );
                let request_config = pipeline_benchmark_config_copy(&config);
                let request_defaults = defaults();
                tokio::task::yield_now().await;
                max_gap.set(0);
                let tick_start = ticks.get();
                let cpu_start = pipeline_benchmark_clock::nanos(2);
                let thread_start = pipeline_benchmark_clock::nanos(3);
                let start = Instant::now();
                let response = render(stream, request_config, request_defaults).await;
                assert_eq!(response.status(), actix_web::http::StatusCode::OK);
                let body = to_bytes(response.into_body()).await.unwrap();
                let wall = start.elapsed().as_nanos() as f64 / 1000.0;
                let thread_cpu = (pipeline_benchmark_clock::nanos(3) - thread_start) as f64 / 1000.0;
                let cpu = (pipeline_benchmark_clock::nanos(2) - cpu_start) as f64 / 1000.0;
                tokio::task::yield_now().await;
                let gap = max_gap.get() as f64 / 1000.0;
                let tick_count = ticks.get() - tick_start;
                assert_eq!(body, expected, "output changed at {name}/{phase}/{iteration}");
                if iteration >= warmups {
                    samples.push((wall, cpu, thread_cpu, gap));
                    writeln!(
                        raw, "{phase},{name},{},{wall:.3},{cpu:.3},{thread_cpu:.3},{gap:.3},{tick_count}",
                        iteration - warmups
                    ).unwrap();
                }
            }
            stop.set(true);
            if let Some(ticker) = ticker { ticker.await.unwrap(); }
            println!(
                "BENCH phase={phase} case={name} n={count} wall_p50_us={:.3} wall_p95_us={:.3} cpu_p50_us={:.3} cpu_p95_us={:.3} event_cpu_p50_us={:.3} event_cpu_p95_us={:.3} tick_gap_p50_us={:.3} tick_gap_p95_us={:.3}",
                pipeline_benchmark_percentile(samples.iter().map(|sample| sample.0), 50),
                pipeline_benchmark_percentile(samples.iter().map(|sample| sample.0), 95),
                pipeline_benchmark_percentile(samples.iter().map(|sample| sample.1), 50),
                pipeline_benchmark_percentile(samples.iter().map(|sample| sample.1), 95),
                pipeline_benchmark_percentile(samples.iter().map(|sample| sample.2), 50),
                pipeline_benchmark_percentile(samples.iter().map(|sample| sample.2), 95),
                pipeline_benchmark_percentile(samples.iter().map(|sample| sample.3), 50),
                pipeline_benchmark_percentile(samples.iter().map(|sample| sample.3), 95),
            );
        }
    }
    std::fs::write(format!("{output_prefix}.csv"), raw).unwrap();
}

@mohamedmansour

Copy link
Copy Markdown
Contributor Author

Follow-up: microsoft/webui-benchmarks against this local checkout

All 16 SSR cases were measured in both modes: 32 measured rounds. The complete publication workflow did not succeed. Its headed browser suite attempted all 160 rounds but rejected the report after 11 failures. No official browser numeric results or homepage summary were produced.

The results below come from the fresh unfiltered run, not the earlier filtered attempt made while the Rust adapter failed to compile. They measure this checkout only: there is no corresponding external-suite before/after baseline, so they do not establish a speedup caused by this PR.

Source and runtime provenance

Item Value
WebUI e274c8b80cb54ecc562ab4528f4ca4d93dd1d9a4, clean checkout
Benchmark repository microsoft/webui-benchmarks at 1d83eddf5573cc28fe32b7cd56cdbc6bf98563cd, plus the one-line adapter correction below
Benchmark source digest 22bf026652097229cb852f6cfa52f00f2ce96c8ca723fbb44d4285d7b530686e
Benchmark dirty state Explicitly recorded as true; no metadata was hidden or falsified
Dependency mode Supported local-source mode, WebUI 0.0.28; local handler/protocol/state/expressions and rebuilt npm packages/native addon
Hardware Intel Core Ultra 9 285K, 24 logical CPUs, approximately 62.53 GiB RAM
OS Linux x86_64, WSL2 6.6.114.1-microsoft-standard-WSL2
Node / Bun / Deno 24.19.0 / 1.4.0 / 2.9.6
Rust / Cargo / pnpm 1.98.0 / 1.98.0 / 11.5.3
Browser Headed Chromium 151.0.7922.34, WSL display
Full workflow 2026-09-11 22:38:17-23:09:52 UTC, 31m 35s
Progressive report timestamp 2026-09-11T22:49:49.677Z
Complete report timestamp 2026-09-11T23:00:59.800Z

The benchmark adapter supplied serialized JSON to the parsed-state API. Its sole local source correction preserves the existing serialization and selects the appropriate existing API:

--- todo/servers/rust-actix/src/lib.rs
+++ todo/servers/rust-actix/src/lib.rs
@@
-            return match state.protocol.render_partial(
+            return match state.protocol.render_partial_json(
                 &state_json,
                 ENTRY_ID,
                 request_path,

The adapter correction is uncommitted in the benchmark repository, not part of this framework PR. No workload, timing, dependency pins, or correctness rules were changed. Existing Rust workspace tests passed after the alignment.

Local-source setup used the benchmark repository's supported pnpm run webui:local -- --path <webui-checkout> mechanism. Cargo metadata resolved the local crates with source: null; all 170 npm distribution files matched their checkout outputs. The measured JS hosts reported the checkout's target/release/libwebui_node.so, not a released addon. The user's default Node version was not changed; the pinned version was selected per command.

Measured artifact SHA-256
Actual checkout addon e498cac4669cdc7466ba9353828630bf8d876160f72517f8e8534531cea9e6dc
CLI e6ce8b6a46ea48d1a6c60179fb53b85cb8e46e6aff7d0abff261136a766c6367
Rust benchmark server ff4d5006845901d9f0547a2ebe75b1d2c61b94c7815c2a3837dd13c262d66ada

The source-snapshot addon has a different hash (3ffd092636618f25c2a13c86796960d302c8c4cb6b95e94a4dde41a39a1f85d1) because the supported build first builds CLI+addon together, then builds the addon alone. Both exact hashes were reproduced from their corresponding Cargo feature-unification builds. The table identifies the actually observed measured binary.

Methodology

The existing pnpm run bench:official workflow was invoked unfiltered, with its standard settings and sequential execution:

pnpm run build
pnpm run bench:official

Its SSR steps were:

node todo/runner/cli.mjs --runtime-mode official --response-mode progressive \
  --save-baseline official --allow-load-errors
node todo/runner/cli.mjs --runtime-mode official --response-mode complete \
  --save-baseline official-complete --allow-load-errors

Each case uses 100 todos and all 32 structural variants, per-request timestamps, natural unpadded response bytes, and the canonical semantic/response contract. One server worker is pinned to CPU 0; two load workers drive 64 measured connections. Each row gets two 4-second saturation probes at 64 and 128 connections, the normal warmup/restart sequence, a 3-second measured-server warmup, and one 20-second measurement.

Peak RSS is server RSS inside the measured RPS window, sampled every 200 ms. It is not startup RSS, total machine memory, or browser memory. No measured workloads or builds were intentionally overlapped. This single-round-per-case methodology supplies no multi-round confidence interval.

All 32 rows have complete case coverage, available telemetry, metricsStatus: "ok", applied CPU affinity, and exactly one measured attempt. Raw report status is still "partial" because of overload/saturation warnings. A complete matrix is not the same as a clean publication result.

Progressive/native-streaming SSR: full matrix

Latency is milliseconds; RSS is MiB. Error and timeout counts overlap: do not add them. Every measured row recorded zero non-2xx responses.

Case Completed RPS P50 ms P95 ms Peak load RSS MiB Errors / timeouts Load status
webui-rust-actix 1,487.60 34.01 75.75 133.26 0 / 0 ok
webui-shadowdom-rust-actix 1,106.35 56.83 65.86 167.05 0 / 0 ok
webui-fast-rust-actix 607.25 102.71 119.80 163.50 0 / 0 ok
webui-bun-serve 325.50 168.37 333.73 279.14 0 / 0 ok
webui-deno-serve 510.50 125.27 192.84 419.55 0 / 0 ok
webui-node-http 480.05 125.88 161.28 679.93 0 / 0 ok
sveltekit 167.95 308.94 452.60 362.33 7 / 7 overloaded
nuxt 75.10 442.29 523.13 567.48 44 / 44 overloaded
next 128.80 451.78 631.94 463.72 0 / 0 ok
astro 60.25 834.24 1,071.33 482.25 14 / 14 overloaded
vue 106.60 374.70 430.37 385.50 20 / 20 overloaded
lit 8.35 1,348.25 2,207.70 383.63 82 / 82 overloaded
preact 283.75 170.79 238.57 350.50 0 / 0 ok
react 213.65 271.05 496.25 591.54 0 / 0 ok
react-tanstack 202.35 277.78 389.37 442.45 1 / 1 overloaded
solid 239.00 231.39 348.68 687.52 0 / 0 ok

168 measured errors, all also recorded as timeouts; six overloaded cases. Unsaturated-probe warnings: vue, react, webui-node-http, react-tanstack. The prescribed fixed measured connection count was not changed.

Complete-response SSR: full matrix

Same units and error interpretation. Every measured row recorded zero non-2xx responses.

Case Completed RPS P50 ms P95 ms Peak load RSS MiB Errors / timeouts Load status
webui-rust-actix 1,632.95 38.85 43.59 128.15 0 / 0 ok
webui-shadowdom-rust-actix 1,281.60 49.33 55.77 140.73 0 / 0 ok
webui-fast-rust-actix 1,009.95 63.12 67.94 130.26 0 / 0 ok
webui-bun-serve 642.40 97.76 115.07 162.79 0 / 0 ok
webui-deno-serve 606.80 103.24 126.11 291.87 0 / 0 ok
webui-node-http 607.10 99.16 133.88 398.80 0 / 0 ok
sveltekit 202.65 287.69 345.68 415.59 1 / 1 overloaded
nuxt 75.05 480.78 743.45 1,066.48 48 / 48 overloaded
next 142.40 414.92 493.24 452.96 0 / 0 ok
astro 71.50 736.11 901.41 520.98 8 / 8 overloaded
vue 226.50 246.41 334.95 422.65 0 / 0 ok
lit 74.25 470.69 538.80 359.53 44 / 44 overloaded
preact 313.70 190.52 227.84 373.32 0 / 0 ok
react 277.25 208.99 290.25 363.02 0 / 0 ok
react-tanstack 224.60 263.93 324.87 429.09 0 / 0 ok
solid 271.80 214.97 280.19 567.18 0 / 0 ok

101 measured errors, all also recorded as timeouts; four overloaded cases. Unsaturated-probe warnings: vue, lit, next, sveltekit. All 64 saturation probes across both modes had zero errors/timeouts, and every measured warmup had status ok.

Headed browser: attempted, but no publishable numeric report

The workflow ran its unchanged official command:

node todo/browser/cli.mjs --official --rounds 10 --headed \
  --output results/ssr-todo-browser-outcomes-official.json

It used ten deterministic randomized rounds per product, fresh headed Chromium processes/contexts/pages, 1440x900 viewport, two unmeasured foreground animation frames, HTTPS/HTTP2, production hydration, and the canonical 29-step interaction sequence. Only one product server ran at a time.

All 160 rounds were attempted. Successful counts below are inferred from the runner's exhaustive final failure list, not from a saved numeric report.

Product Attempts Inferred successful outcomes Failures
webui-rust-actix 10 10 0
webui-shadowdom-rust-actix 10 10 0
webui-fast-rust-actix 10 10 0
webui-bun-serve 10 10 0
webui-deno-serve 10 9 1
webui-node-http 10 10 0
sveltekit 10 10 0
nuxt 10 10 0
next 10 10 0
astro 10 10 0
vue 10 10 0
lit 10 0 10
preact 10 10 0
react 10 10 0
react-tanstack 10 10 0
solid 10 10 0
Total 160 149 11

All ten Lit rounds failed the timestamp/DOM correctness condition: lit did not render one request timestamp across 100 todos. Narrow diagnosis using the runner's own shadow-aware query found exactly 100 title elements in raw SSR HTML, but 300 title elements after hydration, including values outside the required title/timestamp form. The count remained 300 after two seconds. This is a post-hydration correctness discrepancy, not the Rust adapter compile issue.

Deno round 2 failed because its page/context/browser closed. Chromium logged GPU initialization failures and an X connection error. A separate, clearly labeled quick headed diagnostic subsequently passed Deno and reproduced the Lit failure. That diagnostic was not promoted into official evidence or substituted for a failed round.

The official browser runner throws before saving results if any round fails. Consequently no canonical LCP, FCP, hydration/readiness, interaction/navigation, JS transfer/heap, renderer-memory, or other numeric browser metrics survived this attempt, even for successful products. They are unavailable, not zero, and were not reconstructed.

Outputs, publication blockers, and limits

Raw reports remain in the local benchmark checkout:

Output Result
results/ssr-todo-official.json Saved; 16 measured rows; schema-valid, warning-bearing report
results/ssr-todo-official-complete.json Saved; 16 measured rows; schema-valid, warning-bearing report
results/ssr-todo-browser-outcomes-official.json Not saved because official browser rounds failed
results/benchmark-summary.json Not generated: required browser report is absent
results/benchmark-summary-complete.json Not generated: exporter requires a clean benchmark source revision

The complete-only export correctly rejected the uncommitted adapter correction. No clean-source flag was forged, diff hidden, summary fabricated, correctness test weakened, or browser product removed.

Full raw reports, logs, source diff, runtime/dependency provenance, measured binaries, and SHA-256 manifests are retained locally with this run's evidence. The earlier 13-row runs are preserved separately and are not merged into these tables.

These results characterize the current integration checkout, not specifically every changed path: the Rust benchmark adapter uses its own ChannelWriter, rather than webui::streaming::StreamingWriter, and does not benchmark the CLI NDJSON parser or watcher hashing. The focused before/after measurements in the PR description remain the evidence for those changes.

Remaining limitations: one measured SSR round per case/mode; no telemetry-overhead control run; disclosed load/saturation warnings and load-runner negative-duration timer warnings; and the suite's dedicated async data-boundary workload remains unimplemented (available-pending-benchmark-implementation). No older cross-host comparison or whole-server improvement claim is made.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mohamedmansour

Copy link
Copy Markdown
Contributor Author

Final result: keep #527 draft. None of the hard 4/16/64 KiB policies wins as a general default. The isolated pool fix in #532 bounds idle retention, but recurring oversized attributes have a real CPU/latency cost. The independent CLI and watcher changes remain #529 and #530.

The reproducible benchmark source and instructions are now in this PR at e337eec9. This follow-up commit adds the harness only; it does not change production code. All measured cells, including controls and excluded initial HTTP results, are embedded below. Raw per-request JSONL, execution manifests, archived binaries, and machine-specific drivers remain local session artifacts, not public download links.

StreamingWriter experiment results

Merge recommendation

Do not merge hard 4, 16, or 64 KiB as a general/default replacement for the
original soft-target writer on this evidence. Keep the bounded transport work
experimental.
This experiment selected no new default; #527 still contains the held hard-4-KiB proposal.

  • Hard 4 KiB: reject the general change. Large pooled raw/attribute responses
    are approximately 41x/37x slower in the concurrent transport harness; the
    2.4 MB real SSR response is also approximately 22% slower.
  • Hard 16 KiB: reject the default change. It improves the many-small-write
    workloads versus soft-default 4 KiB, but large values remain approximately
    10x slower. Equal-target controls show that the mixed/SSR improvement comes
    mainly from using a larger coalescing target, not from imposing a hard cap.
  • Hard 64 KiB: not a default-merge winner. It is the least costly bounded
    option for large values tested here, but large pooled values remain roughly
    2-3x slower in the transport harness. The configured queue-byte and idle-pool
    budgets are 16x those of hard 4 KiB. Real HTTP raw-body latency is close to,
    not consistently better than, soft-default; its repeated-run ranges overlap.
    A separately scoped, explicitly opt-in strict byte-budget policy could be
    justified for a deployment that accepts these tradeoffs. This is not an
    endorsement of the present default-changing patch.
  • Pool-only: recommend publishing/merging as an idle-retention fix, with the
    recurring-oversized-attribute caveat below.
    It is not a blanket speed
    improvement, and it does not bound queued payload or active response memory.

Setup and interpretation

Common code/dependencies: umbrella e274c8b80cb54ecc562ab4528f4ca4d93dd1d9a4.
The soft comparator restores only crates/webui/src/streaming.rs from
f43db65067d48581ed7f99acc4b8d68f201c8bb9. That streaming source is unchanged on
main 2dab6fc6. The pool comparator's streaming source and tests were checked
byte-for-byte against final commit
a7cba0abaf0f3beccf5dda3e4fc4de2104f5fcb4; the final patch's docs-only movement
does not affect the comparator. supplied-pool-final.patch is retained.

Rust 1.98.0, release profile with thin LTO, one codegen unit, panic=abort;
x86-64 Linux under WSL2. Distinct Cargo target directories were used. Builds
were stopped before timed phases. All measurement subprocesses were sequential;
no timing phase overlapped our compilations.
Host/VM scheduling and CPU-frequency noise are not eliminated or pinned.

Each cell has three separate process runs with rotated/interleaved variant
order and five warmup responses. Fast cells use 15 batches of 100 requests
(tiny/small/mixed) or 20 requests (large/SSR/progressive); paced and HTTP cells
use nine individual requests per process. The tables report the median of
the three process-run request medians
, not a Criterion estimate or a batch
mean. Process CPU is the median of three CPU/request means, including both
producer and consumer (or server and client), not producer-only CPU.
Per-process summaries and ranges remain in summary.json; per-request observations are in the raw JSONL files.
Three-run ranges are descriptive, not confidence intervals or P99 estimates.

The channel has four slots. A persistent OS consumer drains concurrently;
one response is active at a time. A pooled cell shares 16 buffer slots.
This is not a multi-request contention benchmark. The primary matrix uses
pool bytes equal to configured writer target in both compared variants.
The separate pool-only pair uses 5120-byte pools at a 4096-byte target,
preserving the original recommended 1 KiB headroom. Do not conflate those groups.

The 4 KiB path preserves default construction. Larger targets use the existing
with_chunk_size builder on every request, including its replacement-buffer
allocation; changing the compiled production default itself was not tested.
Five warmups do not necessarily fill a 16-slot pool: that builder can increase
idle inventory over successive requests. We call these warmed measurements,
not a proof that every pool inventory has reached steady state.

Primary unpaced results

Pooled, concurrent transport; latency in microseconds:

Workload Output bytes Soft default 4 KiB Hard 4 KiB Hard 16 KiB Hard 64 KiB
Mixed HTML/attributes 107,610 209.1 221.2 72.5 31.9
Real contacts SSR 2,402,786 3,695.1 4,491.7 1,437.2 1,334.5
One raw value 1,048,576 53.4 2,188.0 521.4 114.9
One quoted attribute 1,048,590 63.9 2,341.2 620.2 188.9

The real SSR cell renders /contacts with 1000 contacts, not the much smaller
dashboard. A regression test guards that distinction. The matrix also covers
256-byte tiny output, 1024 sixteen-byte writes, real progressive checkpoints,
and unpooled controls. Small/progressive results have appreciable scheduling
noise; they are not the basis for the rejection of hard defaults.

For pooled raw output, process CPU/request was 37.0, 646.4, 189.9, 63.6 us
for soft/4/16/64 respectively. For the quoted attribute it was
40.2, 670.8, 204.4, 83.5 us. The large-value regressions are not merely
wall-clock waiting.

The large-value result is also present without pooling: raw response medians
were 48.5 -> 2334.7 / 510.0 / 169.6 us, and attribute medians were
60.9 -> 2386.7 / 594.6 / 155.9 us.

Full results: results-transport-focused/summary.{json,tsv}.

Equal-target controls

Pooled, unpaced, identical configured target and pool bytes within each pair;
original headroom and source behavior are otherwise retained:

Workload Soft 16 KiB Hard 16 KiB Soft 64 KiB Hard 64 KiB
Mixed 60.3 58.1 31.5 31.7
Real contacts SSR 1,401.4 1,519.9 1,390.9 1,388.0
Raw 1 MiB 55.1 572.9 48.7 114.2
Attribute 1 MiB 48.6 603.7 59.4 155.3

Thus a larger soft target already obtains most of the mixed/SSR throughput
benefit, while the hard-cap large-value penalty remains. A soft target still
does not provide a configured payload-byte maximum.
See results-target-controls/summary.{json,tsv}.

Slow consumers

Pacing uses cumulative bytes from first-body arrival, including the final
chunk, rather than a fixed sleep per chunk. Exactly one received chunk is held
while its byte budget is serviced. Deadlines do not accumulate a rounded sleep
duration per chunk, although wakeup count and scheduler/timer overhead differ.

For a pooled 1 MiB raw response at 8 MiB/s, completion medians were all
approximately 125.2-125.3 ms. Process CPU/request differed substantially:

Configuration Mean process CPU/request
Soft default 199 us
Hard 4 KiB 4,568 us
Hard 16 KiB 1,508 us
Hard 64 KiB 754 us

Those CPU figures include the paced consumer's wakeups; they must not be
labelled server-writer CPU. Equal completion time under an imposed byte rate
does not imply equal CPU efficiency.

For real SSR at 8 MiB/s, completion stayed around 286.6-286.8 ms. First-body
medians rose from 60 us (soft) to 123/173 us at hard 16/64; total process
CPU fell from 12.75 ms to 7.96/4.54 ms as the many small sends decreased.
Mixed HTML was also exercised at 1 MiB/s, and progressive output at 8 MiB/s.

Actual HTTP confirmation and the socket confound

The first HTTP run, retained in results-http-focused, exposed roughly 40 ms
gaps between headers and body, or before completion. Actix-web 4.14 leaves
accepted sockets at the OS's TCP_NODELAY default unless configured. These
Nagle/delayed-ACK-dominated results were not used to rank writer variants.

The benchmark-only HTTP server was then changed to .tcp_nodelay(true), with
an assertion on every accepted socket and a regression test for that assertion.
All three harness binaries were rebuilt and tested with measurements paused.
Only HTTP socket setup, its reporting/verification, and documentation changed.
No production host default changed. Original binaries and source patch are
archived under measured-v1; the pool, primary transport and equal-target
results all use those exact original binaries. Corrected HTTP rows explicitly
record tcp_nodelay: true.

Corrected HTTP, pooled raw 1 MiB, unpaced:

Configuration Completion P50 Range of process P50s First body P50 Mean process CPU/request
Soft default 245 us 204-443 us 177 us 300 us
Hard 4 KiB 2,752 us 2,575-4,168 us 116 us 1,907 us
Hard 16 KiB 591 us 541-630 us 120 us 677 us
Hard 64 KiB 277 us 246-285 us 141 us 387 us

Hard 64's completion ranges overlap soft-default; there is no convincing
universal latency advantage. Earlier first body is possible without earlier
completion. Progressive HTTP first-body medians were approximately 107-129 us
across configurations, with overlapping completion ranges. Slow raw HTTP
completion was approximately 126.4-126.7 ms at 8 MiB/s.

These are local HTTP/1, non-TLS, one-worker server/client measurements -- not
browser FCP/LCP, proxy behavior, a physical network, or concurrent-request load.
HTTP/TCP can coalesce/split writer chunks and buffer beyond the four-slot
channel. Tokio pacing timer granularity is visible on the small paced response.
See results-http-nodelay/summary.{json,tsv} for the corrected evidence.

Allocation and memory accounting

Accounting is performed separately without recording latency. It verifies
every output byte and semantic flush offset, and records chunk counts/maxima.
Timed warmups perform the same verification; timed bodies retain length checks
without per-byte comparisons. All seven workload output hashes/checkpoints
matched across writer configurations.

The allocator counts alloc, alloc_zeroed and growing realloc calls;
requested bytes count realloc growth deltas. These include harness allocations,
not just chunk buffers. Counters are disabled during timing, but a relaxed
enabled-flag read remains on allocations.

Configuration Raw / attribute 1 MiB chunk counts Maximum queued payload, source-derived 16-slot idle capacity bound, source-derived
Original soft default 1 / 1 No input-independent configured byte bound No configured byte bound
Hard 4 KiB 256 / 257 16 KiB 64 KiB
Hard 16 KiB 64 / 65 64 KiB 256 KiB
Hard 64 KiB 16 / 17 256 KiB 1 MiB
Pool-only, matched 5 KiB pool 1 / 1 No input-independent configured byte bound 80 KiB

For warmed pooled raw output, allocation-call/request counts were
1 / 256 / 65 / 17 for soft/4/16/64. Larger configured targets still allocate
a replacement buffer through the existing builder each request. Observed
idle counts and all cold/warm data are retained in results-accounting-focused.
Idle counts are not measurements of private Vec capacities.

Bounds exclude active producer buffers, a pending send, consumer-held chunks,
spare capacity, ownership/channel metadata, state, input and serialization
scratch. They are not total process-memory limits.

RSS caution: the Python-launched accounting processes all reported the same
25,104,384-byte high-water floor. A separate direct-Bash launch sanity check
removed that launcher-associated floor. Illustrative raw-response process peaks
were soft 9.04 MB, hard 4 6.68 MB, hard 16 7.08 MB, hard 64 7.47 MB;
SSR peaks were all around 16.9-17.1 MB. These are single-process-lifetime
high-water observations including setup, not live memory, idle retention,
per-render deltas, or reliable fine-grained memory rankings
. The source bounds
are the defensible retention/queued-payload claims. Raw sanity-check rows remain
in results-rss-sanity.

Isolated pool-only result and publication caveat

Both sides use a 4 KiB soft target, 5120-byte pool buffers, 16 pool slots
and four channel slots. Writer sizing/headroom/send logic is unchanged.

Workload, pooled/unpaced Original soft P50 Pool-only P50 Original -> pool-only mean CPU/request
Mixed 219.0 us 211.5 us 83.3 -> 79.1 us
Real contacts SSR 3,710.4 us 3,640.8 us 2,878.9 -> 2,877.7 us
Raw 1 MiB 64.5 us 60.7 us 39.5 -> 39.4 us
Attribute 1 MiB 66.7 us 100.2 us 48.2 -> 72.0 us

Do not sell the small apparent wins as guaranteed improvements. The material
caveat is recurring oversized attributes: approximately 50% higher observed
median latency and mean CPU
. Process medians were 65.5-78.1 us before versus
86.3-101.1 us after. At 8 MiB/s, raw completion remained about 125.3 ms, but combined producer/consumer CPU rose from 186.2 to 257.9 us/request (approximately 38%). Its three-run CPU-mean ranges were 177.8-196.0 before versus 244.2-263.2 us after. Pacing hides that CPU cost in wall-clock completion; it does not eliminate it.

Separate warmed accounting observed raw allocation calls 1->3, and attribute
calls 1->4. Requested bytes for the latter rose from 40 to 2,097,218
per response. The oversized allocation is deliberately no longer retained;
these requested bytes are not a live-memory measurement. Mixed and unpooled raw
allocation controls were unchanged.

This tradeoff is compatible with merging an explicitly documented idle-retention
fix, but users with recurring oversized attributes should not expect prior
pooled allocation/throughput behavior. See results-pool/summary.{json,tsv},
pool-accounting-smoke, and pool-comparison.json.

Full measured-cell tables

Times below are microseconds. Brackets show the min/max of three process estimates, not confidence intervals. Latency and first body are medians of process request medians; CPU is the median of process CPU/request means. Rate 0 means unpaced; 8,388,608 means 8 MiB/s. Target/pool sizes and output sizes are bytes. The primary target-sized pools and the isolated 5120-byte pool pair are separate experiments.

There were 360 timed processes and 143,550 measured requests across all phases, including the 48 confounded initial HTTP processes that are excluded from the recommendation. No timed subprocesses overlapped each other or our builds. Five warmups per process are additional, not measured requests.

The non-HTTP comparisons use the archived v1 harness; corrected HTTP uses v2 with TCP_NODELAY enabled and verified. The v2 change affects only HTTP setup/reporting, its regression coverage, and documentation. The public harness is v2. Source/binary fingerprints follow the tables; a fingerprint identifies the measured artifact, not a claim that local raw files are downloadable.

Isolated pool-only: all 12 measured cells
Variant Target B Pool B Pooled Workload Rate B/s Output B Latency us [run range] Mean CPU us/request [run range] First body us
pool-only 4096 5120 false raw_1m 0 1048576 57.764 [56.713-58.797] 32.773 [31.970-34.633] 39.792
pool-only 4096 5120 true attribute_1m 0 1048590 100.153 [86.257-101.057] 71.957 [69.250-83.260] 69.430
pool-only 4096 5120 true mixed 0 107610 211.538 [208.617-212.314] 79.109 [78.761-81.528] 19.639
pool-only 4096 5120 true raw_1m 0 1048576 60.711 [57.548-65.968] 39.427 [38.347-52.000] 42.670
pool-only 4096 5120 true raw_1m 8388608 1048576 125374.877 [125354.936-125784.121] 257.889 [244.222-263.222] 169.359
pool-only 4096 5120 true ssr_1000 0 2402786 3640.789 [3636.838-4047.317] 2877.670 [2862.143-3043.003] 14.693
soft 4096 5120 false raw_1m 0 1048576 57.550 [54.100-71.811] 37.693 [36.100-45.357] 39.535
soft 4096 5120 true attribute_1m 0 1048590 66.720 [65.496-78.149] 48.187 [47.113-54.240] 47.956
soft 4096 5120 true mixed 0 107610 219.034 [218.171-228.948] 83.293 [81.781-91.724] 19.393
soft 4096 5120 true raw_1m 0 1048576 64.507 [63.831-66.716] 39.533 [39.517-45.737] 46.438
soft 4096 5120 true raw_1m 8388608 1048576 125319.874 [125244.429-125399.333] 186.222 [177.778-196.000] 148.692
soft 4096 5120 true ssr_1000 0 2402786 3710.395 [3592.954-3804.739] 2878.930 [2836.527-2928.840] 14.755
Primary transport: all 60 measured cells
Variant Target B Pool B Pooled Workload Rate B/s Output B Latency us [run range] Mean CPU us/request [run range] First body us
hard 4096 4096 false attribute_1m 0 1048590 2386.672 [2282.006-2404.740] 684.953 [637.257-728.210] 16.237
hard 4096 4096 false mixed 0 107610 213.119 [207.505-225.968] 86.067 [81.339-86.128] 15.507
hard 4096 4096 false progressive 0 49927 123.946 [115.505-219.223] 88.530 [81.337-99.110] 14.238
hard 4096 4096 false raw_1m 0 1048576 2334.666 [2212.798-2710.526] 691.963 [660.783-756.863] 16.608
hard 4096 4096 false small_16k 0 16384 32.162 [26.588-35.342] 9.019 [8.599-9.558] 16.974
hard 4096 4096 false ssr_1000 0 2402786 4462.403 [4413.053-4608.677] 3518.067 [3419.417-3521.963] 17.980
hard 4096 4096 false tiny 0 256 34.762 [33.695-40.657] 8.920 [8.007-9.487] 16.705
hard 4096 4096 true attribute_1m 0 1048590 2341.166 [2287.470-2387.631] 670.833 [650.057-725.087] 16.210
hard 4096 4096 true mixed 0 107610 221.224 [220.241-230.945] 81.405 [79.530-90.525] 19.892
hard 4096 4096 true mixed 1048576 107610 102842.937 [102820.472-102846.540] 769.333 [731.778-771.333] 33.931
hard 4096 4096 true progressive 8388608 49927 6090.813 [6085.409-6104.038] 277.333 [246.444-281.889] 37.470
hard 4096 4096 true raw_1m 0 1048576 2188.043 [2180.387-2269.579] 646.403 [613.003-660.357] 15.585
hard 4096 4096 true raw_1m 8388608 1048576 125176.044 [125165.310-125184.313] 4567.667 [4128.000-4754.333] 43.370
hard 4096 4096 true ssr_1000 0 2402786 4491.651 [4310.349-5486.594] 3331.017 [3317.350-4118.127] 19.002
hard 4096 4096 true ssr_1000 8388608 2402786 286619.292 [286614.164-286632.696] 15202.444 [13795.333-16555.667] 52.452
hard 16384 16384 false attribute_1m 0 1048590 594.558 [479.991-688.207] 191.420 [186.333-253.713] 18.819
hard 16384 16384 false mixed 0 107610 58.790 [58.537-66.057] 30.893 [30.852-30.969] 15.176
hard 16384 16384 false progressive 0 49927 83.263 [74.911-102.961] 67.050 [65.930-73.357] 17.783
hard 16384 16384 false raw_1m 0 1048576 509.955 [482.894-580.890] 192.853 [174.143-197.703] 15.558
hard 16384 16384 false small_16k 0 16384 26.415 [25.777-55.185] 8.902 [8.900-9.503] 13.397
hard 16384 16384 false ssr_1000 0 2402786 1503.513 [1487.440-1579.800] 1781.843 [1761.703-1903.123] 38.020
hard 16384 16384 false tiny 0 256 34.035 [32.038-38.833] 10.108 [8.141-10.230] 16.503
hard 16384 16384 true attribute_1m 0 1048590 620.223 [578.024-837.482] 204.360 [199.407-297.407] 16.325
hard 16384 16384 true mixed 0 107610 72.547 [63.895-73.469] 33.223 [31.743-33.669] 16.039
hard 16384 16384 true mixed 1048576 107610 102894.407 [102841.297-102906.264] 280.222 [255.111-299.667] 54.053
hard 16384 16384 true progressive 8388608 49927 6112.486 [6102.720-6121.079] 152.889 [134.667-177.778] 37.298
hard 16384 16384 true raw_1m 0 1048576 521.434 [503.875-531.649] 189.900 [180.293-218.667] 15.686
hard 16384 16384 true raw_1m 8388608 1048576 125183.708 [125178.321-125187.492] 1508.111 [1475.556-1650.000] 43.003
hard 16384 16384 true ssr_1000 0 2402786 1437.215 [1436.198-1503.856] 1768.430 [1711.707-1773.053] 38.282
hard 16384 16384 true ssr_1000 8388608 2402786 286730.424 [286718.817-286730.477] 7955.667 [7913.889-8120.444] 122.828
hard 65536 65536 false attribute_1m 0 1048590 155.879 [155.114-156.913] 68.930 [67.207-72.543] 17.150
hard 65536 65536 false mixed 0 107610 31.226 [30.550-49.404] 22.334 [19.223-30.285] 13.395
hard 65536 65536 false progressive 0 49927 93.954 [80.225-95.052] 75.423 [75.143-76.447] 13.187
hard 65536 65536 false raw_1m 0 1048576 169.649 [164.516-173.277] 133.350 [60.487-147.350] 13.906
hard 65536 65536 false small_16k 0 16384 31.741 [31.301-45.740] 19.327 [18.641-21.691] 13.710
hard 65536 65536 false ssr_1000 0 2402786 1358.216 [1357.317-1380.632] 1545.087 [1516.960-1553.803] 60.276
hard 65536 65536 false tiny 0 256 43.181 [34.650-45.977] 20.433 [17.179-24.223] 17.770
hard 65536 65536 true attribute_1m 0 1048590 188.949 [153.487-226.898] 83.520 [74.360-114.983] 19.278
hard 65536 65536 true mixed 0 107610 31.944 [31.355-40.017] 20.576 [20.226-21.089] 13.741
hard 65536 65536 true mixed 1048576 107610 102870.400 [102848.738-103035.985] 159.333 [150.444-176.111] 84.771
hard 65536 65536 true progressive 8388608 49927 6092.824 [6091.922-6107.997] 150.444 [123.667-175.222] 39.653
hard 65536 65536 true raw_1m 0 1048576 114.931 [113.113-127.313] 63.613 [57.830-65.130] 15.262
hard 65536 65536 true raw_1m 8388608 1048576 125271.601 [125270.479-125382.694] 753.889 [644.889-875.333] 75.098
hard 65536 65536 true ssr_1000 0 2402786 1334.508 [1304.297-1352.729] 1507.880 [1475.643-1541.790] 61.014
hard 65536 65536 true ssr_1000 8388608 2402786 286814.327 [286800.802-286877.755] 4536.222 [4301.333-4900.444] 173.223
soft 4096 4096 false attribute_1m 0 1048590 60.871 [48.876-62.078] 33.820 [31.440-39.650] 35.407
soft 4096 4096 false mixed 0 107610 205.912 [200.664-216.833] 86.271 [84.103-89.074] 15.655
soft 4096 4096 false progressive 0 49927 97.590 [82.809-102.114] 76.103 [73.670-80.857] 13.518
soft 4096 4096 false raw_1m 0 1048576 48.518 [48.260-55.604] 31.530 [30.987-34.267] 34.352
soft 4096 4096 false small_16k 0 16384 33.089 [27.226-33.495] 9.469 [9.077-10.132] 13.945
soft 4096 4096 false ssr_1000 0 2402786 4269.966 [3798.111-4426.215] 3427.623 [3173.037-3540.087] 20.556
soft 4096 4096 false tiny 0 256 33.615 [32.756-34.886] 8.349 [8.075-8.590] 17.186
soft 4096 4096 true attribute_1m 0 1048590 63.892 [62.239-64.407] 40.180 [39.740-41.237] 45.884
soft 4096 4096 true mixed 0 107610 209.126 [204.169-222.738] 81.986 [80.151-82.933] 15.566
soft 4096 4096 true mixed 1048576 107610 102805.361 [102792.807-102850.032] 730.889 [700.444-772.333] 41.619
soft 4096 4096 true progressive 8388608 49927 6097.874 [6067.559-6112.896] 262.667 [242.889-291.222] 39.570
soft 4096 4096 true raw_1m 0 1048576 53.373 [50.490-59.031] 36.980 [36.913-38.307] 38.386
soft 4096 4096 true raw_1m 8388608 1048576 125314.056 [125300.213-125343.299] 199.444 [177.111-202.222] 155.878
soft 4096 4096 true ssr_1000 0 2402786 3695.096 [3521.651-3895.976] 2952.623 [2822.717-3052.973] 17.105
soft 4096 4096 true ssr_1000 8388608 2402786 286609.563 [286607.466-286619.966] 12752.667 [12566.333-14091.889] 60.092
Equal-target controls: all 16 measured cells
Variant Target B Pool B Pooled Workload Rate B/s Output B Latency us [run range] Mean CPU us/request [run range] First body us
hard 16384 16384 true attribute_1m 0 1048590 603.697 [542.600-899.926] 198.573 [186.070-218.017] 17.347
hard 16384 16384 true mixed 0 107610 58.099 [57.992-59.751] 31.112 [28.637-33.501] 15.129
hard 16384 16384 true raw_1m 0 1048576 572.883 [551.692-576.624] 188.607 [175.840-191.250] 18.851
hard 16384 16384 true ssr_1000 0 2402786 1519.867 [1456.689-1523.361] 1764.263 [1649.960-1827.837] 39.353
hard 65536 65536 true attribute_1m 0 1048590 155.315 [149.256-162.981] 72.547 [67.890-83.353] 18.951
hard 65536 65536 true mixed 0 107610 31.720 [30.180-36.248] 23.261 [19.578-23.727] 13.755
hard 65536 65536 true raw_1m 0 1048576 114.169 [112.266-146.228] 62.040 [57.077-76.487] 15.243
hard 65536 65536 true ssr_1000 0 2402786 1388.004 [1359.168-1419.838] 1536.790 [1530.277-1636.767] 67.536
soft 16384 16384 true attribute_1m 0 1048590 48.551 [48.087-60.042] 53.340 [50.767-54.963] 33.494
soft 16384 16384 true mixed 0 107610 60.260 [58.706-66.763] 32.229 [31.341-32.753] 15.376
soft 16384 16384 true raw_1m 0 1048576 55.113 [52.577-57.247] 52.177 [51.603-56.230] 35.838
soft 16384 16384 true ssr_1000 0 2402786 1401.367 [1398.664-1408.685] 1645.517 [1611.083-1703.187] 36.852
soft 65536 65536 true attribute_1m 0 1048590 59.368 [48.084-66.553] 50.917 [47.090-58.970] 41.513
soft 65536 65536 true mixed 0 107610 31.502 [31.279-36.154] 22.597 [21.771-22.939] 13.544
soft 65536 65536 true raw_1m 0 1048576 48.678 [47.446-55.522] 51.017 [47.257-54.347] 34.362
soft 65536 65536 true ssr_1000 0 2402786 1390.944 [1319.686-1391.143] 1576.220 [1466.880-1593.587] 72.725
Corrected HTTP with TCP_NODELAY: all 16 measured cells
Variant Target B Pool B Pooled Workload Rate B/s Output B Latency us [run range] Mean CPU us/request [run range] First body us
hard 4096 4096 true progressive 0 49927 242.471 [219.033-270.426] 202.333 [170.111-244.222] 108.380
hard 4096 4096 true progressive 8388608 49927 7008.224 [6999.696-7019.976] 435.556 [425.111-444.556] 216.280
hard 4096 4096 true raw_1m 0 1048576 2752.281 [2575.126-4167.546] 1906.889 [1766.556-2139.556] 115.738
hard 4096 4096 true raw_1m 8388608 1048576 126385.760 [126238.614-126565.502] 3993.444 [3943.111-4200.889] 269.799
hard 16384 16384 true progressive 0 49927 199.157 [139.266-213.368] 211.778 [143.444-232.778] 128.670
hard 16384 16384 true progressive 8388608 49927 7703.282 [7036.117-7909.699] 403.333 [350.000-416.778] 259.502
hard 16384 16384 true raw_1m 0 1048576 590.988 [540.870-630.481] 677.111 [588.556-695.111] 120.151
hard 16384 16384 true raw_1m 8388608 1048576 126462.012 [126265.493-126668.945] 3013.778 [2694.778-3177.333] 459.905
hard 65536 65536 true progressive 0 49927 172.022 [150.341-199.581] 152.333 [146.111-217.333] 107.578
hard 65536 65536 true progressive 8388608 49927 7655.607 [6819.455-7769.802] 348.556 [339.778-366.333] 265.680
hard 65536 65536 true raw_1m 0 1048576 277.211 [246.450-285.285] 386.556 [376.667-403.556] 141.175
hard 65536 65536 true raw_1m 8388608 1048576 126396.215 [126228.475-126697.062] 2058.667 [1987.778-2124.667] 385.147
soft 4096 4096 true progressive 0 49927 188.547 [161.493-271.850] 189.889 [181.000-222.556] 113.295
soft 4096 4096 true progressive 8388608 49927 7769.321 [7168.345-7854.897] 454.000 [408.444-524.556] 246.186
soft 4096 4096 true raw_1m 0 1048576 244.801 [203.524-443.458] 300.111 [264.667-462.333] 177.427
soft 4096 4096 true raw_1m 8388608 1048576 126656.976 [126346.303-126704.564] 1657.778 [1656.667-1857.111] 616.394
Excluded initial HTTP: all 16 confounded cells; DO NOT rank variants using these
Variant Target B Pool B Pooled Workload Rate B/s Output B Latency us [run range] Mean CPU us/request [run range] First body us
hard 4096 4096 true progressive 0 49927 43972.581 [43969.174-43977.881] 519.222 [468.111-548.222] 43956.370
hard 4096 4096 true progressive 8388608 49927 48894.205 [48040.012-51919.036] 722.222 [698.444-759.444] 41266.324
hard 4096 4096 true raw_1m 0 1048576 3609.399 [3483.708-4012.901] 1738.444 [1700.333-1872.333] 311.943
hard 4096 4096 true raw_1m 8388608 1048576 126743.770 [126392.485-126817.030] 3881.556 [3872.556-4049.556] 554.085
hard 16384 16384 true progressive 0 49927 43962.064 [43957.051-43964.835] 459.444 [447.444-513.778] 43938.239
hard 16384 16384 true progressive 8388608 49927 47995.749 [47938.282-48054.177] 592.667 [558.111-654.333] 41408.736
hard 16384 16384 true raw_1m 0 1048576 43018.554 [42876.712-43748.985] 1258.333 [1073.778-1609.333] 338.890
hard 16384 16384 true raw_1m 8388608 1048576 126595.727 [126330.159-126746.544] 2796.444 [2753.778-3084.667] 399.988
hard 65536 65536 true progressive 0 49927 43973.584 [43932.938-43977.157] 435.778 [411.000-451.778] 43947.484
hard 65536 65536 true progressive 8388608 49927 48074.669 [47967.074-51212.844] 591.444 [521.222-709.444] 40641.362
hard 65536 65536 true raw_1m 0 1048576 416.230 [387.998-600.126] 607.111 [477.556-792.222] 196.713
hard 65536 65536 true raw_1m 8388608 1048576 126553.629 [126528.149-126580.337] 2079.889 [1846.667-2120.667] 380.145
soft 4096 4096 true progressive 0 49927 43954.144 [43951.145-43964.535] 477.333 [474.333-524.111] 43928.391
soft 4096 4096 true progressive 8388608 49927 48851.353 [47955.559-51903.907] 678.222 [596.556-759.000] 41435.314
soft 4096 4096 true raw_1m 0 1048576 42476.095 [1107.402-43869.306] 717.333 [569.778-717.556] 495.253
soft 4096 4096 true raw_1m 8388608 1048576 126708.353 [126559.740-126733.632] 2011.667 [1984.111-2200.556] 680.184

Full allocation-accounting tables

These are separate untimed observations, not throughput measurements or live heap. Counts include growing realloc calls and harness allocations; requested bytes use realloc growth deltas. Cold and warmed results must not be conflated. Idle counts do not measure private Vec capacities. The launcher-associated RSS floor makes the primary accounting RSS unusable for fine-grained rankings, so it is not repeated as a comparative column.

Primary warmed accounting: all 60 measured cells
Variant Target B Pool B Pooled Workload Rate B/s Phase Chunks Max chunk B Allocation calls Requested B Idle count
hard 16384 16384 true attribute_1m 0 warm_pool 65 16384 66 18984 13
hard 16384 16384 true mixed 0 warm_pool 7 16384 8 16664 12
hard 16384 16384 true mixed 1048576 warm_pool 7 16384 8 16664 13
hard 16384 16384 true progressive 8388608 warm_pool 6 16384 33 33209 13
hard 16384 16384 true raw_1m 0 warm_pool 64 16384 65 18944 12
hard 16384 16384 true raw_1m 8388608 warm_pool 64 16384 65 18944 13
hard 16384 16384 true ssr_1000 0 warm_pool 147 16384 8639 221545 12
hard 16384 16384 true ssr_1000 8388608 warm_pool 147 16384 8639 221545 13
hard 16384 16384 false attribute_1m 0 warm_pool 65 16384 68 1085464 0
hard 16384 16384 false mixed 0 warm_pool 7 16384 10 135192 0
hard 16384 16384 false progressive 0 warm_pool 6 16384 38 135465 0
hard 16384 16384 false raw_1m 0 warm_pool 64 16384 66 1069056 0
hard 16384 16384 false small_16k 0 warm_pool 1 16384 3 36864 0
hard 16384 16384 false ssr_1000 0 warm_pool 147 16384 8641 2628233 0
hard 16384 16384 false tiny 0 warm_pool 1 256 4 36888 0
hard 4096 4096 true attribute_1m 0 warm_pool 257 4096 257 10280 7
hard 4096 4096 true mixed 0 warm_pool 27 4096 27 1080 6
hard 4096 4096 true mixed 1048576 warm_pool 27 4096 27 1080 7
hard 4096 4096 true progressive 8388608 warm_pool 15 4096 41 17185 7
hard 4096 4096 true raw_1m 0 warm_pool 256 4096 256 10240 6
hard 4096 4096 true raw_1m 8388608 warm_pool 256 4096 256 10240 7
hard 4096 4096 true ssr_1000 0 warm_pool 587 4096 9078 222761 6
hard 4096 4096 true ssr_1000 8388608 warm_pool 587 4096 9078 222761 7
hard 4096 4096 false attribute_1m 0 warm_pool 257 4096 259 1056792 0
hard 4096 4096 false mixed 0 warm_pool 27 4096 29 114712 0
hard 4096 4096 false progressive 0 warm_pool 15 4096 46 82217 0
hard 4096 4096 false raw_1m 0 warm_pool 256 4096 257 1052672 0
hard 4096 4096 false small_16k 0 warm_pool 4 4096 5 20480 0
hard 4096 4096 false ssr_1000 0 warm_pool 587 4096 9080 2607753 0
hard 4096 4096 false tiny 0 warm_pool 1 256 3 8216 0
hard 65536 65536 true attribute_1m 0 warm_pool 17 65536 18 66216 12
hard 65536 65536 true mixed 0 warm_pool 2 65536 3 65616 9
hard 65536 65536 true mixed 1048576 warm_pool 2 65536 3 65616 9
hard 65536 65536 true progressive 8388608 warm_pool 4 45836 31 82281 11
hard 65536 65536 true raw_1m 0 warm_pool 16 65536 17 66176 12
hard 65536 65536 true raw_1m 8388608 warm_pool 16 65536 17 66176 13
hard 65536 65536 true ssr_1000 0 warm_pool 37 65536 8529 266297 11
hard 65536 65536 true ssr_1000 8388608 warm_pool 37 65536 8529 266297 13
hard 65536 65536 false attribute_1m 0 warm_pool 17 65536 20 1183768 0
hard 65536 65536 false mixed 0 warm_pool 2 65536 5 200728 0
hard 65536 65536 false progressive 0 warm_pool 4 45836 36 348457 0
hard 65536 65536 false raw_1m 0 warm_pool 16 65536 18 1118208 0
hard 65536 65536 false small_16k 0 warm_pool 1 16384 4 135192 0
hard 65536 65536 false ssr_1000 0 warm_pool 37 65536 8531 2693769 0
hard 65536 65536 false tiny 0 warm_pool 1 256 4 135192 0
soft 4096 4096 true attribute_1m 0 warm_pool 1 1048590 1 40 2
soft 4096 4096 true mixed 0 warm_pool 27 4167 27 1080 6
soft 4096 4096 true mixed 1048576 warm_pool 27 4167 27 1080 7
soft 4096 4096 true progressive 8388608 warm_pool 12 17043 38 17065 7
soft 4096 4096 true raw_1m 0 warm_pool 1 1048576 1 40 2
soft 4096 4096 true raw_1m 8388608 warm_pool 1 1048576 1 40 2
soft 4096 4096 true ssr_1000 0 warm_pool 503 4805 8994 219401 7
soft 4096 4096 true ssr_1000 8388608 warm_pool 503 4805 8994 219401 7
soft 4096 4096 false attribute_1m 0 warm_pool 1 1048590 5 2102322 0
soft 4096 4096 false mixed 0 warm_pool 27 4167 55 144008 0
soft 4096 4096 false progressive 0 warm_pool 12 17043 51 95332 0
soft 4096 4096 false raw_1m 0 warm_pool 1 1048576 3 1053696 0
soft 4096 4096 false small_16k 0 warm_pool 4 4096 9 25696 0
soft 4096 4096 false ssr_1000 0 warm_pool 503 4805 9498 2791833 0
soft 4096 4096 false tiny 0 warm_pool 1 256 3 10264 0
Matched 5 KiB pool accounting: all 16 cold/warmed rows
Variant Target B Pool B Pooled Workload Rate B/s Phase Chunks Max chunk B Allocation calls Requested B Idle count
pool-only 4096 5120 true attribute_1m 0 cold_pool 1 1048590 13 2103762 1
pool-only 4096 5120 true attribute_1m 0 warm_pool 1 1048590 4 2097218 1
pool-only 4096 5120 true mixed 0 cold_pool 27 4167 41 33224 6
pool-only 4096 5120 true mixed 0 warm_pool 27 4167 27 1080 6
pool-only 4096 5120 true raw_1m 0 cold_pool 1 1048576 12 1055160 1
pool-only 4096 5120 true raw_1m 0 warm_pool 1 1048576 3 1048616 1
pool-only 4096 5120 false raw_1m 0 cold_pool 1 1048576 11 1055120 0
pool-only 4096 5120 false raw_1m 0 warm_pool 1 1048576 3 1053696 0
soft 4096 5120 true attribute_1m 0 cold_pool 1 1048590 13 2103762 2
soft 4096 5120 true attribute_1m 0 warm_pool 1 1048590 1 40 2
soft 4096 5120 true mixed 0 cold_pool 27 4167 41 33224 6
soft 4096 5120 true mixed 0 warm_pool 27 4167 27 1080 6
soft 4096 5120 true raw_1m 0 cold_pool 1 1048576 12 1055160 2
soft 4096 5120 true raw_1m 0 warm_pool 1 1048576 1 40 2
soft 4096 5120 false raw_1m 0 cold_pool 1 1048576 11 1055120 0
soft 4096 5120 false raw_1m 0 warm_pool 1 1048576 3 1053696 0
All seven output hashes and semantic flush offsets
Workload Output B SHA-256 Flush byte offsets
attribute_1m 1048590 7a25c2d8f331a60793f9eb83012142b09687a3f1f6d643b9302e3c43444e4977 1048590
mixed 107610 00e40aad7d51647ba613af82c99ab3122e208dbcc6df31df794841cb7ca61feb 107610
progressive 49927 d5a971af3f8a16b90f86666e006a78ff1964300b19cf82efc4d551411a90055c 114, 45950, 49790, 49927
raw_1m 1048576 92d79153c7718b9c724a04116878c1243918851be8734da1f2275decd9556582 1048576
ssr_1000 2402786 83c1491273caff40622842b030addcb366ba9f508de3d4af05d1042374ab9f1a 2402786
small_16k 16384 4ba5baf48e86e67f8c41052aea1fed7638c7c81523f1f036cb998b8d38d3483c 16384
tiny 256 411e5d7363eee72cd47feccca49bb0e5437952efb3f78ea1da5ac5b86408e569 256
Dataset and measured binary fingerprints
Summary artifact SHA-256
results-pool/summary.json 16506665daa2b60abb302b62667c91286929f7f73e926cc84831515e13ed97f7
results-transport-focused/summary.json 0bec7128d7c25d2ee5cc9064f55ec27ba0d012315438b08f77aa2c6d0c3e1d85
results-target-controls/summary.json e78b138618daf7705173126d551f5cd83583f90fd6b90b85f48af059eaf87c4f
results-http-nodelay/summary.json b9ee640cc65209c13a5c06320ab47ee3bd6f2b04938ef71d9d7c5d03b7cc1dbf
results-http-focused/summary.json 620b8fed28e4683e1d59c97648ee396052c5f3c1a1ec536030df4537345f526e
Generation/variant Release binary SHA-256
v1/soft 5dccd266244aa3c756d40d87b551dce1d43cb3e0f5206a167df97afd5797b376
v1/pool-only 44f3b34e5f3b43f55200c75696ac349c494ff50dcde9a712f30abd4df0f96a9e
v1/hard 3324d01171a3597d81a0489e75c512542e2f3e8aef3bfd8ad69af098bd8ef8c9
v2/soft 52b5bd56417f2dc6a367efcfaf6fb03d28b5a9c1c0f83c7c2dce69fa80a320c7
v2/hard 07ed1f4836a0bcc6caf30c2c733e38fb30f22cd17bb6c3390232f02a15a981b5

v1 benchmark patch SHA-256: 22a1c252fd22e4b0bc170b462d678c0d65ec688fb39c0d5f3104a66068c6c819.
v2 benchmark patch SHA-256: 2e50f78d41ed0a5a9bad926b71c5fddc018ec91289373f57f0f4138aadad6af3.
Pool-only runtime source blob: f12d150117680e76bd245f891c050612c26dc391.

The CLI-only results and their memory uncertainty remain in #529. These transport measurements do not establish a CLI RSS improvement, and the separate external SSR/browser run is not a before/after comparison.

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.

1 participant