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

nordisk/znippy-zoomies

.nornir/gatling-design.md

Gatling — design

Class: design · Function: gatling (the no-barrier engine). Strict: this file states only the shape we ship. The why (the rayon ban, the "1-ends cook" bug, the road here) lives in gatling-reasoning.md; the how-to is gatling-guide.md; the plain-words intro is gatling-for-idiots.md.

Source (leaf gatling crate, re-exported by znippy-zoomies): gatling/src/gatling.rs (streaming), gatling/src/gatling_forkjoin.rs (fixed set), gatling/src/gatling_io.rs (async I/O), gatling/src/gatling_ordered.rs (ordered item sink), gatling/src/parwrite.rs (parallel writers).

The shape every variant shares

One idea in several bodies:

  1. No barrier. There is never a "submit all, then await all" stall. The instant a worker finishes a unit it claims the next — it does not wait for a slow sibling at a chunk boundary. An N-core box stays N-busy even when units cost wildly different amounts.
  2. Self-dispatch via one atomic. A single shared AtomicUsize hands out the next index with fetch_add — no central scheduler, no per-unit channel send, the cheapest 1→N hand-off.
  3. std::thread::scope substrate. The sync engines are pure scoped threads; the async sibling is tokio tasks. Scoped threads let worker closures borrow caller stack data directly — no 'static/Arc ceremony.
  4. Zero-copy, zero-contention output. Each unit owns a disjoint slice of the output (a pixel row, an index slot, a byte range in a slot ring). Workers write in place — never sharing a cache line, never locking.

The reader only reads; the collector only stitches in order; all heavy work is in the workers in the middle.

Two families

A. Streaming — work arrives a chunk at a time (gatling.rs)

Reader → split → N workers → collector → sink. The caller plugs in a codec (how to split + transform) and a sink (how to consume); the engine carries no format knowledge.

Tuned by Config { chunk_size, carry_headroom, ring_slots, initial_carry, slot_fill }. ring_slots is the steady-state width; SlotFill::Incremental (default) faults only the bytes read, SlotFill::Big pays the full slot fault once for planet-scale inputs.

B. Fork-join — the work set is known upfront (gatling_forkjoin.rs)

No reader, no carry: a fixed 0..n range (or a row grid) and N workers draining a shared counter. The family you reach for when you already hold a Vec/grid/file list.

C. Async I/O sibling — latency-bound fan-out (gatling_io.rs)

For I/O-bound work (PUT/GET/fsync) a blocked thread is wasted. gatling::io keeps every invariant (no barrier, bounded N-in-flight, ordered output) but swaps the substrate to ≤ max_in_flight tokio futures. Payloads are bytes::Bytes, moved in and out, never deep-copied.

D. Parallel-writer — the writer itself fans out (parwrite.rs)

The families above (and the codecs built on them) funnel decoded output through one collector + one writer thread — the last un-parallelised stage. parwrite removes it: the same no-barrier, atomic-cursor, std::thread::scope pool, but each worker writes its own output — no collector, no writer thread on the critical path. It is safe without any ordering because the output units are provably disjoint:

Each worker reuses its scratch buffers across the units it claims (zero-alloc hot loop), never copies input into the pool, and is rayon-free. This is the family a codec reaches for when output sizes are known up front (a central directory, or a prior decode pass) and ordering can be dropped — see the writer story in gatling-reasoning.md.

The variants

Variant Shape Use when
gatling::run streaming, byte mode, reassembled bytes arrive a chunk at a time, output reassembled at boundaries (bz2/gz → XML)
gatling::run_typed streaming, typed mode, in-order records workers produce finished records, split cuts logical boundaries
gatling::io::run / run_ordered async, latency-bound I/O fan-out over ready payloads; run = fastest-first, run_ordered = submission order
gatling::ordered::run_ordered_sink item-oriented, ordered caller yields (Label, Input), N workers map, sink emits in producer order through a bounded reorder buffer
gatling_forkjoin::gatling_for_each fixed 0..n → Vec<T>, index order a fixed set of independent CPU units, results back
gatling_forkjoin::gatling_run fixed 0..n, side-effects only each worker writes into per-i state the caller owns
gatling_forkjoin::gatling_map_owned owned Vec<T> consumed → Vec<R>, input order per-item payload owned and possibly !Sync
gatling_forkjoin::gatling_for_each_balanced 0..n, LPT + batched → Vec<T> unit costs heavily skewed — schedule heaviest-first, claim in batches
gatling_forkjoin::gatling_map_balanced owned &[T], LPT + batched, f(i, &item) → Vec<R> skewed work you map by reference then group-by-key
gatling_forkjoin::gatling_scanlines 2D rows × stride buffer each row independent, writes a disjoint slice (rendering)
parwrite::extract_entries_unordered N workers pread + decode + write own file independent archive entries (ZIP/JAR members) → separate output files, no ordering
parwrite::write_segments_positional N workers decode → pwrite at offset independent regions of one pre-sized stream, offsets known (prefix-sum), exact byte order
parwrite::write_slices_positional N workers pwrite ready slices at offsets segments already decoded (prior pass), zero-copy parallel pwrite into one stream

The balanced bodies (skew killers)

gatling_for_each_balanced(n, n_workers, batch, weight, f) adds two refinements while keeping 0..n output order: a heaviest-first (LPT) schedule from a caller-supplied weight(i) (e.g. file byte size), and batched self-dispatch (claim batch indices per atomic fetch_add; batch == 0 ⇒ 1). gatling_map_balanced(items, n_workers, batch, size_of, f) is its sibling for an owned Vec of heterogeneous tags: size_of(& item) and f(i, &item) both read one borrow (no external re-index), results land in disjoint output slots in original order, ready to group-by-key. First real consumer: nornir's member-scan parsing every .rs across all cores scheduled by byte size.

Whereabouts map — where every piece lives

Every engine variant, codec, and the law test, with a file:line you can jump to. The engine lives in the leaf gatling crate; the codecs are workspace members; all are re-exported / path-depended by the root znippy-zoomies crate.

Thing Crate file:line
streaming byte engine run gatling gatling/src/gatling.rs:328
streaming typed engine run_typed gatling gatling/src/gatling.rs:622
Codec / Sink traits gatling gatling/src/gatling.rs:68 / :83
TypedCodec / TypedSink traits gatling gatling/src/gatling.rs:99 / :127
Config (chunk_size, ring_slots, slot_fill…) gatling gatling/src/gatling.rs:156
gatling_for_each gatling gatling/src/gatling_forkjoin.rs:60
gatling_run gatling gatling/src/gatling_forkjoin.rs:147
gatling_for_each_balanced (LPT + batch) gatling gatling/src/gatling_forkjoin.rs:209
gatling_map_balanced (item-ref LPT) gatling gatling/src/gatling_forkjoin.rs:318
gatling_map_owned (consumes Vec<T>) gatling gatling/src/gatling_forkjoin.rs:409
gatling_scanlines (2D rows) gatling gatling/src/gatling_forkjoin.rs:490
async io::run / io::run_ordered gatling gatling/src/gatling_io.rs:95 / :147
ordered::run_ordered_sink / OrderedSink gatling gatling/src/gatling_ordered.rs:113 / :77
chunk_revolver slot pool (ChunkRevolver, get_chunk_slice) gatling gatling/src/revolver/{mod,revolver,ring}.rs
chunk_revolver coalescing mode (with_coalescing, Filler, PackedSlice, Returner) gatling gatling/src/revolver/coalesce.rs:255,351,66,143
parwrite::extract_entries_unordered (independent files) gatling gatling/src/parwrite.rs:85
parwrite::write_segments_positional (decode → pwrite) gatling gatling/src/parwrite.rs:191
parwrite::write_slices_positional (ready slices → pwrite) gatling gatling/src/parwrite.rs:263
parwrite::EntryJob / WriteStats gatling gatling/src/parwrite.rs:57 / :47
lgz entry decompress_tar_gz_filter; split lgz lgz/src/lib.rs:63; chunk.rs:29; deflate_scan.rs:92
lbzip2 block scan; split lbzip2 lbzip2/src/block_scan.rs:85 (all-blocks), :221 (split); chunk.rs:35
ljar entry decompress_jar; gatling fan; split ljar ljar/src/lib.rs:65; reader.rs:122; deflate_scan.rs:16
lzip-parallel entry decompress_zip; gatling fan; split lzip-parallel lzip-parallel/src/lib.rs:58; reader.rs:172; deflate_scan.rs:16
linflate DEFLATE core inflate_segment* / inflate_to_vec linflate linflate/src/lib.rs:81,93,108,206
linflate flush-boundary SIMD scanner (FlushBoundary, find_next_flush, find_all_flushes) linflate linflate/src/deflate_scan.rs:25,57,201
the law test (whole_workspace_is_rayon_free, workspace_src_roots) root tests/rayon_free_law.rs:127,52

parwrite (the writer-lift) is landed on master alongside every other variant.

Gatling-adjacent primitives (same crate, same no-rayon law)