Skip to the document
Madhuopen lab

HanuDB, tuned while I slept · part 2 of 6

adopted14 September 2026cost: local CPU; a free-tier VM for the real runs

Reads: filters, sort and full-text on one core

Caches that are safe because segments never change

The question

Of 39 attempts in this theme, which ones survived two benchmark runs, the correctness suite and the gates?

adopted

On one core, every background thread competes with request handling: moving work off the write lock onto a spare thread (0003) made throughput worse. Blocking writers briefly is free; extra CPU is not.

Attempts39
Kept21
Discarded or crashed13
Best kept score37,280.7

Every run in this theme

Every run in this theme
#scorewrites/sreads/sverdictwhat changed
00029,237.99,6508,843keepindexer thread owns IndexWriter (channel, bounded 10k); no RwLock or commit task; +2% (noise) but structural
000712,204.425,3425,877discardfjall block cache 64 -> 256 MB: no change in reads (5877 vs 5926/s), +180 MB RSS; the read cost is not cache misses
000813,496.825,2717,208keepindex paths namespaced by collection (k.{col}.path, t.{col}._all); no col MUST term (was 70% of read CPU in the posting intersection); id term = col+id (cross-collection…
001013,527.925,3867,209discardcommit interval 500 -> 2000 ms: identical (reads 7209 vs 7208/s); segment count is not what limits reads; keep 500 ms visibility
001112,026.724,7825,837discardid as STRING|FAST column instead of doc store: reads -19% (5836 vs 7208/s); a 500k-term dictionary lookup per hit is slower than a doc-store block read
001613,851.928,9216,634keeptext field holds only the joined _all string per collection (no per-field text copy): writes +16% (28.9k/s), rebuild 579k docs in 10.1 s at 280 MB peak (was 498k in 12.2…
001714,016.230,0726,533discardHANUDB_THREADS=1 (single tokio worker): +1.2% (noise); real e2-micro has 2 vCPUs so the default stays
001816,113.129,1198,916keepstring equality / $in filters evaluated as fast-column predicates in the collector when a text clause drives the query (no posting intersection): reads 6634 -> 8916/s…
001920,354.528,93214,320keepequality filters as per-segment cached bitsets (built once from postings, 32 MB budget) applied in a Query/Weight wrapper so tantivy block-max pruning still works; most…
002122,129.829,34916,687keepsort by numeric/bool fast column through a per-segment cached Column<u64> (custom SortKeyComputer; strings keep tantivy's erased path): reads 14320 -> 16687/s (+17%),…
002222,912.829,12318,027keepfjall docs keyspace: data_block_hash_ratio_policy(0.75) (hash index inside data blocks for point reads): reads 16687 -> 18027/s (+8%), score +3.5%
002322,389.429,47917,005discardfjall data block compression disabled: reads -6% (17005 vs 18027/s); larger blocks cost more than the LZ4 decode
002423,365.829,41718,559keepper-segment cached StoreReader with a 4096-block cache for hit ids (tantivy default 100 blocks thrashed on big segments): reads +3%, score +2.0% (low-noise harness, kept)
002526,424.928,94124,127keepsingle-clause booleans unwrapped so text searches run as TermQuery / term union and get tantivy block-max WAND pruning (filter applied in the pruning callback): reads…
002727,031.528,29825,822keepcache maps behind RwLock (was a Mutex per segment per hit: 9% of read CPU in mutex waits natively) and one doc-store lookup per distinct segment per query: reads +7%,…
002825,978.222,66329,778discardJSON docs stored in the tantivy doc store so find hits skip fjall: reads +15% but writes -20% (doc store + merges carry the JSON) and peak RSS 368 -> 507 MB; calibration…
003327,00026,54126,890keeptokio workers = available cores (1 in the container; was max(2)): score neutral (26715 / 27632 / 26654 vs 2-worker 27051 / 26790; the first env run's 30423 was an…
003530,456.227,32333,949keepquick_cache LRU of hot documents (50k entries, invalidated on put/delete): reads 26890 -> 33949/s (+26%), score +12.8%; confirmed by a second run at 31372 (cal 647)
003622,95731,039crashdoc cache 150k entries: RSS gate failed (716 MB). Cause: cached Bytes come from Bytes::from_owner(fjall slice) and pin the whole 4 KB block each; 0035 had the same…
003732,320.628,22537,011keepdoc cache stores a compact copy instead of a Bytes pinning fjall's 4 KB block: peak RSS 309 -> 220-261 MB; runs 27496 (low outlier) and 32320 (best ever); mean 29.9k vs…
003835,169.827,37645,182keepdoc cache 150k compact entries (default): runs 35170 / 31069, mean 33.1k vs baseline mean 29.9k (+11%); RSS 310-337 MB
004735,009.227,40644,722keepsnapshot staging (index commit + hard links) moved after the segment upload, outside the WAL lock (snapshot may hold a little beyond its position; replay is idempotent):…
004837,280.729,60946,941keepmerge policy knob (HANUDB_MERGE default|lazy|none) + index_segments in status. default 37281 (best single run; reads 46.9k), lazy 27259 (reads 24.1k), none 27626 (reads…
005030,589.830,39230,789discardcommit interval 500 -> 2000 ms: runs 30590 / 31579 (mean 31.1k vs ~35k); writes best ever (30.4k, p99 4.9) but reads 31-33k (segments less merged when reads start)
005233,913.528,04341,013discardeager LogMergePolicy (min_num_segments 4): runs 33914 / 34042 (mean 34.0k vs ~35k); very consistent reads (41.0k / 40.9k) but no gain
005537,011.429,34246,686discardfjall block cache 64 -> 32 MB: runs 37011 / 30145 (mean 33.6k vs 37.0k; second run hit the low-read merge state); only -30 MB
005833,954.829,39939,217discardidle merge kick (merge small segments 1 s after writes go quiet): runs 33955 / 34349 (mean 34.2k vs 37.0k), read p99 1.0 -> 3.8 ms; the merge competes with reads on the…
006436,495.128,73646,350keepcompact retries when a policy merge holds segments (the suite caught a flaky 400); scripts/smoke.sh passes 3/3. Off the hot path
006636,367.628,13847,004keepMETRIC: disk_read_mb / disk_write_mb in status (/proc/self/io) + an io log line every 10 s; bench.sh prints the last value before the crash.
0071c2,0734,868infoREAL e2-micro, 4 clients (what an app sees): write p50 1.7 ms / p99 4.8 ms, get p50 0.30 ms, find 0.43 ms, search 0.41 ms, read p99 1.2 ms
007629,094.926,61031,812keepSAFETY: LogMergePolicy max_docs_before_merge 2M (segments beyond that are never merged: bounded merge memory on a 1 GB box). Bench unchanged (segments never reach 2M in…
0087a2,7037,858infoVM check after rolling 0087 out (e2-micro, twin mode, 10 s + 10 s, client on the VM): 2703 writes/s (p50 6.3 / p99 71 ms), 7858 reads/s (p50 2.6 / p99 13 ms), rss 193…
009036,886.633,38740,933discardMEMORY/READS: fjall block cache 128 MB (was 64). Three runs: reads 45.9k / 42.7k / 34.2k (mean 40.9k vs the baseline's 40.1k), writes unchanged, peak rss +85 MB (519).…
009337,717.333,96242,041discardDISK: LZ4 on every kv level (was: two youngest levels uncompressed). Disk writes 2.06 -> 1.76 MB per 1k docs (-15%), kv on disk 352 -> 151 MB, read-phase disk reads 250…
009635,29735,14335,453keepPROFILE-DRIVEN: perf on the write phase: tantivy indexing is the CPU (postings writer 9%, tokenizer 2.6%, fst 4%, TfAndPositionRecorder 2%), zstd 3%, kernel 18%; read…
010137,426.735,04940,130info--merge eager re-test on the lsm_tree store: runs 35192 / 39661 - but 'eager' has no match arm (falls through to the default LogMergePolicy), so this was two more…
010235,833.630,98741,470keepMERGES: a real --merge eager (LogMergePolicy min_num_segments 4, min_layer_size 10k, 2M cap): reads 40.8k / 42.1k (mid-band, no better than default's 45k runs) while…
0103infoVM END-TO-END: scripts/remote-check.sh (API-only: bucket-durable batch insert, idempotency replay, get, search+filter with read-your-writes, $in + sort, stats aggs,…
010522,659infoFOLLOWER UNDER LOAD (scripts/soak-follower.sh, 3 min mixed workload 60/30/10, leader on core 1, follower on core 2 tailing the same bucket with poll 200 ms): leader…
hanudbreadsautonomous loop