Skip to content

bench: native helper spike — C/WASM vs Python fast-path primitives (#48) - #84

Merged
valiantone merged 1 commit into
mainfrom
spike/native-helper-48
Aug 28, 2026
Merged

bench: native helper spike — C/WASM vs Python fast-path primitives (#48)#84
valiantone merged 1 commit into
mainfrom
spike/native-helper-48

Conversation

@alphakenz

Copy link
Copy Markdown
Contributor

What

Benchmark spike comparing C and WebAssembly native-helper candidates against the real Python hot paths for HotMem's file-native primitives, with a data-backed recommendation: no native helper yet — pure-Python fixes deliver more.

Closes #48 (spike deliverables: comparison, recommendation, packaging analysis).

Why

Issue #48 asks which native helper surface (Rust/C/WASM), if any, is worth implementing first, gated on ≥3x speedup on the 100MB+ checksum path + clean packaging + graceful fallback. The spike measures real HotMem paths — provenance.verify_range (the verified-hydration double-read), JSONLInspector._stream/inspect(), bundle.parse_bundle, and whole-file manifest checksums — rather than synthetic throughput.

Changes

All new files under bench/native_spike/ (17 files, ~7k lines; zero production code changes):

  • Harness: gen_corpus.py (deterministic 384MB corpus, seed 48 — swap-shaped JSONL with a manifest-committed malformed line, bundle trees, binary files to 100MB), bench_worker.py (subprocess-isolated cells), run_bench.py (orchestrator with hard parity assertions + markdown report), benchlib.py (graceful-unavailable loaders, /proc-based RSS, fadvise cold cache, memory guards)
  • Candidates: c_checksum/ (self-contained SHA-256; pread + mmap range-hash via ctypes), c_jsonl/ (line scanner + strict RFC 8259 validator), wasm_parser/scanner.wat (wasmtime scanner, host-orchestrated I/O)
  • Results: results.json (committed full run, 5 runs/arm, warm+cold), README.md (methodology, tables, packaging matrix, recommendation, follow-ups), manifest.json (corpus fingerprints for reproducibility)
  • .gitignore: corpus + build artifacts

Rust was scoped to packaging analysis only (no toolchain on the bench box; C + WASM cover both candidate primitives).

Results (100MB arms, median warm)

Path Best native Best Python Verdict
Range checksum C mmap: 1625ms (2.2x slower) py streaming: 673ms, 20MB RSS vs current 850ms/219MB Native fails the ≥3x gate — hashlib is already OpenSSL C
JSONL scan C scan+valid: 490ms (3.55x) py scan-only: 319ms Clears 3x numerically, but ctypes packaging is fragile and WASM shows no gain (1.04x)
Bundle parse embed_text = 37–78% of parse time; algorithmic fix, not native

All parity checks pass (digests, row counts, first-bad-line, sample boundaries, manifest sets), and the optional-boundary check proves graceful degradation with HOTMEM_SPIKE_DISABLE_NATIVE=1.

Bonus finding: parity testing exposed a real bug — _stream overstates line offsets by len(carry) when lines span read-chunk boundaries (src/hotmem/inspectors/jsonl_inspector.py:129), demonstrated on corpus data (+330 bytes on the malformed-line offset). Documented with the one-line fix; out of spike scope.

Recommended follow-ups (pure Python, zero new deps)

  1. Single-read verify in the hydrate path (+16% on verified hydration)
  2. Streaming range hash for large ranges (1.26x, 10x less peak RSS)
  3. Fix the _stream offset bug (correctness)
  4. JSONL validation policy (per-line json.loads is 5.5x of scan cost)
  5. embed_text batching (bundle-load bottleneck)

Verification

  • ruff check bench/ clean
  • Full test suite untouched: 389 passed, 6 skipped
  • src/, tests/, pyproject.toml unmodified; wasmtime is a bench-only venv dep
  • Reproduce: cd bench/native_spike && python gen_corpus.py --profile reduced && python run_bench.py

…ion #48

Benchmark C (self-contained SHA-256 checksum, JSONL scanner) and WASM
(wasmtime line scanner) candidates against the real Python hot paths
(verify_range double-read, JSONLInspector._stream, parse_bundle) on a
deterministic 384MB corpus. All parity checks pass; recommendation:
no native helper yet — pure-Python fixes deliver more (single-read
verify, streaming hash, validation policy, embed batching). Also
documents the _stream chunk-boundary line-offset bug discovered by
parity testing. Bench-only; no production code changes.
@valiantone

Copy link
Copy Markdown
Contributor

Local review + follow-up tickets

Reviewed pre-merge: additive-only (bench/native_spike + .gitignore), all parity checks in results.json pass, optional-boundary check ok, ruff check clean, C/WAT semantics match the documented contract. The _stream offset bug claim was verified against src/hotmem/inspectors/jsonl_inspector.py:129 — it is real.

Follow-ups filed for the README recommendations and review findings:

Ticket Finding Source
#86 _stream line-offset overstatement (chunk-spanning lines) spike parity run
#87 Single-read verified hydration (drop redundant re-read) README follow-up 1
#88 Streaming range hash for large ranges (219 MB → 20 MB peak RSS) README follow-up 2
#89 JSONL validation policy (per-line json.loads = 5.5x at 100 MB) README follow-up 4
#90 Batch embed_text trigram hashing (37–78% of parse_bundle) README follow-up 5
#91 Bench nits: gen_corpus.py +=38 vs 41 bytes, ruff format, report rows column local review

All on milestone M5: Performance + Inspectors. Verdict: safe to merge.

@valiantone
valiantone merged commit bd0760d into main Aug 28, 2026
8 checks passed
@valiantone
valiantone deleted the spike/native-helper-48 branch August 28, 2026 10:00
@valiantone valiantone added v0.2.x HotMem post-v0.2 follow-up work and removed v0.3.0 labels Aug 28, 2026
valiantone added a commit that referenced this pull request Aug 28, 2026
_stream computed line_start = offset + pos in (carry+chunk) coordinates,
overstating file offsets by the carry length for every line after a
chunk-spanning line. Affected unsupported_reason offsets and byte_ranges
(feeding FileInspection -> API/MCP/CLI). Offsets now use base + pos with
base = offset - len(carry), verified for exact parity against the spike's
C scanner (bench/native_spike). Adds a chunk-boundary regression test;
discovered by the native helper spike (#48, #84).
valiantone added a commit that referenced this pull request Aug 28, 2026
* feat: optional derived vector index (#49)

- VectorIndex protocol + NullVectorIndex default (zero vector deps)
- Optional ChromaVectorIndex backend (lazy import, hotmem[vector] extra)
- Index supplies oversampled cosine candidates; SQLite re-scores with the
  identical hybrid formula, preserving the /v1/search ranking contract
- Rebuild reads SQLite only (embeddings reused, never recomputed, no file
  I/O); rebuild marker records the store fingerprint for staleness
- Admin endpoints: POST /v1/vector-index/rebuild (emits index.rebuilt),
  GET /v1/vector-index/status, DELETE /v1/vector-index
- hotmem serve --vector-index {none,chroma}; CI vector job; docs + changelog

* fix: snapshot vector-index rebuild fingerprint before reading rows (#49)

The rebuild marker now records the store fingerprint taken BEFORE the
all_rows() read. A mutation landing mid-rebuild (concurrent insert) thus
changes the live fingerprint and the marker reads stale, so search takes
the deterministic fallback until the next rebuild. Snapshotting after the
read could bless an index that silently misses rows inserted during the
rebuild. Adds a regression test simulating the mid-rebuild insert.

* release: v0.2.4 — optional derived vector index (#49)

* chore: refresh uv.lock for v0.2.4 version bump

* fix: correct JSONL _stream line offsets for chunk-spanning lines (#86)

_stream computed line_start = offset + pos in (carry+chunk) coordinates,
overstating file offsets by the carry length for every line after a
chunk-spanning line. Affected unsupported_reason offsets and byte_ranges
(feeding FileInspection -> API/MCP/CLI). Offsets now use base + pos with
base = offset - len(carry), verified for exact parity against the spike's
C scanner (bench/native_spike). Adds a chunk-boundary regression test;
discovered by the native helper spike (#48, #84).

* chore(bench): spike review nits — corpus byte accounting, report rows, formatting (#91)

- gen_corpus.py: malformed-line accounting used a hardcoded 38 instead of
  the actual 41 bytes (40 + newline); counter now derives from the line.
  The insertion threshold is unaffected (drift began after insertion), so
  the committed manifest stays valid for regenerated corpora.
- run_bench.py: report rows column now takes the first arm with a
  measured row count instead of blindly the first arm; reformatted.

---------

Co-authored-by: valiantone <zrjohn@yahoo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:benchmarks Eval harness, LOCOMO benchmarks, provider comparisons v0.2.x HotMem post-v0.2 follow-up work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native helper spike: Rust, C, or WebAssembly fast-path primitives

2 participants