v0.16.0
EC1 thr recovery after Parallel-era STW / scrub / counter fallout. Supported
path remains EC parallelism 1, GCRY_TLAB off (Parallel+TLAB stays
experimental — FINDINGS only, not folded into PERF).
Performance
- Linux Kemal (same-host median-of-3,
wrk -c 100 -d 30, scrub on):/json
~87% of Boehm @ ~0.80× post-GC RSS;/~82% @ ~0.79×. Session
bench/log/linux/2026-08-01-093130/(cb4d7f2; idle/fromslash-recut/).
Fair Boehm ~40k baseline. See docs/PERF.md (Linux). - EC1 thr levers (Boehm ~40k fair): restore v0.15 parked-fiber scrub on EC1
(4 KiB blind clear; Parallel keeps 512 B +clear_range_safe). Tip with
512 B + safe retained ~4× morelive_objectsthan bebedae. EC1 alloc/free
counters use plain get/set (heap_counters_atomiconly when
EC_PARALLELISM>1) — avoid LOCK XADD/CAS on the hot path. - EC1 sweep pause: STW
live_objects/free_bytesupdates no longer
CAS-loop per dead object. Empty dormant/munmap freelist cleanup batches
into onerebuild_size_class_freelistper size class. Dormant post-STW
flush early-outs whendormant_chunk_bytes == 0. - EC>1 thr gap (experimental): auto-collect trylock-or-skip on
@post_stw(no waiter pile-up; wait_total ~11s/20s → ~0). Default major
threshold 64 MiB whenEC_PARALLELISM>1(GCRY_THRESHOLDstill wins;
EC1 stays 32 MiB). Same-host re-cut: gcry EC4/json~68% of Boehm EC4
@ ~53k abs (was ~52% @ ~36k). Long soak 100/100 soft=0 hard=0
(2026-07-31-ec4-soak-100-post-thr). NoPERF.mdfold-in. See FINDINGS. - Parallel empty-chunk reclaim opt-in: default stays off under EC>1 (thr).
GCRY_PARALLEL_DORMANT=1DONTNEEDs empty chunks (RSS ~3× better, thr ~25%
down on Kemal EC4).GCRY_PARALLEL_RELEASE=1adds munmap excess (hung in
A/B). EC1 dormant+munmap unchanged. See FINDINGS RSS A/B. - EC>1 alloc-path A/B:
GCRY_TLAB=1@ EC4 still ~½ of TLAB-off thr (soft 0
— keep opt-in).@alloc_lockaspthread_mutexdeadlocks under STW
(collections=0) — rejected; stay onCrystal::SpinLock. Fold
note_alloc_bytesinto the freelist lock (one acquire per small alloc /
TLAB hit). Session2026-07-31-ec4-alloc-thr-ab. NoPERF.mdfold-in.
See FINDINGS. - Atomic alloc counters:
bytes_since_gc/live_objects/free_bytes/
etc. areAtomicso TLAB hits need no@alloc_lockfor accounting. EC4
TLAB-off thr unchanged (~51k); TLAB-on still ~52% of off. Session
2026-07-31-ec4-atomic-counters. NoPERF.mdfold-in. See FINDINGS. - Per-size-class freelist SpinLocks: TLAB-off small alloc/free lock only
that size class (not global@alloc_lock). Large + TLAB table/refill keep
@alloc_lock(per-class refill hurt TLAB-on via@index_lock×find_block).
Quiet EC4/json~55k (was ~51k). Session
2026-07-31-ec4-sizeclass-locks. NoPERF.mdfold-in. See FINDINGS.
Fixed
-
EC1 STW stack scan thr regression (Parallel fallout): process-STW full
fiber/pthread scans added for EC>1 mid-swap were also applied on EC1
(main+SYSMON). Every Thread root fiber is named"main", so SYSMON hit a
full pthread map scan (phase_stacks0.02→3ms; Kemal/json86%→80%
Boehm). Restore cheap SP/stack_topother-thread scans when
!multi_mutator_threads?; keep aggressive Parallel path. Limit
foreign-SP scrub skip to Parallel only. Sessions2026-07-31-164302
(regress),2026-07-31-173530(fix); final cut above. -
Parallel
@suppress_collectrace: plainInt+=/-=under concurrent
realloclost decrements so suppress stuck high (≈4607) and auto-collect
never ran (collections=0, thr collapsed). UseAtomic(Int32). Exposed when
alloc counters left@alloc_lock(shorter critical section). See FINDINGS. -
chunk_containinglock during post-STW: skipped@index_lockwhenever
@collecting(not only@world_stopped). Flush keeps@collectingafter
start_world, so Parallel mutatorsindex_insertwhile peers realloc
unlocked → falseowns_user_pointer?(pointer is not a gcry allocationon
String::Builder). Lock skip only under true STW. Soft errors 0/60 after
empty-chunk gate (was 2–3/60). See FINDINGS. -
Parallel empty-chunk release off: under multi-mutator STW, skip empty-chunk
munmap even whenrelease_empty_chunksis on (EC1 unchanged). Residual
mark-miss × post-STW munmap surfaced as Kemal/jsonsoft
pointer is not a gcry allocation(22/40 → 3/40 with the gate; hard
deaths 0/40).GCRY_STW_STACK_LAGenv for LAG A/B (default 512 KiB). See
FINDINGS mark-miss triage. -
EC1
stw_sp_clampcounters: idle/stack_topother-thread scan now
incrementssp_clamp_fallbacks(missed after cheap-scan restore; aarch64 /
Darwin CIsamples/stw_sp_clampsaw hits=0 fallbacks=0). -
pattern_fuzzStride CI floor: raise Stride p99/max vs-baseline limit
20→80× after EC1 4 KiB parked-fiber scrub (quiet ~11×; GHA crystal-latest
hit ~45–57×). -
No live TLAB steal:
steal_from_other_tlabscould null another thread's freelist head while that thread was in lock-freetlab_alloc_small(TOCTOU dual-alloc). Removed cross-TLAB steal; idle freelists return via STWflush_all_tlabs.@tlab_stealsstays 0 (metric reserved for a future CAS steal). -
FREE-claim × minor: stack/thread FREE-claim cleared
FREEbefore the minor/old filter, so an old freelist node became USED-unmarked and scrub dropped it. Skip claim entirely for old nodes during minor (minor never munmaps old chunks); nursery nodes still claim+mark. -
Parallel worker STW stack scan:
scan_other_thread_stacksusedmax(stack_top, sp)for running fibers; stalestack_topabove hardware SP skipped live frames, so Parallel+TLAB in-flight mallocs were swept (pin saw FREE). Prefer suspend SP (+ x86_64 red zone), mark saved GP registers from the suspenducontext, and with TLAB scan the full fiber stack (SP/greg alone still flaked under Parallel>2). CI:stw_mt_property_test --tlab --nurserymixes minors. -
TLAB FREE-claim chain mark: stack/thread FREE-claim only marked the current freelist
user; TLAB batch tails reachable vianext_freestayed unmarked FREE, so empty-chunk release munmapped them andtlab_alloc_smallSEGVd inBlockHeader.free?(KemalGCRY_TLAB=1@ EC1). Claim now marks thenext_freechain (keep FREE on tails); abandon TLAB heads that failfind_block. -
Parallel mutator heap-index races (partial): under
EC_PARALLELISM>1,chunk_containing/ last-chunk cache racedindex_insert(falseowns_user_pointer?/ corruption). Added@index_lock;with_alloc_lockalways locks (was a no-op when TLAB off);ensure_tlabsboots under@alloc_lock. Process-STW other-thread fiber stacks always full-scan. KemalEC>1HTTP still fails — see FINDINGS. -
TLAB per-slot freelist locks: Parallel dual-alloc on lock-free TLAB heads (
ec_alloc_stressdouble-free /not a gcry allocation). Per-slotCrystal::SpinLock(StaticArray — no GC malloc under@alloc_lockat boot). STWflush_all_tlabsmust not take slot locks (suspended mutator may hold them). Refill always re-claims under the slot lock. KemalEC>1still open. -
STW running-fiber scan:
scan_all_fiber_rootsskippedfiber.running?, relying onthread.@current_fiber; under Parallel that TLS can be briefly nil so stacks were missed. Under process STW, scan running fiber stacks too; ifcurrent_fiberis nil, fall back to pthread stack bounds + greg. -
STW × ExecutionContext deadlock (
GCRY_STRESS): signal-suspendingSYSMONdeadlocks (fiberyieldwait, or lostSIG_RESUMEleavingsigsuspendforever). Fix: skip SIGPWR for the Monitor; cooperative STW via@world_stoppedbarriers inallocate/lock_read; busy-wait@suspendedfor other threads (noyield_current); holdThread.lockfor stop→start; harden resume handshake; forbid process collect onSYSMONso the Monitor cannot STW-suspend the mutator. -
TLAB@EC1 measured: correctness OK (Kemal 20/20 default + thr=32KiB; STW MT
--tlab)./jsonthr ~71–77% of TLAB-off on same host — keep opt-in (GCRY_TLAB=1), not an EC1 default. Hit-pathfind_blockdominates; stripping it SEGVs. See FINDINGS. -
EC>1 thr vs Boehm (measured): Kemal EC4 TLAB-off
/json~23% of Boehm EC4 and ~0.52× gcry EC1 (session2026-07-31-100844-ec-parallel-thr). Correctness quieter; Parallel still anti-scales — experimental. -
Multi-mutator STW stack LAG: full
guard→bottomon every parked fiber dominated EC4phase_roots(~100ms+/collect). Prefer suspend SP−red_zone when present; otherwise scan fromstack_top − 512KiB(not full guard). Same-host A/B/jsonmedian-of-5: LAG ~30k vs stw_full ~16k (~1.9×); EC4 soak 30×8s 0/30. Quiet re-cut vs Boehm: EC4/json~37% Boehm EC4 and ~0.87× gcry EC1 (was ~23% / ~0.52×).GCRY_TLAB=1@ EC4: soak 3/20, thr not above good TLAB-off — keep opt-in. See FINDINGS. -
EC4 post-STW queue: SpinLock wait on
@post_stwburned ~8–11s/20s of worker time under Parallel HTTP. Switch to embeddedpthread_mutex; auto-collect coalesce when a peer already cleared the debt; pause stats exclude queue wait. EC4/json~40k med (d=20) + soak 20/20 (was ~22k + crash outliers). Quietd=30re-cut vs Boehm: EC4/json~52% Boehm EC4 and ~1.17× gcry EC1 (was ~23% / ~0.52× pre-LAG). Long soak 96/100 (4× SEGV/MARK_MISS). See FINDINGS. -
Post-STW flush keeps
@collecting+@suppress_collect: clearing@collectingbefore flush allowed stress/auto re-entry while still holding@post_stw_lock(non-recursive SpinLock). Hold collecting through flush. -
realloc suppress-collect + Boehm-like thread stacks: growing
reallocsets@suppress_collectaround the fresh allocate so a mark miss cannot free-then-reuse the pinned buffer mid-copy (String::Builder/jsondouble-free).scan_other_thread_stacksalways scanscurrent_fiber's stack (noname=="main"early-out — every Thread main fiber is named"main"); also scans the pthread stack when suspend SP lies there. RegisterString::Builderlayout (@buffernoscan). -
type_id_gate stacks off by default: process GC gated all ambient roots; stack words pointing at Channel/Deque buffers (no Crystal type_id) were dropped, so
Log::AsyncDispatcher#write_logsSEGVd under frequent collect (GCRY_THRESHOLD=32KiBkilled even EC1 at boot). Gate now applies to static roots only;GCRY_TYPE_ID_GATE=1restores stack gating. KemalEC>1still has residual flakes. -
Post-STW flush × Parallel collect race:
@collectingcleared beforeflush_pending_empty_chunks, so another EC worker couldstop_worldmid-munmap while a peer swept (realloc(): invalid pointervia!is_heap_ptr→LibC.realloc). Serialize next collect behind@post_stw_lockheld through post-STW flush; refuse LibC.realloc for addresses still in the historic heap span. -
Parallel pthread stack always scanned: when SP sat on a pool fiber,
scan_other_thread_stacksskipped the OS thread stack, so scheduler/main frames left on the pthread mapping were unmarked (Kemal EC4 ~1–2/40 SEGV). Always scan pthread bounds (SP−red_zone clamp when SP is there; full mapping otherwise). -
STW scan stack that holds SP:
Scheduler#swapcontextsetscurrent_fiberbefore saving the previous SP. Mid-swap STW then scanned the next fiber / stalestack_topand missed live frames on the previous stack (SEGV @0x4). Also scan[SP−red_zone, bottom)of whichever fiber stack contains the suspend SP. -
Process-STW full fiber stack scan: under
@world_stopped, scan every fiber from guard→bottom (ignore parkedstack_top). Parallel EC4 still flaked with SP/current_fiber heuristics alone. -
Skip fiber scrub when SP still on stack: parked-fiber scrub used
stack_topwhile Parallel mid-swap left the OS thread SP on that stack — wiping live frames before mark. -
Historic heap span for realloc/free:
@heap_min/@heap_maxtighten after munmap, so a dangling gcry pointer fell outside the live span andGC.realloc/freecalled LibC (realloc(): invalid pointer). Keep a monotonic@heap_span_*for the LibC-fallback guard. -
Mutator stack scan from hardware SP:
scan_mutatorusedpointerof(local)(mid-frame), skipping the leaf/red-zone window on the collecting worker under Parallel. -
Freelist unlink cycle guard:
unlink_freelist_rangecould spin forever on a corruptednext_freecycle (Parallel EC4 long-GDB hang: DEFAULT-1 in sweep while peers stuck in STW). Bound the walk and break self-loops; install the partial freelist instead of hanging the stopped world. Skip precise Hash entry walk when@entriesis not a live heap pointer. -
Revert Hash
@entriesgrey-scan: marking@entriesviamark_candidatefalse-retained capacity-slot garbage (layout_spec) and collapsed Kemal/jsonthr (~36% of Boehm).@entries/@indicesstay noscan; Entry walk remains authoritative. -
-Dwithout_mtcompile: Parallel EC root pins (Thread.@execution_context/Fiber::ExecutionContext) are gated with the same Crystal flag condition sofork_reinitand Darwin/aarch64 sample builds compile. -
STW fiber full-scan only with multi-mutator: process-STW always full-scanning every parked fiber (Parallel mid-swap hardening) crushed CI Kemal
/jsonthr (78%→48% Boehm). Restorestack_topclamp when only main+Monitor threads exist; multi-mutator now uses SP /stack_top−512KiBLAG (see above) instead of blanketguard→bottom. -
CI pause-budget floor: major p99 floor 100→200 ms, major max floor 250→350 ms (GHA flakes
100.72,163.6/270). Stresshello_env/ sample steps wrapped intimeoutso a hang fails fast instead of a 6h cancel.