perf: restore #173 ArcSwap time_buckets — fix .177 cold P99 regression - #248
Conversation
ROOT CAUSE FIX for the P50 regression Justin identified. The time_buckets field was `Arc<Mutex<TimeBucketManager>>`. Every query acquired this Mutex at the start of execute_query_with_collector and held it for the ENTIRE function execution. This serialized all queries through a single lock. When one query was slow (100ms+ from lazy load or cache miss), all concurrent queries queued behind it. v1.0.85 didn't have this issue because time buckets were added later. The feature introduced a global serialization point that regressed query P50 from sub-2ms to 60-85ms on ~30% of queries (confirmed by pre_cache_us trace data from PR #172/v1.0.168). Fix: Replace `Mutex<TimeBucketManager>` with `ArcSwap<TimeBucketManager>`. - Query path (3 sites): `tb.load_full()` — lock-free Arc snapshot, no contention, queries run fully concurrent. - Flush/refresh path (~8 sites): load → clone → mutate → store. Clone is cheap (4 buckets, Arc'd bitmaps = refcount bumps only). Store is atomic, readers never see partial state. - TimeBucket + TimeBucketManager now derive Clone. Local validation: 32-worker 30s load test, 2286 queries, no crashes, query_duration avg = 31µs (same as pre-fix baseline). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LGTM — Code review (Scarlet via team channel)Diff is textbook ArcSwap conversion pattern:
Net +40 / -31, single file ( Validation (Donovan local rig, 400 prod-shape cold queries, conc=32, prod MALLOC_CONF, fresh boot)
80% of cold queries finish in <1ms engine time. Real cold work (~136ms) was hidden behind 290ms Mutex serialization. Tail residue ~1% in ContextPR #173 (commit 25a91c8) authored by @JustMaier was on an orphan branch that never merged. Both Original commit message says it directly:
Investigation today: shadow flip on v1.0.177-jemalloc OOM-killed pod in 30s (RSS 11.7→110 GB). Root cause = exactly this serialization: cold queries pile up holding working set × jemalloc dirty_decay_ms:0 retention = ratchet to OOM. Fix at the source. Ready to merge. Tom can build v1.0.178-jemalloc from main HEAD post-merge. |
CI status: pre-existing rot on main, not introduced by this PRAll five hit the same Filed as follow-up: warning hygiene PR to clean the 88 stale-warning items + restore green CI. Tracked. Local cargo build (release, full features) passes cleanly. Validation curve in PR body holds. |
Summary
Cherry-picks
25a91c8(PR #173, original Justin authorship preserved) onto current main. PR #173 was developed on a parallel branch line that never merged into main; the .177-jemalloc release line forked off v1.0.150 before #173 landed and built parallel work (#220, #224, #226, #233, #234, #237, #238, #244, #246) without ever picking it up.Result: the cold-path Mutex serialization fix the 2026-04-10 PR #173 commit message describes is missing from the deployed code path.
Why this matters (validated 2026-04-29 cold-burst rig)
Local v1.0.177-jemalloc reproduces the exact bug PR #173 originally fixed:
Diagnostic instrumentation on a v1.0.177 build confirmed:
The Mutex wait IS the entire cold P99 budget on this branch line. At conc=32 cold burst, all 32 queries serialize behind one lock — each waits behind ~10-25ms of prior queries' real engine work, summing to 200-300ms
tb_lock_us.Validation curve (400 prod-shape cold queries, conc=32, prod MALLOC_CONF)
le=50usbucket countle=100usbucket countle=1msbucket countfilter_usandsort_ustotals roughly tripled in absolute time post-fix — that's because they're now actually parallel (CPU contention, not Mutex contention), so the engine is doing more work per second.Tail residue (~1% in
le=5s)4 of 405 queries land in
le=5s. These are first-touch cold lazy-loads (e.g. modelVersionIds shards on never-queried values). Real engine work, not serialization. Acceptable under shadow flip-on burst with #61 (jemalloc decay tuning) handling RSS retention.Ship sequence
This unblocks v1.0.178 cold-path safety:
dirty_decay_ms:0→5000/muzzy_decay_ms:0→10000. Already in flight.Combined: prod shadow flip-on no longer causes Mutex queueing × cap=32 saturation × jemalloc retention → no RSS ratchet to 110 GB.
Test plan
concurrent_engine.rs)cargo check --features server,pg-sync --bin bitdex-serverpassesle=50usbucket 9× more queriesRefs
25a91c8, 2026-04-10, Justin Maier)🤖 Generated with Claude Code