Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/storage-git-gix/parked-gix/.nornir/transport-encoding-design.md

Negotiated transport encoding for gunnar: measured, and rejected

Opened 2026-08-03, against master at 87524d6 ("Admission control, and a measured decision not to parallelise"). Companion to .nornir/performance-design.md, which is owned by another agent: nothing here edits that file, or any crate. New entries use the same block format and continue its numbering at P-019, so they merge unchanged.


The answer, first

Do not build the capability. P-001 makes it unnecessary, and on the largest real corpus measured here pack-copy beats every negotiated alternative on both axes at once.

On a real 74,523-object repository, serving the whole thing:

bytes on wire server CPU
pack-copy (P-001), no capability, stock git can read it 70,028,538 25.30 ms
best negotiated arm at comparable CPU (raw objects + zstd-3) 103,168,818 2,039.29 ms
best negotiated arm at any CPU (raw objects + zstd-9) 72,551,454 3,940.52 ms
today (per-object zlib L6) 627,387,423 28,609.15 ms

Raw-objects-plus-one-zstd-stream is the right idea and it does beat what gunnar does today by a wide margin: 6.1× fewer bytes and 14× less CPU than the current writer. P-001 simply beats it by more, for free, without touching the format or asking anything of the client. Spending zstd's CPU to approach a number pack-copy reaches with a memcpy is the whole result.

There is exactly one arm that sends fewer bytes than pack-copy: pack-copy's own content, undeflated, through one zstd stream (A6 below). It buys 9.2% on the large corpus for 39× the server CPU and a mandatory client-side transcode. At the load target — thousands of concurrent clones on one box, where serve CPU is the thing that saturates — that trade runs backwards. It is recorded as P-019 and marked WONTFIX rather than TODO.

And the third layer's premise turns out to be the weakest part. A capability only pays when both ends are gunnar. Stock git is the client in nearly every request a forge serves, and stock git can consume pack-copy output unmodified. The capability's addressable share of traffic is the share where the client is also gunnar, which is small, and on that share it loses anyway.


What was measured, and on what

Machine. oden, an AMD Ryzen Threadripper PRO 3975WX. nproc = 32, sha_ni/avx2/ bmi2 present. Every number in this file is single-core, pinned with taskset, best-of-N after a warm-up (N = 7 for the two smaller corpora, N = 3 for the large one). Measured 2026-08-03. The box was shared with other agents during the run; best was used rather than mean for exactly that reason, as P-008 did.

Probe. A standalone throwaway crate at /home/rickard/scratch/cargo/gunnar-transport-probe/, pinned to the exact versions gunnar's Cargo.lock resolves (gix-pack 0.73.0, gix-odb 0.83.0, gix-object 0.63.0, gix-hash 0.26.0, flate2 1.1.9 with zlib-rs), plus zstd 0.13.3 and lz4_flex 0.13.1. --release, opt-level = 3. It is a probe, not the bench suite; P-015 still owns the historized one. No crate in /home/rickard/git/gunnar was modified.

Corpora. Real git objects out of real repositories, cloned --bare --no-hardlinks into scratch and git repack -adf --window=250 --depth=50'd, i.e. exactly the shape a client's git push deposits in the incoming tier.

corpus source objects raw content pack on disk
small /home/rickard/git/gunnar 690 7.62 MiB 1.41 MiB
mid /home/rickard/git/nornir 12,305 325.51 MiB 7.25 MiB
large /home/rickard/.hermes/hermes-agent 74,523 2,189.55 MiB 66.79 MiB

The ground-truth check that makes the pack-copy row believable. git clone --bare of each corpus, by stock git, produces packs of 1,475,754 / 7,600,737 / 70,028,558 bytes. The A5 pack-copy arm below produces 1,475,734 / 7,600,717 / 70,028,538, twenty bytes fewer in each case, which is the SHA-1 trailer the probe does not append. The pack-copy row is therefore not a model of what git would send; it is byte-for-byte what git does send, measured against coreutils-grade external truth rather than against the probe's own arithmetic.

What the CPU columns include and exclude. Codec work only: deflate/inflate, zstd, lz4, and the memcpy that assembles a stream. The graph walk, object lookup, syscalls and the network are excluded, as performance-design.md's CPU budget excludes them and for the same reason. So is the trailer hash, P-008's subject, ~0.17 ms/MiB of output on every arm.

Every arm is round-trip asserted before it is timed. A3/A4/A6 assert the decompressed stream is byte-identical to the input; A1 asserts the inflate recovers exactly raw content bytes. An arm that lost data would abort rather than post a flattering number. This mattered: it is the only thing standing between "one zstd stream" and a plausible-looking wrong answer.


The three layers, kept separate

  1. Wire to stock git. Zero freedom. Each object in a packfile is an independent zlib stream. This is the format. Nothing below proposes changing it, and the winning arm does not touch it.
  2. At rest, the cold tier. Total freedom, and out of scope here; see P-005 and the unresolved §4 note, which this file does not attempt to settle.
  3. Gunnar ↔ Gunnar. Negotiable. The subject of this file, and the layer where the measurement says do nothing.

The measured table

Small corpus, gunnar: 690 objects, 7.62 MiB raw content.

arm bytes on wire ratio compress CPU decompress CPU
today: per-object zlib L6 (zlib-rs) 2,889,059 2.77× 107.91 ms 12.05 ms
today, at L1 3,965,731 2.01× 32.05 ms 12.36 ms
zstd-1 over the finished packfile 2,877,818 2.78× 108.47 ms 12.13 ms
zstd-3 over the finished packfile 2,797,649 2.86× 110.26 ms 12.14 ms
zstd-9 over the finished packfile 2,800,999 2.85× 112.98 ms 12.16 ms
raw objects + one zstd-3 stream 1,808,260 4.42× 22.02 ms 4.37 ms
raw objects + one zstd-9 stream 1,302,180 6.13× 64.94 ms 2.99 ms
raw objects + one lz4 stream 3,896,873 2.05× 16.72 ms 3.10 ms
pack-copy (P-001) 1,475,734 5.41× 0.05 ms 0 ms
A6: pack entries raw + one zstd-3 1,284,279 6.22× 18.93 ms 2.62 ms

Mid corpus, nornir: 12,305 objects, 325.51 MiB raw content.

arm bytes on wire ratio compress CPU decompress CPU
today: per-object zlib L6 98,028,776 3.48× 4,279.11 ms 380.72 ms
today, at L1 139,397,943 2.45× 1,077.66 ms 421.55 ms
zstd-3 over the finished packfile 72,265,771 4.72× 4,566.54 ms 429.30 ms
zstd-9 over the finished packfile 69,617,715 4.90× 5,038.38 ms 432.24 ms
raw objects + one zstd-3 stream 11,996,845 28.45× 343.51 ms 146.95 ms
raw objects + one zstd-9 stream 7,468,173 45.70× 756.83 ms 142.72 ms
raw objects + one lz4 stream 119,234,280 2.86× 761.93 ms 215.24 ms
pack-copy (P-001) 7,600,717 44.91× 0.98 ms 0 ms
A6: pack entries raw + one zstd-3 6,255,991 54.56× 133.87 ms 17.71 ms

Large corpus, hermes-agent: 74,523 objects, 2,189.55 MiB raw content.

arm bytes on wire ratio compress CPU decompress CPU
today: per-object zlib L6 627,387,423 3.66× 28,609.15 ms 2,326.39 ms
today, at L1 883,493,001 2.60× 6,760.03 ms 2,573.61 ms
zstd-1 over the finished packfile 504,191,848 4.55× 29,003.37 ms 2,535.30 ms
zstd-3 over the finished packfile 438,554,115 5.24× 30,491.35 ms 2,643.95 ms
zstd-9 over the finished packfile 408,463,458 5.62× 33,552.97 ms 2,656.34 ms
raw objects + one zstd-3 stream 103,168,818 22.25× 2,039.29 ms 887.09 ms
raw objects + one zstd-9 stream 72,551,454 31.65× 3,940.52 ms 735.08 ms
raw objects + one lz4 stream 776,050,979 2.96× 7,385.14 ms 2,322.82 ms
pack-copy (P-001) 70,028,538 32.79× 25.30 ms 0 ms
A6: pack entries raw + one zstd-3 63,559,867 36.12× 988.72 ms 118.46 ms
A6: pack entries raw + one zstd-9 60,751,204 37.79× 2,369.98 ms 117.89 ms
A6b: pack entries raw + one lz4 77,943,345 29.46× 694.65 ms —

Reading the rows

The naive arm loses, as predicted, and the number is worth having. zstd over the finished packfile buys 3.2% / 26.3% / 30.1% on the three corpora for +2.34 / +287 / +1,882 ms. On the small corpus it is the rounding error the hypothesis expects, because every object is already deflated and zstd sees entropy. On the two larger ones it is not negligible, but only because those packs contain many similar deflate streams, and what zstd finds is duplication between compressed entries, not inside them. It is a real 26–30%, obtained at 7–15% extra CPU on top of the most expensive arm in the table. Nobody should build it, but the "compressing entropy buys exactly zero" framing is too strong and the measurement says so.

The hypothesis is correct about the shape and still loses. Sending objects raw and zstd-ing the concatenation is dramatically better than what gunnar does today. On the large corpus that is 6.1× fewer bytes at 14× less CPU than per-object zlib L6. Cross-object redundancy really does become visible to the compressor for the first time, exactly as predicted, and the effect is large (mid corpus: 28.45× vs 3.48×). The problem is that git's own delta encoder already extracted that redundancy in the pack the client pushed, and P-001 ships it for a memcpy. Two mechanisms are competing to exploit the same structure; the free one wins.

lz4 is not competitive at any size and should not be reconsidered. It is the fastest compressor here and the worst by bytes on every corpus (2.05× / 2.86× / 2.96×), losing to plain per-object zlib on two of three. It decompresses fast, which is worth nothing when the winning arm decompresses nothing at all.

The interesting row is A6, and it is why the answer is "do not build" rather than "impossible". Taking P-001's pack entries, stripping the per-entry zlib wrapper while keeping the delta instructions, and running one zstd stream over the concatenation is the only arm that beats pack-copy on bytes: 13.0% / 17.7% / 9.2% fewer. It is the genuinely correct synthesis of the two ideas. Its cost is 379× / 137× / 39× the server CPU of plain pack-copy, plus a client transcode. Bad trade for a forge.


The safety boundary, and whether it eats the win

It does, and hardest for the arm people would actually reach for.

Everything in .git is zlib: loose objects are zlib, pack entries are zlib. A gunnar client that receives raw-encoded objects cannot store them; it must re-deflate to produce something stock git can read. Today that cost is zero: git index-pack writes the received pack bytes to .git/objects/pack verbatim. The transcode is a cost that exists only because the capability exists.

corpus A3/A4 transcode (deflate all content, L6) A6 transcode (re-deflate entry payloads only)
small 109.45 ms 63.31 ms
mid 4,332.09 ms 320.90 ms
large 30,091.57 ms 3,625.01 ms

For A3/A4 the transcode is by construction identical to arm A1. The client redoes, byte for byte, the deflate the server was celebrated for skipping. (The figures above come from a second probe run and differ from the A1 rows by 1.4–5.2%; that is this box's noise under other agents' load, and the two are the same operation.) On the large corpus that is 29.7 seconds of client CPU to save 2.0 seconds of server CPU. The win is not reduced; it is relocated, and made larger. A3/A4 are dead on this alone, before the byte counts are even consulted.

For A6 the transcode is genuinely much smaller and this must be said honestly: A6 ships delta instructions, so the client re-deflates only 122,273,926 B of entry payload on the large corpus rather than 2.19 GiB of content. Measured: 63.31 / 320.90 / 3,625.01 ms, i.e. 2.4–3.7× the server's own A6 encode. That transcode does produce a real, valid pack, 69,761,188 B on the large corpus, marginally smaller than the 70,028,526 B git shipped, because zlib-rs at L6 is not git's exact deflate.

So A6's true system cost — server encode + client decode + client transcode — is 4,732 ms on the large corpus against pack-copy's 25.30 ms, or 187×, for 9.2% fewer bytes. Still a bad trade, but a bad trade rather than an absurd one. The distinction is worth preserving, because it is the difference between "wrong idea" and "right idea, wrong economics".

One asymmetry in pack-copy's favour that is easy to miss. The transcoded pack is not the pack the server holds. Deflate is not deterministic across levels or backends, as P-007's closing note records. So a raw-encoded transfer also gives up bit-identical replication between server and client, which pack-copy provides for free and which gunnar mirror (§11) would like to have.


What the capability costs when nobody negotiates it

A capability nobody negotiates is not free, and gunnar's own caps.rs is built around that fact; its module doc is about advertisements drifting out of sync with parsers.

Carrying gunnar-encoding=zstd would cost, permanently, on every stock-git request:

Against a benefit that is negative on bytes for A3/A4, and 9–18% of bytes for 39–379× server CPU for A6, on the fraction of requests where both ends are gunnar.


znippy-zoomies: what is usable, what it costs, and the C question

Corrected against the brief, which understated this workspace. znippy-zoomies is more than src/; it carries eight sibling crates. What follows is per-crate, because the P-006 toolchain cost belongs to exactly one of them and it is not the one people assume.

crate role runtime deps build.rs C / cmake / network
gatling (published as rotaryengine 0.1.10) fork-join engine anyhow, bytes, futures none none, pure Rust
linflate 0.1.10 raw-DEFLATE decompressor none none none, pure Rust, zero deps
znippy-zip-core ZIP central-dir + flush split gatling, linflate none none
lgz parallel gzip/tar decode gatling, linflate, flate2, tar, memchr, memmap2, anyhow none none by default
ljar, lzip-parallel parallel JAR/ZIP decode gatling, linflate, znippy-zip-core none none by default; opt-in zlib-ng feature pulls flate2/zlib-ng-compat → C + cmake
lbzip2 parallel bzip2 decode gatling, memchr, anyhow none none
zstd-sys-rs 1.5.18 zstd FFI libc yes cc + bindgen + vendored C, -march=native

P-006's cost is OpenZL's, and it does not follow from using zoomies. P-006 correctly faults znippy's codec (openzl-sys-rs) for cmake-building C/C++ and fetching facebook/openzl from GitHub during cargo build and linking stdc++. Nothing in znippy-zoomies does that. Its one C crate, zstd-sys-rs, vendors its zstd sources in-tree and compiles them with cc; there is no network fetch and no cmake. That is a different class of problem, and it is also an opt-in crate nobody here needs to use.

The decisive fact about the zoomies codecs, for this file's purposes: they are decompressors, all of them. Every flate2::write::GzEncoder occurrence in the workspace — lgz/src/lib.rs:131, lgz/src/parallel.rs:236,245, lgz/src/speculative.rs:702,744 — is inside #[cfg(test)] mod tests, building fixtures. There is no production compressor anywhere in znippy-zoomies. Serving a pack needs deflate. The compressor side of any transport encoding gets nothing from that repo, and would come from flate2/zlib-rs (already linked) or zstd (a new C build step).

linflate vs zlib-rs on real git objects: MEASURED, and it loses here

Same box, same day, single core, best-of-9, correctness asserted against zlib-rs's own output on every stream before timing (mismatch = 0 across all 87,708 streams tested).

corpus streams mean object zlib-rs linflate speedup
gunnar, loose objects on disk 690 11,585 B 497.1 MiB/s 374.3 MiB/s 0.75×
nornir, per-object streams in a pack 12,305 1,673 B 394.6 MiB/s 313.7 MiB/s 0.80×
nornir, loose objects on disk 191 23,749 B 597.9 MiB/s 449.6 MiB/s 0.75×
hermes, per-object streams in a pack 74,522 1,640 B 483.7 MiB/s 388.1 MiB/s 0.80×
gunnar, single biggest object, 380 KB 1 — 1,644.9 MiB/s 1,357.3 MiB/s 0.83×
hermes, single biggest object, 2.27 MB 1 — 377.8 MiB/s 290.1 MiB/s 0.77×
hermes, one 64 MiB stream over the same content 1 — 742.0 MiB/s 645.1 MiB/s 0.87×

linflate is 0.75–0.87× of zlib-rs on every git-shaped input tried, and it is not a small-object artefact. The one-big-stream row, built specifically to reproduce the shape linflate's README benchmarks (Silesia, one large buffer), still gives 0.87×. Rebuilt with RUSTFLAGS="-C target-cpu=native" to rule out a crippled SIMD build, the result is unchanged (0.78× / 0.83× / 0.84×); copy.rs:85-90 resolves AVX2/AVX-512 at runtime via is_x86_feature_detected!, so the default build was never the handicap.

The README's table (linflate ~700 MB/s vs zlib-rs ~550 MB/s, Silesia) is not contradicted for Silesia; that corpus was not run. What is measured is that it does not carry over to git objects on this box, and git objects are the only input gunnar has. The likely reason is visible in the design: linflate is full-buffer with a thread-local table pool, optimised for long streams with stable Huffman tables, whereas git deflates each object independently at level 6 with a fresh table, so table construction is amortised over 1.6 KB of output in a pack, not megabytes.

This is a negative result about a house crate and it should be read as one number, not a verdict on linflate. It is a well-built decompressor that wins on the corpus it was tuned for. It is simply not the faster path for git objects, and P-007 already moved gunnar onto the one that is.

Do git's streams contain flush boundaries? MEASURED: no

The brief's second correction is right in general and does not apply here. Only a measurement settles that.

linflate::deflate_scan::find_all_flushes scans for the 4-byte 00 00 FF FF that a Z_FULL_FLUSH produces. Run over every stream in every corpus, with each candidate then validated by inflate_segment-ing from it and checking the output is a byte-exact suffix of the true decompressed data:

corpus streams streams containing 00 00 FF FF candidates genuine boundaries
gunnar loose 690 0 0 0
nornir pack streams 12,305 0 0 0
nornir loose 191 0 0 0
hermes pack streams 74,522 6 6 0
hermes, one 64 MiB stream 1 1 1 0

Zero genuine flush boundaries in 87,709 real git streams. The six hits in the hermes pack are the coincidental in-Huffman-data matches deflate_scan.rs:17-19 explicitly warns about, and validation rejects all six. Git compresses with Z_FINISH and never emits a full flush, so deflate_scan has nothing to find in git data. The intra-stream parallel axis is not available for git objects, and the parallel axis for a pack is across objects, which is P-004's territory, where the serving tier is deliberately serial.

Where linflate would matter after P-001: barely

The brief's own position is the correct one, and the numbers support it. P-001 deletes the inflate from the common serve path entirely: a copied pack entry is never decompressed. What is left that genuinely inflates is repack(), connectivity checks on push, and policy gates that read blobs. All batch or receive-side work, none of it the clone path. Applying a 0.80× factor to a shrinking share of a shrinking cost is not worth a dependency, even a zero-dependency one. rotaryengine remains the right answer for the batch tier's parallelism (P-011), which is a separate question from which inflate runs inside each worker.


No, none of this requires forking gix, and here is the place that comes closest

The brief's position is tested below rather than assumed, and it survives. Three things were checked, and one genuine obstruction was found that is not about codecs.

1. The entry type is fully constructible. There is no codec lock-in. gix_pack::data::output::Entry (data/output/mod.rs:20-33) has four public fields:

pub struct Entry {
    pub id: ObjectId,
    pub kind: entry::Kind,        // Base | DeltaRef | DeltaOid — all public
    pub decompressed_size: usize,
    pub compressed_data: Vec<u8>, // whatever bytes gunnar wants
}

Nothing is sealed, nothing is #[non_exhaustive], and Entry::to_entry_header (data/output/entry/mod.rs:166) is public. Gunnar can construct entries carrying raw, zstd-framed or any other payload without touching gix. A fork buys nothing at this layer.

2. The pack-entry seam P-001 needs already exists as a public type. gix_pack::find::Entry { pub data: Vec<u8>, pub version } (find.rs:1-11) is exactly P-001's proposed read_pack_entry return shape, and it is what output::entry::Entry::from_pack_entry (entry/mod.rs:65-131) consumes. That function copies the existing compressed bytes verbatim and re-points the OfsDelta header (:89-112), downgrading only RefDelta (:113). gix_pack::Find (find_traits.rs:14) is a plain public trait with no sealing and no private supertrait; gunnar's TieredObjectStore can implement it over packs it already owns.

3. The one place a gix internal genuinely blocks something. Named, as asked:

gix_pack::data::output::bytes::FromEntriesIter holds its output field as gix_hash::io::Write<W>, private (data/output/bytes.rs:22), constructed in FromEntriesIter::new (bytes.rs:56-77). gix_hash::io::Write::new (gix-hash-0.26.0/src/io.rs:115-125) hardcodes crate::hasher(object_hash), which is sha1_checked::Sha1 with UBC collision detection. There is no way to inject a different hasher.

This matters concretely: P-001's prescribed fix step 3 is "route write_pack through gix_pack::data::output … rather than hand-rolling, per LAW 5". Doing that literally would hand the trailer back to sha1dc and give up P-008's landed 4.02×, precisely in the shape (L0, no deflate) where P-008 measured 4.02× rather than 1.06×. The two entries' prescriptions collide, and nothing in either file says so.

It does not require a fork, because the route around it is short and is the shape gunnar already has. Use gix for what is hard and generic, count::objects → entry::iter_from_counts(Mode::PackCopyAndBaseObjects) to produce output::Entry values, then keep gunnar's own PackWriter (which already owns TrailerHasher, crates/gunnar-pack/src/checksum.rs) to emit the bytes. All the types crossing that boundary are public. LAW 5 is satisfied: the delta/pack-copy logic is reused, and what is kept is a 60-line byte emitter gunnar wrote for a measured reason. Recorded as P-020.

A fourth thing worth flagging while in there. iter_from_counts fans out via gix_features::parallel::in_parallel_if with a thread_limit (iter_from_counts.rs:66, 75-79), and its threshold is counts.len() > 4_000, which nearly every real clone exceeds. On the serving path that is per-request intra-request fan-out, which P-004 settled against. Whoever lands P-001 must pass thread_limit: Some(1) on the serve path and let the batch tier use the pool. This is a one-field decision, not a fork, but it is silent if missed — the same shape as P-002.

Conclusion on the fork question: no fork. No gix internal blocks a zero-copy or alternative-codec path. The single obstruction found is a hardcoded hasher, not a hardcoded codec, and it is routed around by keeping a writer gunnar already owns. Inheriting ~40 fast-moving crates to avoid one Vec<u8> copy of a pack header would be a bad trade even if the obstruction were real.


Entries

P-019 — A negotiated non-zlib transport encoding is measurably worse than pack-copy, on both axes

It loses anyway, because P-001 extracts the same redundancy for free. Git's delta encoder already found it, in the pack the client pushed; pack-copy ships those deltas with a memcpy. MEASURED (large corpus, 74,523 objects, single core, oden, 2026-08-03): pack-copy 70,028,538 B / 25.30 ms; raw+zstd-3 103,168,818 B / 2,039.29 ms; raw+zstd-9 72,551,454 B / 3,940.52 ms. Pack-copy wins on bytes and on CPU against both.

The naive variant — zstd over the finished packfile — was measured too, as the version people reach for: 3.2% / 26.3% / 30.1% fewer bytes on the three corpora for +2.34 / +287 / +1,882 ms on top of the most expensive arm in the table. Not zero, as the "compressing entropy" argument predicts, because zstd finds duplication between similar deflate streams, but never worth its cost.

lz4 is dominated everywhere (2.05× / 2.86× / 2.96× on bytes) and needs no further consideration.

The one arm that beats pack-copy on bytes is pack-copy's own entries, undeflated, through one zstd stream (delta instructions preserved): 13.0% / 17.7% / 9.2% fewer bytes for 379× / 137× / 39× the server CPU, plus a client transcode measured at 63.31 / 320.90 / 3,625.01 ms, i.e. 2.4–3.7× the server's own encode. Total system cost on the large corpus, server encode + client decode + client transcode: 4,732 ms against pack-copy's 25.30 ms, or 187×, for 9.2% fewer bytes. At the load target, where serve CPU saturates before bandwidth does, that is the wrong direction. If bandwidth ever becomes the binding constraint and CPU does not, this is the arm to revisit, and only this one.

P-020 — P-001's prescribed fix routes through FromEntriesIter, which silently gives back P-008's 4.02×

So adopting FromEntriesIter would revert P-008, which landed crates/gunnar-pack/src/checksum.rs::TrailerHasher precisely to keep sha1dc off the pack trailer. And it would revert it in the shape where it is worth most: P-008 measured 1.06× at L6 but 4.02× at L0. L0 is exactly the shape pack-copy produces, because the deflate that used to dominate is gone. The two entries prescribe incompatible things and neither says so.

While there, pass thread_limit: Some(1) on the serving path. iter_from_counts fans out through gix_features::parallel::in_parallel_if whenever counts.len() > 4_000 (iter_from_counts.rs:66, 75-79), which nearly every real clone exceeds. That is per-request intra-request fan-out, which P-004 settled against. The batch tier (repack, gc) should use the pool; the serve path must not.

P-021 — linflate is 0.75–0.87× of zlib-rs on git objects; the zoomies workspace has no compressor at all

corpus mean object zlib-rs linflate speedup
gunnar, 690 loose objects 11,585 B 497.1 MiB/s 374.3 0.75×
nornir, 12,305 pack streams 1,673 B 394.6 MiB/s 313.7 0.80×
hermes, 74,522 pack streams 1,640 B 483.7 MiB/s 388.1 0.80×
hermes, one 64 MiB stream — 742.0 MiB/s 645.1 0.87×

Not a small-object artefact: the one-big-stream row reproduces the shape the README benchmarks (Silesia) and still gives 0.87×. Not a crippled build either: rebuilt with -C target-cpu=native the figures are unchanged (0.78×/0.83×/0.84×), and copy.rs:85-90 resolves AVX2/AVX-512 at runtime anyway. The README's Silesia numbers are not disputed; they do not carry over to git objects, plausibly because git deflates each object independently at L6, so linflate's full-buffer table setup amortises over ~1.6 KB rather than megabytes.

Two structural facts found alongside, both worth keeping.

  1. linflate is raw-DEFLATE only. No zlib CMF/FLG parse, no adler32 verify. Git objects are zlib-wrapped, so using it means skipping 2 bytes, ignoring 4, and giving up the adler32 integrity check that zlib-rs performs. That last part is a correctness cost, not a six-line wrapper.
  2. Git streams contain no Z_FULL_FLUSH boundaries, so deflate_scan is inert on git data. Across all corpora: 87,709 streams, 7 00 00 FF FF candidates, 0 genuine independently-decodable boundaries after validating each by inflate_segment against a byte-exact suffix of the true output. The intra-stream parallel axis does not exist for git objects; the axis for a pack is across objects, which is P-004's settled territory.

And the compressor gap, which is the load-bearing finding: znippy-zoomies contains no production compressor of any kind. Every GzEncoder in the workspace (lgz/src/lib.rs:131, lgz/src/parallel.rs:236,245, lgz/src/speculative.rs:702,744) is inside #[cfg(test)] mod tests. Serving a pack needs deflate; that comes from flate2/zlib-rs, already linked.


P-022 — EWAH is word-aligned so OR/AND NOT "obviously" vectorise; measured, negative, removed

The corpus settles it before any timing. MEASURED over the 53 real .bitmap files on oden, 2026-08-04 — 4,824 commit bitmaps, 28,001 literal runs:

literal-run length, u64 lanes share of runs share of lanes
exactly 1 69.6% 36.1%
2 16.3% 16.9%
3..=4 9.0% 15.4%
5..=8 3.4% 10.8%
9..=32 1.3% 8.7%
33..=128 0.3% 11.5%

95.0% of literal runs cannot fill one AVX2 register (4 u64 lanes), and they carry 68.5% of all literal lanes. Microbenchmark, both backends in one process, best-of-9, oden idle-pinned:

lanes 1 2 4 8 16 64 256 1024 8192 65536
avx2 ÷ portable 0.58× 0.52× 0.59× 0.53× 0.87× 1.44× 1.30× 1.40× 1.14× 1.08×

Crossover between 16 and 64 lanes; the gain decays above 1024 as the loop goes memory-bandwidth-bound, which is uber_garm's finding about encode on a different codec and the same box.

The dense pair was the remaining hope, and it also lost. Whole bitmaps are wider than literal runs (p50 83 words, p90 566, p99 1165), so or_with/and_not_with sit in the 1.30-1.44× region. A build dispatching only those was still slower on Reachability::objects_to_send, 7 of 8 alternating pairs, taskset-pinned to one core under box load 82-122:

pair 1 2 3 4 5 6 7 8
portable, µs/query 4.107 4.100 4.147 4.095 4.372 3.996 4.026 4.021
dense-only AVX2 4.237 4.154 4.197 4.138 4.192 4.159 4.096 4.120

~1.5%, seven times out of eight. Dispatching everywhere was 4/4 against at 23-52% slower. All three builds selected the identical 417,075 objects over 9,542 queries, so this is the same work three ways and not a faster build that did less.

The portable change that did stand on its own is separate and stayed: the literal arm of apply_ewah indexed with get_mut and break, which is a loop with an early exit and cannot be vectorised at all. It is now a clamped slice.

What could not be determined