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

nordisk/znippy

.nornir/zoomies-design.md

znippy-zoomies — design

Class: design · Function axis: f0 (zoomies as one system) + the parts: linflate core · the decoder family · the candidate-offset output model (B6) · the gatling engine · stree. Strict: this file states only the shape we run. Every why, every alternative, lives in zoomies-reasoning.md; the how-to in zoomies-guide.md; the measured numbers in benchmarks.md.

f0 — zoomies as one system (the whole)

znippy-zoomies is a sibling repo consumed as path deps: pure-Rust parallel decoders and fast primitives that znippy leans on. One invariant runs through everything: a decoder takes a borrowed &[u8] and never copies its input. That single rule is what lets a 164 GB planet file stream through on one read-copy — bytes are read into a slot once, sliced, decoded in place, and stitched, with no intermediate owning buffer.

The workspace is tokio-free except the iceberg crate (and the async gatling::io sibling). The pieces, and what each uses:

part crate/file role
linflate linflate/ the shared SIMD DEFLATE core — every decoder inflates through it
decoder family ljar lgz lbzip2 lzip-parallel format front-ends (JAR / gzip / bzip2 / ZIP), all on linflate
candidate-offset model (B6) ljar/src/chunk.rs, ljar/src/bin/ljar.rs the zero-copy parallel decode layout
gatling src/gatling.rs, src/gatling_io.rs generic no-barrier streaming engine (sync + async)
stree src/stree.rs static search tree for id→position lookup, re-exported into osm-katana

Consumed by: znippy proper (decompress path), and osm-katana (OSM→GeoParquet/PBF), whose xml→pbf path rides gatling and whose node store uses stree (re-exported as stree64).

linflate — the shared inflate core

linflate/ is a pure-Rust, full-buffer, SIMD DEFLATE decoder: the libdeflate full-buffer model (no streaming window) fused with zlib-ng branchless tricks. Hot working set ≈ 13 KB — it fits in L1-D. Files:

file holds
bitreader.rs 64-bit shift register; branchless refill guaranteeing ≥56 bits after refill, one refill per symbol
tables.rs packed-u32 Huffman; 11-bit litlen first-level table; thread-local table pool
fixed.rs the static fixed-Huffman tables
copy.rs const-generic copy::<N> SIMD match copy → vmovdqu
fastloop.rs the hot loop: preload-before-copy, 3-literal unroll, #[cold] slow paths
lib.rs the API

API (linflate/src/lib.rs):

fn / const signature note
OVERWRITE_HEADROOM pub const = copy::CHUNK_SIZE + 258 (:43) part of the API contract — every output region must include it so SIMD copies never scribble into a neighbour; undersized ⇒ OutputOverflow, not UB
inflate_into (data, &mut [u8]) → Result (:81) decode into a caller buffer
inflate_segment (…) (:93) decode a slice that need not end on BFINAL=1
inflate_segment_with_prefix[_limited] (:108, :120) segment decode with a back-reference prefix window
inflate_to_vec (data, expected) → Vec<u8> (:206) allocates expected + OVERWRITE_HEADROOM, set_len(expected)
error InflateError::OutputOverflow (:53) undersized output

inflate_segment decoding a slice that does not require BFINAL=1 is the linchpin of parallel decode: mid-stream segments — split at Z_FULL_FLUSH boundaries by deflate_scan — are each independently decodable.

the decoder family

Four format front-ends, one shared core. Each takes a borrowed &[u8]:

crate format parallelism
ljar JAR parallel (per-entry + per-chunk)
lgz gzip parallel (flush-marked, concatenated members, and arbitrary markerless via speculative resync)
lbzip2 bzip2 parallel over BWT blocks
lzip-parallel ZIP parallel over entries

All four inflate through linflate; none owns a second DEFLATE implementation.

the candidate-offset output model (B6)

The zero-copy parallel decode layout. One pre-allocated buffer; the flow:

  1. Candidate offset per segment = prefix sum of generous caps (not true sizes).
  2. Decode all segments in parallel, in place, each at its candidate offset — zero per-segment allocation (ljar/src/chunk.rs:220, output.resize(acc, 0) pre-allocates the whole candidate region once).
  3. Forward-seek compaction (chunk.rs:267): walk segments in order, moving each down to its true cumulative offset. Because actual ≤ cap, every move is leftward, so nothing not-yet-moved is ever clobbered. Then set_len(total).
  4. Spill path (chunk.rs:258): a segment that outgrows its cap spills to an owned Vec (rare) → sequential rebuild. It never corrupts; output is byte-identical.

Two implementations of this model:

impl where shape
library ljar::chunk::decode_chunk_into (ljar/src/chunk.rs:173) rayon fork-join per chunk
binary ljar/src/bin/ljar.rs woven into the no-barrier 5-thread pipeline Reader → Main(carry+split) → N Workers → Collector → Writer (232 MB ring slots)

the gatling engine

src/gatling.rs — a generic, no-barrier streaming engine. A Read source is split into segments in a bounded zero-copy slot ring; N worker threads transform each segment with no cross-worker barrier; an ordered collector reassembles in stream order and hands to a Sink. Two modes:

mode driver workers produce collector
byte run + Codec (:68) / Sink (:83) Vec<u8> concatenate + safe_end cut
typed run_typed + TypedCodec (:99) / TypedSink<T> (:127) arbitrary Output forward each in order, no reassembly (split pre-cuts logical segments)

Config (:156): chunk_size, carry_headroom, ring_slots, initial_carry, slot_fill (slot size = carry_headroom + chunk_size). SlotFill (:141): Incremental grows a slot to the bytes read (small/partial inputs); Big sizes once and reuses without re-zeroing (planet-scale).

Async sibling src/gatling_io.rs (gatling::io) — same invariants on tokio: run / run_ordered, bound by max_in_flight; payloads are bytes::Bytes moved, never cloned. For I/O-bound work (e.g. S3 multi-PUT).

The 1→32→1 principle: 1 reader only reads (a cheap O(n_workers) candidate-offset split, no CPU work); N workers carry a unit through all heavy stages — boundary-find, decode, parse, encode, sort — with no barrier and self-dispatch; 1 writer only stitches in order. Heavy work on a 1-end is the serial ceiling.

stree

src/stree.rs — a static search tree for id→position lookup under the OSM node store, re-exported into osm-katana as stree64. A cache-line S⁺-tree with branchless SIMD compare and prefetched batch traversal.

type / fn signature note
STree64 pub struct (:55) in-memory tree
STree64Mmap pub struct (:175) internal nodes only; mmap is the leaf layer
find_exact (q) → Option<usize> (:123) serial lookup
batch_stream::<P> (&[i64]) → Vec<Option<usize>> (:773) software-pipelined + prefetched
lookup_batch_pipeline::<16> (&[i64], mmap) → Vec<…> (:387) mmap-backed batch
count_lt AVX2, dispatched once per query/batch (count_lt_avx2) branchless node compare
advise_hugepage (&[T]) (:487) THP hint for the RAM-bound regime

Derived from Ragnar Groot Koerkamp's static-search-tree (MIT, © 2025) — acknowledged in stree.rs:3. Our contribution at i64 width: B=16 (two cache lines) internal nodes.