Repository navigation
Threads: nursery/generational GC and context lifecycle performance #16
Description
Activity
Landed a Context teardown/lifecycle reduction across the collector boundary:
zig-gc921696b perf(heap): support slab-owned bulk teardownzig-js37d1881 perf(context): skip per-cell frees at teardown
What changed
- Added
Heap.deinitRetainingCellStorage()for embedders that finalize every cell and reclaim the backing slab/arena wholesale immediately afterward. - The normal
Heap.deinit()contract is unchanged and still individually frees cell storage. Context.destroy()enters the existing single-ownerGcCellBackingteardown mode, runs all finalizers and collector side-buffer cleanup, skips per-cell cell-storage frees, then releases whole slabs.- Bucket-shaped object/collector side allocations are still freed normally while finalizers run.
- Added an explicit fallback to ordinary heap deinit if a GC heap ever exists without
gc_cell_backing. - Added exact
gc-profileattribution:skipfreemust equal finalized cells.
The profile witness reported:
- empty explicit-GC context: 1,044 finalized cells / 1,044 skipped cell frees;
- empty no-GIL context: 1,079 / 1,079;
- object-heavy explicit-GC destroy: 11,044 / 11,044;
- object-heavy no-GIL destroy: 11,079 / 11,079.
Wall-clock lifecycle rows remain noisy across local runs, so this update does not overstate a timing percentage; the eliminated dispatch count is exact and regression-tested.
Verification
zig-gc:zig build testzig-gc:zig build test -Dtsan=truezig-js: focused bulk-destroy test and focused TSan witnesszig-js:zig build test -Dtest-filter=enable_gczig-js:zig build test -Dtsan=true -Dtest-filter=enable_gczig-js:zig build test -Dtsan=true -Dtest-filter=parallel_jszig-js:zig build testzig-js:zig build threads-test— 227/227 files passed on the clean rerunzig-js: waiter-storm regression case passed twice in isolation and in the clean full corpuszig-js:zig build threadfuzz -Dfuzz-iters=20zig-js:zig build threadfuzz -Dfuzz-verify=true -Dfuzz-iters=300zig-js:zig build gc-profilezig-js:bun run docs:build
The nursery/remembered-set/promotion work and broader Context lifecycle milestone remain open.
Landed the first complete nursery/generational milestone:
zig-gc2d88e78 feat(heap): add one-cycle nursery collectionzig-gc9575198 fix(heap): rescan binding-selected old cells in minor GCzig-gc2b10d10 feat(heap): expose old-space collection pressurezig-jse19645d feat(gc): add quiescent nursery collection
What is complete
- Non-moving one-cycle nursery: dead young cells are reclaimed and every survivor is tenured in one minor cycle.
- Owner-aware remembered old containers plus a conservative child-only compatibility path.
- Separate weak-container barriers, exact minor-cycle WeakRef/WeakMap/WeakSet/FinalizationRegistry handling, and ephemeron fixed-point processing.
- Binding-selected rescanning for mutable type-erased old side cells, keeping Object/Environment tracing on the narrower remembered-owner path.
- Remembered-set OOM fallback to full GC.
- Old-space pressure separated from total heap pressure, so large young batches receive a minor cycle before forcing a full trace.
- Demand-driven quiescent policy at script/module boundaries. Explicit
collectGarbage()remains full-heap; parallel mid-script GC remains the existing abort-safe full collector. - Owner-aware barriers wired through properties/elements/accessors, environments, promises, Maps/Sets, prototypes, async-generator requests, weak collections, and finalization records.
gc-profilenow reports boundary pause, young input, reclaimed cells, promoted cells/bytes, and minor/full deltas.
The final profile witness (512 object graphs, 1/4 retained, full trigger isolated) reported:
mode pause young reclaimed promoted promoted bytes minor/full explicit GC 2.94 ms 1,543 1,286 257 156,256 1 / 0 threaded no-GIL 3.10 ms 1,543 1,284 259 157,144 1 / 0 These are local point measurements, not a general performance guarantee; the row is now repeatable for future tuning.
Locally verified
zig-gc:zig build testzig-gc:zig build test -Dtsan=truezig-js: focused nursery normal and TSan testszig-js: concurrent generator/iterator-helper regression testzig-js:zig build testzig-js:zig build test -Dtsan=true(full all-tests sweep)zig-js:zig build test -Dtsan=true -Dtest-filter=parallel_jszig-js:zig build threads-test— clean 227/227- waiter-table storm: three consecutive focused passes plus the clean full-corpus pass after making nursery collection threshold-driven
zig build threads-test -Dthreads-parallel-js=true -Dthreads-case=sync/condition-wait-notify.jszig build threadfuzz -Dfuzz-iters=20— 0 failureszig build threadfuzz -Dfuzz-verify=true -Dfuzz-iters=300— 0 failureszig build gc-profilebun run docs:build
The sharded no-GIL PR-249 TSan corpus, suppression witness, test262-parallel slice, and longer/nightly fuzz sweeps remain CI-backed gates; this update does not claim they were newly rerun locally.
Remaining in #16
- Tune adaptive nursery sizing and pause/throughput behavior.
- Measure before adding multi-age, moving/copying, or parallel minor collection.
- Continue Context create/destroy reductions and publish reuse/pooling guidance.
Nursery follow-up landed in
b9dd15d fix(gc): remember native synchronization edges.A focused Debug
condition asyncWaitprofile exposed a young Promise reclaimed while reachable only from an old Condition wrapper's native queue. Lock, Condition, and ThreadLocal side records now retain their wrapper as the owner for remembered-set barriers; queued lock jobs, async condition waiters/lock edges, and ThreadLocal map values are covered.The new deterministic regression tenures the wrappers, stores young values only in native records, forces a minor collection, and requires condition reacquire plus ThreadLocal reads to survive. It passes normally and under TSan, the focused profile passes in Debug and repeated ReleaseFast runs, the complete profiler is green, and the clean corpus remains 227/227.
- added a commit that references this issue
on Jul 10, 2026 Landed
134c203(docs(gc): document context pooling guidance (#16)).What changed:
- documented the supported context reuse pattern: bounded pool per isolation domain, one task at a time per context unless the host intentionally shares a realm, and quiescent
collectGarbage()at measured task boundaries; - documented when to destroy instead of reuse: realm/global/module reset requirements, unreleased host handles, untrusted code state pollution, or live Worker/Thread activity;
- tied the guidance to
gc-profilerather than claiming universal timings.
Local profile evidence from this run:
zig build gc-profile- explicit GC task lifecycle: recreate/evaluate/destroy
26,015,541 ns/task, reuse+GC3,897,863 ns/task,6.67xratio; - threaded no-GIL GC task lifecycle: recreate/evaluate/destroy
25,567,857 ns/task, reuse+GC3,817,355 ns/task,6.69xratio; - the reuse row is one warmup evaluation, then 40 tasks in one context with
collectGarbage()every 10 tasks.
Validation:
bun run docs:buildgit diff --check
Remaining #16 work stays focused on nursery sizing/pause tuning and future lifecycle reductions backed by the profile rows, not on pretending these local ratios are portable guarantees.
- documented the supported context reuse pattern: bounded pool per isolation domain, one task at a time per context unless the host intentionally shares a realm, and quiescent
Advanced the nursery tuning work in two small pushed chunks:
- zig-gc
4fafccc(tune nursery threshold decay) now records last-minor young/reclaimed/promoted byte totals and makes the next nursery trigger decay by at most half instead of snapping directly to the 64KiB floor after a mostly-garbage young batch. This keeps low-survival cycles from over-tightening the next workload immediately while still letting the threshold shrink when pressure stays low. - zig-js
38832d8(bench(gc): expose nursery byte pressure) prints those byte totals plus the computed next nursery threshold inzig build gc-profile, so future nursery sizing changes can be judged from pause + reclamation + threshold drift together.
Validation:
zig build testin~/Code/Libraries/zig-gczig build gc-profilein~/Code/Libraries/zig-jsbun run docs:buildgit diff --check
Current local nursery row from the profile after the change:
mode pause ns young young bytes reclaimed recl bytes promoted prom bytes next thresh minor full gc 3382542 1543 747928 1286 591672 257 156256 312512 1 0 threaded no-gil 3626333 1543 747928 1284 590784 259 157144 314288 1 0Leaving the broader "tune nursery sizing/pause/throughput" item open: this gives us the measurement and first less-twitchy policy, but we still need more workload shapes before deciding whether to add aging/copying/parallel minor machinery.
- zig-gc
Landed a zig-gc dependency fix in f33d48c and consumed it from zig-js main in 4bf4e35.
CI root cause: tsan-threadfuzz-lifecycle reported concurrent appends to zig-gc weak_slots during GC tracing. zig-gc now has a weak_lock around weak slot append, clear, and processing paths, matching the synchronization style already used for barrier and remembered-set handoff.
Validation included:
- zig build test in zig-gc
- zig build threadfuzz -Dtsan=true -Dfuzz-lifecycle=true -Dfuzz-iters=1 in zig-js
- THREADFUZZ_SEED_TIMEOUT_MS=300000 zig build threadfuzz -Dtsan=true -Dfuzz-midgc=true -Dfuzz-iters=1 in zig-js
This keeps the generational/weak-edge roadmap moving without changing public API.
Dependency hardening follow-up landed in
zig-utils/zig-gcmain asc7426a3(fix(gc): use tsan-visible weak slot lock).Root cause from the failed
tsan-threadfuzz-lifecyclelog on run https://github.com/zig-utils/zig-js/actions/runs/29113163306 was still weak-slotArrayList.appendracing in TSan. The previous fix serialized the path with a custom atomic spinlock, but TSan does not model that as a mutex for the protected list. The new chunk wraps weak-slot registration with a POSIX pthread mutex on pthread platforms, with the old atomic fallback elsewhere, and destroys it in heap teardown.Local validation:
zig build testin~/Code/Libraries/zig-gcpassed. The fresh zig-js push at5631d8dshould consume this dependency through the sibling checkout; CI is pending at https://github.com/zig-utils/zig-js/actions/runs/29114200905.Follow-up dependency fix landed in
zig-utils/zig-gcmain as74d7d8d(fix(gc): allocate weak slots from scratch arena).Root cause update from
tsan-threadfuzz-lifecycleon run https://github.com/zig-utils/zig-js/actions/runs/29114200905: after switching the weak-slot lock to a pthread-visible mutex, TSan still reported two different mutexes protecting writes to the same weak-slot ArrayList backing. The weak-slot list is collector scratch, but it was still allocating fromHeap.backing; in zig-js that is the reusable GC cell slab allocator, which is the wrong allocator for cross-thread collector scratch and made lifetime/reuse ambiguous to TSan.The weak-slot scratch list now allocates/deinits with
Heap.aux, same class as the mark/barrier scratch buffers. Local dependency validation:zig build testin~/Code/Libraries/zig-gcpassed.A fresh zig-js push at
caad91eshould consume this dependency and is queued here: https://github.com/zig-utils/zig-js/actions/runs/29114710319.Lifecycle/GC scratch follow-up landed in zig-gc as
f8205c7(fix(gc): serialize weak scratch globally).The failing
tsan-threadfuzz-lifecyclelog on zig-jsaa697dfstill showed weak-slot scratch appends racing under distinct per-heap mutexes during parallel multi-context create/destroy. Since weak-slot registration is collector scratch bookkeeping rather than per-heap policy, I moved it behind a single process-wide weak scratch lock. This keeps independent heap lifecycles TSan-visible even when their aux allocator reuses scratch pages across threads.Validation:
zig build testinzig-gc- downstream exact gate:
zig build threadfuzz -Dtsan=true -Dfuzz-lifecycle=true -Dfuzz-iters=2in zig-js passed with112 programs from seed 1, 0 failures
Downstream CI rerun for the paired zig-js fix: https://github.com/zig-utils/zig-js/actions/runs/29116066646
GC dependency update pushed:
zig-utils/zig-gc@a2b8cb2reserves weak-slot scratch up front for full, minor, and parallel concurrent marking.- This closes a sanitizer-observed allocator/scratch reuse race where
markWeakcould growweak_slotsduring concurrent marking while no-GIL mutators grew object backing storage. - Validation: TSan
threadfuzz midgc 1 1passed locally with 45/45 programs after the fix. This is correctness hardening first, but it also keeps the scratch allocation policy explicit for future nursery/concurrent-GC tuning.
Pushed a measured #16 allocation-churn tuning chunk in
zig-js@42fc9c9(perf(gc): retain bounded reusable cell slabs).What changed:
GcCellBacking.trimEmptyTailChunks()now keeps up to 8 fully empty tail chunks per live size-class bucket instead of trimming every empty tail slab after each full collection.- Larger one-off spikes are still returned: chunks beyond that bounded reusable tail are trimmed, and all chunks are still released at Context destroy.
- The backing allocator tests now cover the bounded-retention contract, inner empty chunks, retained free-list nodes, and excess-tail trimming.
Profile evidence (
zig build gc-profile):- Before this chunk, my initial local row showed threaded no-GIL churn reuse at
0.15%(fresh=335513,reused=535,chunks=3). A one-slab experiment only reached0.95%, which was too small to justify as the final policy. - With the 8-chunk cap, the same focused row reports threaded no-GIL churn reuse at
6.55%(fresh=314009,reused=22039,freed=336051,chunks=11,live=1076). - Plain GC in the same run reports
62.01%reuse (fresh=127638,reused=208410,chunks=11), so no-GIL reuse is still a future tuning target, but the pathological near-zero reuse under pooled-context churn is materially reduced with a bounded memory tradeoff.
Validation:
zig fmt src/context.zigzig build test -Dtest-filter=GC cell backing --summary nonezig build test -Dtest-filter=enable_gc: collectGarbage trims empty GC backing tail chunks --summary nonezig build gc-profilegit diff --check
Leaving #16 open: this is a measured tail-slab reuse improvement, not the end of nursery/context lifecycle tuning.
Landed
eba4172(bench(gc): compare nursery retention shapes).This keeps #16's nursery tuning work measurement-led:
zig build gc-profilenow compares four 512-object young-generation shapes instead of one fixed 1/4-retained row:mode shape pause ns reclaimed recl bytes promoted prom bytes next threshold gc ephemeral 6,998,000 1,542 747,320 1 608 131,072 gc 1/16 retained 6,342,750 1,478 708,408 65 39,520 131,072 gc 1/4 retained 5,276,292 1,286 591,672 257 156,256 312,512 gc all retained 4,353,917 518 124,728 1,025 623,200 1,246,400 threaded no-gil ephemeral 5,467,084 1,540 746,432 3 1,496 131,072 threaded no-gil 1/16 retained 6,378,542 1,476 707,520 67 40,408 131,072 threaded no-gil 1/4 retained 5,686,917 1,284 590,784 259 157,144 314,288 threaded no-gil all retained 5,026,917 516 123,840 1,027 624,088 1,248,176 Each row had
young=1543,young bytes=747928,minor=1,full=0. Docs now describe the shape matrix so future threshold/pause changes can be compared against retention, reclamation, and threshold drift together.Validation:
zig build gc-profilebun run docs:buildgit diff --check
Leaving #16 open: this is better instrumentation for tuning decisions, not the final nursery policy.
Pushed a measurement-focused #16 chunk in 570a046 ().
What changed:
- now prints byte survival and reclamation percentages for each quiescent nursery retention shape.
- The percentages are computed from the same young/reclaimed/promoted byte counters already captured by zig-gc, so the table now shows pause, byte volume, survival/reclamation rate, and next-threshold drift together.
- Updated testing and production-readiness docs so the profiler contract mentions the new survival/reclamation percentage columns.
Local profile evidence from this run:
Validation:
- �[?25l�[32m[Crosswind]�[0m CSS engine loaded
�[36m⠋�[0m Building documentation...
�[36m⠙�[0m Building documentation...
�[36m⠹�[0m Building documentation...
�[36m⠸�[0m Building documentation...
�[36m⠼�[0m Building documentation...
�[36m⠴�[0m Building documentation...
�[36m⠦�[0m Building documentation...
�[36m⠧�[0m Building documentation...
�[36m⠇�[0m Building documentation...
�[36m⠏�[0m Building documentation...
�[36m⠋�[0m Building documentation...
�[K�[?25h�[32m✓�[0m Built 21 pages to HTML in 951.1089590000001msLeaving #16 open: this improves the measurement surface for nursery tuning, but does not yet decide/implement a new sizing policy across more workload shapes.
6 remaining items
Landed a #16 nursery tuning slice across
zig-gcandzig-js.Commits:
zig-utils/zig-gc@1d7c031(gc: cap nursery threshold growth)zig-utils/zig-js@a92b6fd(docs(gc): record capped nursery growth)
What changed:
zig-gckeeps the existing gradual threshold decay after low-survival nursery cycles.- Upward nursery-threshold growth is now capped by the just-observed young batch size, so a high-survival fixed-size burst cannot jump the threshold above the next comparable batch and skip the next quiescent minor.
- Added a direct
zig-gcunit test pinning both capped growth and unchanged gradual decay. - Updated
zig-jsthread docs with the policy and the reason.
Focused profile evidence:
- Before this slice,
zig build gc-profile -Dgc-profile-case="nursery drift"showed the all-retained 512-object row raising the threshold to about1.28 MiB, causing alternate batches to skip minor collection (minor=0) and carry a dead young batch. - After
zig-gc@1d7c031, the same focused row keeps the all-retained threshold at the observed764344-byte batch size in bothgcandthreaded no-gilmodes, and each of the four repeated batches receives a minor collection. - Ephemeral,
1/16 retained, and1/4 retainedrows keep their previous threshold trajectories.
Validation:
- In
zig-gc:/Users/chris/.local/bin/zig build test --summary all(23/23tests passed) - In
zig-js:/Users/chris/.local/bin/zig build gc-profile -Dgc-profile-case="nursery drift" - In
zig-js:/Users/chris/.bun/bin/bun run docs:build git diff --checkin both repos
CI pending: https://github.com/zig-utils/zig-js/actions/runs/29194875744
Follow-up: push CI for the #16 capped nursery-growth slice completed green.
Run: https://github.com/zig-utils/zig-js/actions/runs/29194875744
Result:31success,1expected skip,0failures.This covers the
zig-js@a92b6fddocs/evidence commit while CI consumes the current local-path dependency checkout, includingzig-gc@1d7c031.Landed a small #16 pooled-context scratch-retention slice across the GC dependency and docs.
Commits:
zig-utils/zig-gc@8059de0(gc: trim oversized collector scratch)zig-utils/zig-js@045ff0e7(docs(gc): record scratch trim policy)
What changed:
zig-gcnow trims empty collector scratch buffers after a collection when their retained capacity is disproportionate to the current live-cell count.- Normal scratch reuse is preserved up to a floor of 4096 entries, and high-live contexts can retain up to roughly 2x current live-cell pressure.
- One-off spikes no longer force pooled contexts to carry arbitrarily large empty mark-stack / weak-slot / remembered-card / concurrent scratch buffers forever.
- Added a direct
zig-gcregression test for oversized post-spike scratch freeing and retained normal scratch. zig-jsdocs now record the dependency policy and test coverage surface.
Validation:
- In
zig-gc:zig build test --summary all(24/24passed) - In
zig-js:zig build gc-profile -Dgc-profile-case="nursery" --summary all - In
zig-js:bun run docs:build git diff --checkin both repos
CI for the downstream docs/evidence commit is pending: https://github.com/zig-utils/zig-js/actions/runs/29200182099
CI routing note: the
045ff0e7run for the scratch-trim docs/dependency-consumption slice was cancelled by the follow-upb8136312push. The live main-branch gate that includes045ff0e7and consumeszig-gc@8059de0is now: https://github.com/zig-utils/zig-js/actions/runs/29200325235CI routing note: the previous live gate for the scratch-trim/docs history was cancelled by the follow-up main push. The current main-branch gate that includes the zig-gc scratch-trim downstream docs commit and later history is now: https://github.com/zig-utils/zig-js/actions/runs/29200500880
CI routing note: the previous current gate was superseded by the follow-up #36 push. The current main-branch gate that includes the zig-gc scratch-trim downstream docs commit and later history is now: https://github.com/zig-utils/zig-js/actions/runs/29200654888
CI routing note: the previous current gate was superseded by the follow-up #36 iterator-helper push. The current main-branch gate that includes the zig-gc scratch-trim downstream docs commit and later history is now: https://github.com/zig-utils/zig-js/actions/runs/29200732250
CI routing note: the previous current gate was superseded by the follow-up #36 async-generator push. The current main-branch gate that includes the zig-gc scratch-trim downstream docs commit and later history is now: https://github.com/zig-utils/zig-js/actions/runs/29200819725
CI routing note: the previous current gate was superseded by the follow-up #36 generator-handler push. The current main-branch gate that includes the zig-gc scratch-trim downstream docs commit and later history is now: https://github.com/zig-utils/zig-js/actions/runs/29200885804
CI routing note: the previous current gate was superseded by the follow-up mid-GC telemetry push. The current main-branch gate that includes the zig-gc scratch-trim downstream docs commit and later history is now: https://github.com/zig-utils/zig-js/actions/runs/29201026809
Pushed
b79f9774(bench(gc): add array nursery shape) as a small #16 measurement-surface slice.What changed:
- Added an
array/object 1/4retained case to the existinggc-profilenursery case list. - Updated the nursery and nursery-drift table headings from
512 objectto512 allocationbatches. - Updated thread testing and production-readiness docs so the profile contract now says the nursery rows cover retention/allocation shapes, not only object graphs.
Focused local evidence:
zig build gc-profile -Dgc-profile-case="nursery" --summary allnow prints thearray/object 1/4row in bothgcandthreaded no-gilmodes.zig build gc-profile -Dgc-profile-case="nursery drift" --summary allincludes the same shape across the four-cycle threshold trajectory.- In this run, the array/object row has the same young/promoted-byte trajectory as the object
1/4 retainedrow, which is useful evidence before policy tuning: array/object mix currently differs mainly in wall-clock pause shape, not in the nursery byte-threshold inputs.
Other validation:
bun run docs:buildgit diff --check
This does not tune the nursery policy yet; it broadens the focused profile surface so future #16 policy decisions can compare object-only versus array/object workloads without running the full profile matrix.
CI: https://github.com/zig-utils/zig-js/actions/runs/29201561685 is pending for
b79f9774.Also updated the #11 issue body to match the live reference audit:
236/259promoted executable PR-249 files,23executable reference-only files, and5helper/preload files; #35 is now recorded as resolved rather than still tracking an open promotion blocker.- Added an
Routing note: the CI run linked for
b79f9774was cancelled by the newer #12 memory-model docs push. The #16 array/object nursery-profile row remains onmain; use current combined-head CI ford0092f5e: https://github.com/zig-utils/zig-js/actions/runs/29201642565 (queued at the time of this note).Green gate note for
d0092f5e: https://github.com/zig-utils/zig-js/actions/runs/29201642565The nursery profile rows, array/object allocation-shape evidence, threshold/scratch dependency sync with
zig-gc, and docs updates are covered by the successful combined gate. Next work should stay measurement-driven across more workload shapes.Closing this tracker as completed.\n\nThe implementation now has the intended GC-backed threaded baseline: size-class slabs, a non-moving one-cycle nursery, exact remembered-set/write-barrier coverage, full-heap fallback, weak/finalization safety, adaptive threshold telemetry, scratch trimming, focused profile rows, lifecycle/pooling guidance, and normal/TSan/concurrent-GC/fuzzer regression coverage. The issue history contains before/after profile evidence for the allocation, nursery-shape, threshold, scratch-lifetime, and context-lifecycle changes, and the docs distinguish slab allocation, nursery policy, full/concurrent collection, and embedder pooling.\n\nDesign decision for the remaining research line: current focused measurements do not show that multi-age, moving/copying, or parallel minor collection earns its correctness and complexity cost. The supported policy remains the non-moving one-cycle nursery plus measured quiescent full collection and bounded context pooling. That is a positive decision, not deferred implementation.\n\nCombined acceptance gate: https://github.com/zig-utils/zig-js/actions/runs/29232877388 — 31 success, 1 expected skip, 0 failures. Future profile evidence that identifies a concrete nursery or lifecycle bottleneck should open a narrow issue with before/after counters rather than keeping this design tracker permanently open.
Parent: #1
Goal
Make the GC-backed threaded runtime competitive for allocation-heavy and create/destroy-heavy embedders without weakening no-GIL correctness.
Current baseline
collectGarbage()remains full-heap; no-GIL mid-script collection remains the abort-safe full concurrent protocol.gc-profileseparates lifecycle, allocation, backing, churn, finalizer, and nursery attribution. Focused cases are available with-Dgc-profile-case='<exact table name>'so nursery and lifecycle rows can be checked without paying for the full profile matrix.zig-gcrecords last-minor young/reclaimed/promoted byte totals, uses gradual threshold decay after low-survival cycles, caps upward threshold growth to the observed young batch size, trims oversized empty collector scratch after post-spike collections, and serializes allocation metadata for single-mutator concurrent marking without opting that mode into the multi-mutator parallel-GC protocol.collectGarbage()at measured task boundaries.Milestones
gc-profile.Remaining work
Acceptance criteria