Skip to content

Add transaction log read-path benchmark (baseline capture)#546

Draft
harper-joseph wants to merge 2 commits into
mainfrom
benchmark-readpath-baseline
Draft

Add transaction log read-path benchmark (baseline capture)#546
harper-joseph wants to merge 2 commits into
mainfrom
benchmark-readpath-baseline

Conversation

@harper-joseph

Copy link
Copy Markdown

Summary

Draft PR — this exists solely to capture baseline numbers from the
self-hosted CI runner before a separate PR introduces read-path
optimizations. Once the baseline is on record, we can compare against
the optimized PR's CI run.

Adds benchmark/worker-transaction-log-read.bench.ts — a 4-pattern
read-path benchmark designed to expose mutex/atomic contention on the
transaction log read path under concurrent subscribers.

Benchmarks added

All four run at 8 workers:

  1. Bulk forward scan, no writes — each worker scans every entry from start. Per-entry iteration cost (mmap'd buffer + JS DataView).
  2. Bulk forward scan with 1 concurrent writer — 7 readers + 1 writer; tests whether write activity slows readers.
  3. High-frequency short-range queries — 8 workers × 1000 iterator-opens per tick at random positions. Most sensitive to per-query mutex contention.
  4. Cursor-advance tail scan with 1 concurrent writer — 1 writer + 7 subscribers tail-following. Mirrors CDC subscriber pattern.

LMDB options aligned with Harper's actual audit-store usage:

  • `noSync: true` (matches rocksdb-js's lazy-WAL `.txnlog` semantics)
  • `getRange({ snapshot: false })` (matches Harper's audit reads)
  • numeric timestamp-like keys (matches rocksdb-js's timestamp-based queries)

Why this is just the bench

A companion PR (read-path optimizations) makes substantial C++ changes to the transaction log read path. Before those changes are evaluated by CI, we need to know what the runner produces on baseline so we can quantify the impact. Splitting it this way means:

  • The optimization PR's diff stays focused on the optimization itself, not the bench setup
  • CI history retains a clean "this is what the read-path looked like before" data point
  • We can iterate on the bench (sizing, options, scenarios) without churning the optimization PR

Test plan

  • Type-check passes
  • Bench runs locally on macOS arm64 (~20s wall time, all 4 benches complete)
  • Bench completes on the self-hosted Linux benchmark runner
  • Numbers are captured and documented for use as the baseline reference

Bench expectations on baseline

From local runs on baseline (no opts, 8-core M-series Mac):

Access pattern rocksdb-js lmdb rocksdb-js vs lmdb
Bulk forward scan, no writes ~3,150 hz ~85 hz ~37× faster
Bulk forward scan w/ 1 writer ~2,250 hz ~93 hz ~24× faster
Short-range queries ~114 hz ~480 hz ~4× slower
Cursor-advance tail scan w/ writer ~1,160 hz ~860 hz ~1.35× faster

The short-range row is the interesting one — that's the workload where the planned read-path optimizations provide a 22× speedup, flipping rocksdb-js from a 4× regression to a 5× lead vs LMDB.

🤖 Generated with Claude Code

harper-joseph and others added 2 commits May 7, 2026 21:40
Adds a 4-pattern read-path benchmark that surfaces the contention
characteristics of the transaction log under concurrent subscribers:

  1. Bulk forward scan, no writes
  2. Bulk forward scan with 1 concurrent writer
  3. High-frequency short-range queries (1k iterators × 8 workers)
  4. Cursor-advance tail scan with 1 concurrent writer

Each pattern compares rocksdb-js against lmdb with matched lazy-durability
semantics (`noSync: true`) and Harper-realistic LMDB options
(`getRange({snapshot: false})`, numeric timestamp-like keys).

Runs as part of the standard `pnpm bench` flow. Total wall time ~20s
(4 benches × ~5s sample window) — fits comfortably in CI.

This PR is intentionally a draft so we can capture the baseline numbers
from the self-hosted CI runner before any optimization changes land.
A separate PR will introduce the read-path optimizations and we'll
compare CI numbers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

get-sync.bench.ts

getSync() > random keys - small key size (100 records)

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 lmdb 1 24.32K ops/sec 41.11 39.66 553.942 0.118 121,622
🥈 rocksdb 2 12.54K ops/sec 79.75 75.80 22,376.841 0.886 62,694

getSync() > sequential keys - small key size (100 records)

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 lmdb 1 28.48K ops/sec 35.11 34.01 562.251 0.103 142,415
🥈 rocksdb 2 12.85K ops/sec 77.85 73.74 573.345 0.050 64,230

ranges.bench.ts

getRange() > small range (100 records, 50 range)

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 lmdb 1 25.70K ops/sec 38.91 36.02 1,490.316 0.282 128,496
🥈 rocksdb 2 16.39K ops/sec 61.01 54.90 1,074.218 0.117 81,951

realistic-load.bench.ts

Realistic write load with workers > write variable records with transaction log

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 181.96 ops/sec 5,495.702 67.44 159,275.209 42.32 384
🥈 lmdb 2 26.05 ops/sec 38,392.438 458.124 1,217,292.392 137.264 64.00

transaction-log.bench.ts

Transaction log > read 100 iterators while write log with 100 byte records

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 34.33K ops/sec 29.13 14.22 365.865 0.205 171,637
🥈 lmdb 2 440.23 ops/sec 2,271.543 146.212 24,299.824 1.42 2,202

Transaction log > read one entry from random position from log with 1000 100 byte records

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 764.51K ops/sec 1.31 1.13 4,898.569 0.202 3,822,575
🥈 lmdb 2 483.42K ops/sec 2.07 1.15 792.251 0.279 2,417,079

worker-put-sync.bench.ts

putSync() > random keys - small key size (100 records, 10 workers)

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 845.88 ops/sec 1,182.197 1,025.605 1,873.434 0.359 1,692
🥈 lmdb 2 1.16 ops/sec 864,105.174 834,980.557 904,058.007 1.94 10.00

worker-transaction-log-read.bench.ts

Transaction log read access patterns (8 workers) > Bulk forward scan, no writes: 8 workers each scan ~8000 entries

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 942.49 ops/sec 1,061.014 938.477 2,700.608 0.347 1,885
🥈 lmdb 2 96.70 ops/sec 10,340.915 6,828.924 42,766.057 3.98 194

Transaction log read access patterns (8 workers) > Bulk forward scan with 1 concurrent writer (7 readers)

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 1.00K ops/sec 996.145 839.414 1,872.156 0.386 2,008
🥈 lmdb 2 104.79 ops/sec 9,542.446 6,668.962 12,425.766 1.88 210

Transaction log read access patterns (8 workers) > Short-range queries: 8 workers each open 1000 iterators per tick

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 lmdb 1 467.71 ops/sec 2,138.073 1,403.427 4,789.001 1.28 936
🥈 rocksdb 2 177.58 ops/sec 5,631.264 5,095.714 7,084.364 0.756 356

Transaction log read access patterns (8 workers) > Cursor-advance tail scan with 1 concurrent writer (7 readers, 50 writes/tick)

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 lmdb 1 739.97 ops/sec 1,351.411 1,072.376 17,256.556 2.70 1,480
🥈 rocksdb 2 526.62 ops/sec 1,898.891 1,584.017 3,848.027 0.675 1,054

worker-transaction-log.bench.ts

Transaction log with workers > write log with 100 byte records

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 18.70K ops/sec 53.49 29.74 498.28 0.524 37,392
🥈 lmdb 2 800.99 ops/sec 1,248.454 296.211 14,586.834 5.91 1,602

Results from commit 0bb68f8

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant