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 ingatling-reasoning.md; the how-to isgatling-guide.md; the plain-words intro isgatling-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:
- 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.
- Self-dispatch via one atomic. A single shared
AtomicUsizehands out the next index withfetch_add— no central scheduler, no per-unit channel send, the cheapest 1→N hand-off. std::thread::scopesubstrate. 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/Arcceremony.- 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.
- Byte mode —
run+Codec/Sink: workers emit raw bytes; the collector concatenates and cuts at aSink::safe_endboundary (e.g. end of an XML element). - Typed mode —
run_typed+TypedCodec/TypedSink:splitguarantees logical boundaries so workers emit finished records; the collector forwards each in order, no reassembly.
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:
- Independent files —
extract_entries_unordered: each workerpreads its entry's compressed bytes from the shared input (thread-safe positional reads, no shared file offset), decodes, and writes its own output file (ZIP/JAR members are independent). - Independent regions of one stream —
write_segments_positional(decode thenpwrite) /write_slices_positional(already-decoded slices,pwrite): the caller pre-computes each segment's offset (prefix-sum of known output sizes), pre-sizes the file, and each workerpwrites its region at its offset. Positional writes to disjoint regions never conflict, so exact stream byte-order holds with no ordered collector.
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)
chunk_revolver— the zero-allocation slot pool behind the streaming engine (chunk_revolver-design.md).psort— parallel sample sort over 16-byte AoS records keyed by a leadingi64.stree/stree32— the AVX2 static search tree (stree-design.md).xml/vtd/json— zero-copy parallel record scanners; all three parallelize throughgatling_forkjoin::gatling_for_each.