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

nordisk/znippy-zoomies

lbzip2/design_for_claude.md

design_for_claude.md — Read This First Every Session

Purpose: Stop the rediscovery loop. Read this file + design.md before touching any code. design.md is the human-readable design doc (Rickard maintains it). This file adds AI-specific context: what NOT to do, hardware, timing data, session protocol.

Session start protocol:

  1. Read design_for_claude.md (this file)
  2. Read design.md (architecture, module map, benchmarks)
  3. Only then ask what to work on

1. Core Design Principles

  1. ZERO ALLOCATIONS in the hot path. Pre-allocate everything. Reuse buffers. Only large one-time allocations are acceptable (ring slots, output buffer).
  2. Zero-copy. The 200 MB compressed data stays in place. Send offsets, not data.
  3. Simple parallelism. N cores = N workers pulling from a work list. No magic.
  4. The iBWT+RLE2 optimization is sacred. 1.5x faster than C libbz2 single-thread. 2-step prefetch, raw pointer output, memset repeats. DO NOT TOUCH.

2. What This Crate Does

Pure Rust parallel bzip2 decompressor. No C/FFI. Two interfaces:

Primary use case: decompressing 147 GB planet.osm.bz2 (~1.5 TB decompressed) as part of the osm-katana pipeline.


3. CLI Pipeline Architecture (lbunzip2)

Worker pool pipeline -- NO rayon barrier:

Reader --> Main (carry+split) --> 32 Workers --> Collector --> Writer
  ^                                                    |
  +------------- slot recycle -------------------------+

Key design points

Ring Buffer ("Slot Pool")

Carry Mechanism (zero-copy for the 200 MB)

Data Flow Per Slot

  1. Reader fills slot with 200 MB compressed data (~50-90ms on NVMe)
  2. Main thread receives slot, copies tiny carry into headroom (~1.8ms)
  3. Main thread calls split_chunk() -> 32 segment boundaries (~3-14ms)
  4. Main thread computes new carry from split boundaries (immediate, no decode wait)
  5. Main thread posts 31 work items to worker channel, loops to next slot
  6. Workers decode segments -> send (chunk_id, seg_id, Vec) to collector
  7. Collector accumulates results per slot, when all 32 done -> sends to writer
  8. Writer writes segment outputs to disk, collector recycles slot

Constants

CHUNK_SIZE     = 200 MB    // compressed data per slot
BUF_CAP        = 4 MB      // BufReader/BufWriter capacity
RING_SLOTS     = 6         // number of pre-allocated slot buffers
CARRY_HEADROOM = 32 MB     // space for carry at start of each slot
SLOT_SIZE      = 232 MB    // CARRY_HEADROOM + CHUNK_SIZE

4. Parallel Split + Decode Strategy

Parallel Offset Finding (via rayon, O(N))

This is the key insight -- NOT a full scan:

  1. Calculate n_splits = n_workers evenly-spaced nominal positions in 200 MB
  2. Forward-scan from each nominal position for the next BLOCK_MAGIC
  3. Quick-verify with 73-bit header check (rejects false positives instantly)
  4. Each scan covers only ~500 bytes forward
  5. Cost: 3-14ms (negligible -- DO NOT try to optimize this)

Split still uses the rayon thread pool (lib.rs). Workers use std::thread.

Segment Decode

pbzip2 Concatenated Streams

Planet files are ~1.3M mini-streams concatenated (~120 KB each). FINAL_MAGIC appears every ~120 KB. Each segment decoder handles: FINAL_MAGIC -> skip CRC + pad + BZhN header -> continue to next BLOCK_MAGIC


5. Allocation Policy

What's good (keep):

Current output allocation:


6. Block Decode Pipeline (per block, per thread) -- DO NOT CHANGE

BitReader (64-bit buffer, bulk 8-byte refill)
  -> Header (CRC + orig_ptr + bitmap + selectors)
  -> Huffman (10-bit packed u16 lookup, 2KB/table, L1-resident)
  -> MTF (fast-path n=0, n=1)
  -> RLE1 (RUNA/RUNB) -> tt[] array (~3.6 MB, thread-local pool)
  -> Inverse BWT (in-place T-transformation)
  -> RLE2 (2-step prefetch pointer chase, raw pointer output, memset repeats)
  -> output bytes

This pipeline is 1.5x faster than C libbz2 single-threaded. The iBWT+RLE2 prefetch optimization is the core of that speedup. All Huffman tables, selectors, bitmaps are stack-allocated -- zero heap.

block.rs refactoring:


7. Stage Breakdown (Single-Thread, Odin, Liechtenstein)

Stage Time %
Header + bitmap + selectors + trees 4 ms 0.7%
Huffman decode + MTF + RLE1 129 ms 22.7%
Inverse BWT 121 ms 21.3%
RLE2 + output 314 ms 55.2%
Total 568 ms

RLE2 dominates: dependent pointer chain through ~3.6 MB random-access tt[]. Memory-latency-bound. Prefetch helps but cannot break serial dependency.


8. Timing Data (Planet 1GB, Odin, 32 cores)

Worker pool pipeline (current):

Phase Time Notes
Reader I/O 50-90ms NVMe
Carry copy ~1.8ms Tiny
Split boundaries 3-14ms Via rayon thread pool
Main thread total ~5-16ms Posts work, loops immediately
Workers decode (wall) ~1000ms 32 segments across all workers

Total: 5.2s, 1909 MB/s (1 GB -> 9.86 GB decompressed)

Comparison with old architecture:

Old (rayon barrier) New (worker pool)
Architecture par_iter().collect() 32 persistent workers
Slots 4 6
Segments per chunk 255 32
Barrier waste ~700ms/slot ~0ms
1 GB time 5.5s 5.2s
Throughput 1804 MB/s 1909 MB/s

Timing Feature

cargo build --release --features timing -- instrumented stderr output. Code in chunk.rs and bin/lbunzip2.rs, #[cfg(feature = "timing")]. Segment CSV: /tmp/lbzip2_segments.csv.


9. Module Map

src/
  lib.rs           # BLOCK_MAGIC/FINAL_MAGIC, dedicated rayon pool (LBZIP2_THREADS)
  bitreader.rs     # 64-bit buffered reader, arbitrary bit offset, peek/consume
  block.rs         # decode_block_common + rle2_decode_alloc/into, thread-local tt pool
  block_scan.rs    # 48-bit scanner, split_boundaries_parallel, quick_verify
  bwt.rs           # Inverse BWT (in-place T-transformation)
  chunk.rs         # split_chunk, decode_segment, ChunkDecoder, pbzip2 support
  huffman.rs       # 10-bit packed lookup + tree fallback
  mtf.rs           # Move-to-front: fast n=0/n=1
  parallel.rs      # In-memory parallel (small files only)
  reader.rs        # StreamingBz2Read + ParallelBz2Read
  stream.rs        # Sequential decoder (reference/benchmark)
  bin/lbunzip2.rs  # CLI: worker pool pipeline (reader + main + 32 workers + collector + writer)

Public API (chunk.rs):


10. Build & Test

cargo build --release                    # normal build
cargo build --release --features timing  # with timing instrumentation
cargo test                               # 25 unit tests
cargo test --release --test decompress_bench -- --nocapture --ignored  # benchmark
cargo test --release --test stage_breakdown -- --nocapture --ignored   # hotspot analysis
LBZIP2_THREADS=8 cargo run --release --bin lbunzip2 -- input.bz2 out  # override threads

11. Test Data


12. Hardware

Machine CPU Cores SMT RAM L2 L3 Storage
odin Threadripper PRO 3975WX (Zen2) 32 OFF 512 GB DDR4 16 MB 128 MB 2x Samsung 1735 RAID-0 XFS
loki Ryzen 9 7900 (Zen4) 12 OFF 64 GB DDR5 12 MB 64 MB Corsair MP700 Pro PCIe 5.0
laptop T14s 2024, Ryzen AI 360 (Zen5) 8+SMT ON 64 GB DDR5 8 MB 24 MB Kioxia NVMe

13. Common Mistakes -- DO NOT DO THESE

  1. Do NOT re-analyze the codebase from scratch -- read this doc + design.md first.
  2. Do NOT optimize split_boundaries_parallel -- 3-14ms, negligible.
  3. Do NOT suggest "pipelining the scan ahead" -- 3ms vs 1000ms decode.
  4. Do NOT touch the iBWT+RLE2 decode path -- it's 1.5x faster than C, it stays.
  5. Do NOT add allocations in the hot path -- the goal is FEWER allocs, not more.
  6. Do NOT confuse parallel.rs with chunk.rs -- parallel.rs is for small files only.
  7. Do NOT forget pbzip2 streams -- FINAL_MAGIC every ~120 KB in planet files.
  8. Do NOT claim disk is the bottleneck -- NVMe reads are 50-90ms, decode is 1000ms.
  9. Do NOT suggest increasing buffer sizes as optimization -- the ring design is correct.
  10. Do NOT reintroduce rayon barriers -- the worker pool pipeline eliminated collect() barriers.
  11. Do NOT reduce RING_SLOTS below 6 -- workers need slots to flow into without waiting.

14. Znippy Ecosystem

Used by osm-katana: bz2 -> VTD XML -> PBF. Library API (ChunkDecoder) designed for zero-copy: caller owns buffers, decoder borrows. End-to-end planet: ~76 minutes estimated with worker pool, 309 MB/s decompressed.