ReviewOS

zig-utils/zig-js

Perf: enable bounded mid-script GC for production no-GIL contexts

#116
Closed chris opened this 9 days ago ·
9 days ago

Parent: #97 Related: #62, #49, #44, #119

Delivered

Production no-GIL contexts now use a bounded cooperative young-generation rendezvous under sustained shared allocation:

  • 38f4daa6 — arm aggregate cell-byte accounting only after a second worker exists, elect one collector after a 1 GiB tranche, freeze peers at existing safepoints, and collect young only after every active peer acknowledges;
  • 8332a814 — release a published stop if the elected collector exits abruptly;
  • fff12f90 — distinguish cooperative peer parks from the older abort-safe collector's non-blocking root publications;
  • 0ada3601 — publish the clean 1,540-sample report, raw evidence, benchmark guide, and README scorecard.

The rendezvous waits at most 100 ms. A timeout performs no collection and leaves the existing safe fallback intact. Lone-worker allocation remains byte-identical because accounting is not armed until the second worker record is registered. The opt-in abort-safe concurrent collector remains available for its dedicated stress coverage.

Accepted evidence

Measured commit: fff12f905819fab376372fabddb46d6a95f02f97

Published by: 0ada3601

Report: https://github.com/zig-utils/zig-js/blob/main/docs/.data/benchmark-comparison-2026-07-15.md

Raw evidence: https://github.com/zig-utils/zig-js/blob/main/docs/.data/benchmark-comparison-2026-07-15.tsv

Shared object_churn medians / mode-local scaling / RSD:

  • 1 lane: 193.853 ms / 1.00x / 13.09%
  • 2 lanes: 391.765 ms / 0.99x / 13.07%
  • 4 lanes: 752.843 ms / 1.03x / 8.40%
  • 8 lanes: 2,374.329 ms / 0.65x / 6.32%

The exact order-balanced parent/candidate A/B held one lane effectively flat (196.399 -> 195.479 ms, -0.5%) and cut eight lanes from 5,554.628 to 2,214.625 ms (-60.1%) with exact checksums. A separate exact seven-sample eight-lane run under /usr/bin/time -l reported maximum resident set size 1,216,266,240 bytes and peak memory footprint 1,215,253,168 bytes, versus the original 7.8 GB profile peak (about 84% lower).

Direct object churn is 109.144 ms versus 117.864 ms for JSC. The production policy is shared-context-only and does not alter private independent-context allocation.

Validation

  • full normal unit gate: 770 passed, 0 skipped, 0 failed, 0 leaked;
  • full TSan gate: 769 passed, 1 expected sustained-allocation soak skip, 0 failed, 0 leaked;
  • focused cooperative, abandoned-collector, GC-cell quota recovery, and ArrayBuffer quota recovery cases pass under normal and TSan builds;
  • benchmark harness guard: 7/7;
  • clean full benchmark matrix: 1,540/1,540 samples, exact checksums, every median above 50 ms;
  • documentation build: 24 pages.

Acceptance

  • Materially reduce shared object-churn peak memory and improve eight-lane throughput/scaling without moving work outside the timed boundary.
  • Do not regress direct or warmed independent object churn.
  • Keep exact object/prototype/indexed replacement/barrier/OOM/finalization/checksum semantics.
  • Pass focused no-GIL, GC, OOM, finalization, full-unit, and TSan gates.
  • Refresh the generated report, raw evidence, benchmark guide, and README after a clean full matrix.

#97 remains open because shared object churn still scales below 1.0x at eight lanes; this issue's bounded production collection scope is complete.

9 days ago

The first production-default experiment is rejected and fully reverted (no commit).

With the exact published workload and checksum, enabling the existing abort-safe mid-script collector for every production no-GIL context failed to complete even one shared object_churn 100 8-lane sample in 70 seconds; baseline samples were 2.615–5.136 seconds. Code inspection explains the result: continuous allocation prevents the born set from reaching a finishable lull, while an elected attempt can spend up to 64 publication/mark rounds before aborting and retrying. The current policy is suitable for tested workloads with allocation lulls, not as a production default for continuous churn.

The issue stays open, but the implementation direction must change: add a bounded stop-the-world/cooperative young-generation rendezvous or another convergence mechanism that guarantees reclamation under continuous allocation. A retry-policy tweak that still aborts without reclaiming is insufficient. The clean worktree is restored; no benchmark-only behavior was retained.

9 days ago

Latest clean publication after the rejected default-promotion experiment:

#118 intentionally keeps the shared/GIL collector conservative and does not address this issue. The existing abort-safe mid-script collector still cannot simply become the production default: under continuous allocation the candidate failed to finish one exact shared-eight sample in 70 seconds, versus 2.6–5.1 seconds for the frozen baseline, because publication/mark rounds abort and retry without a finishable allocation lull.

This remains open for a bounded cooperative/STW young-generation rendezvous or another policy with guaranteed convergence, explicit pause/footprint budgets, and exact no-GIL/finalization semantics.

9 days ago

Rejected another local, uncommitted production shared-heap experiment on 2026-07-15: a bounded cooperative nursery rendezvous at existing VM safepoints. It was correct enough to compile and complete the smoke checks with exact checksums, and its 100 ms timeout prevented an unbounded wait, but coordination happened far too frequently.

ReleaseFast object_churn, 5 jobs, 1 sample (clean 88bd1b0 binary -> candidate):

  • 1 lane: 6.253 ms -> 707.342 ms (113.1x slower)
  • 2 lanes: 10.799 ms -> 1,213.429 ms (112.4x slower)
  • 4 lanes: 30.450 ms -> 2,121.808 ms (69.7x slower)
  • checksums remained exact: 51,008,112 / 102,301,664 / 205,395,904

The candidate was fully reverted and will not be committed. This rules out polling shouldCollectYoung() at every mid-script checkpoint and opening a stop generation immediately. The next design needs allocation-side hysteresis/epoch gating so one nursery threshold crossing triggers at most one rendezvous per meaningful allocation tranche, with peers joining through cheap request checks.

9 days ago

Rejected a second local, uncommitted follow-up on 2026-07-15: let the shipping shared fixed-shape allocation kernel finish a bounded 16K logical-step tranche, then replay every crossed 1,024-step termination/task/GC checkpoint from materialized roots. The existing exact-step/fallback focused test passed and all benchmark checksums stayed exact.

It reduced heap/bucket lock acquisitions from roughly one per 17 objects toward one per ~285 objects, but did not fix the full workload:

  • shared object_churn, 5 jobs, 1 lane: clean ~6.48 ms; candidate 7-sample median 11.73 ms
  • 100 jobs, 1 lane: recent clean 7-sample median 193.71 ms (published 206.28 ms); candidate median 220.69 ms (+13.9% vs recent clean)
  • 100 jobs, 8 lanes: clean 7-sample median 6,430.02 ms; candidate 3-sample median 5,662.16 ms, still in the same noisy/non-scaling 5.7–6.2 s band and nowhere near compensating for the one-lane regression

The candidate was fully reverted. Together with zig-gc #25, this shows acquisition frequency and the metadata critical section are secondary: allocation still performs serialized per-cell owned-backing work, and the shipping path carries roughly 512-byte object cells until the quiescent boundary. The next slice must reduce/bound that cell volume or make the backing common path genuinely parallel; merely widening the checkpoint batch is not acceptable.

9 days ago

Accepted and pushed the bounded production rendezvous in 38f4daa6 (perf(gc): bound shared nursery churn (#116)).

Design that survived A/B and TSan:

  • arm allocation-byte accounting only once a second worker record exists, keeping lone-worker allocation byte-identical;
  • after a 1 GiB shared cell tranche, elect one mutator and open a generation while interpreter registration is gated;
  • peers freeze precise/native roots at existing lock-free checkpoints; the collector waits at most 100 ms and performs no collection on timeout;
  • run one convergent young collection only after every running peer acknowledged (native-parked peers remain pinned by the existing root-lock protocol);
  • retain the old abort-safe concurrent collector exclusively for parallel_midscript_gc stress coverage.

Order-balanced ReleaseFast object_churn, 100 jobs, 7 samples, exact checksums:

  • shared 1 lane: clean 196.399 ms -> candidate 195.479 ms (-0.5%)
  • shared 8 lanes: clean 5,554.628 ms -> candidate 2,214.625 ms (-60.1%)
  • 8-lane aggregate scaling improves from 0.28x in this paired run to 0.71x; the published table was 0.27x.

Threshold sweep (3-sample medians) selected 1 GiB: 512 MiB gave ~807/2,431 ms at 4/8 lanes; 1 GiB ~772/2,354 ms; 2 GiB regressed to ~782/2,686 ms.

Focused gates before push:

  • normal cooperative rendezvous: 2/2 passed, 0 leaks
  • TSan cooperative rendezvous: 2/2 passed, 0 reports/leaks
  • allocation-byte batch/reset coverage passed

The full normal/TSan matrices and 1,540-sample publication benchmark are being batched now; README/docs will be refreshed from that final run in the next small commit.

9 days ago

Full-gate follow-up pushed as 8332a814 (fix(gc): release abandoned shared stops (#116)). The 769-test run exposed the existing 4 MiB quota-OOM sibling-thread case spinning after an elected worker exited with its stop generation still published. A CPU sample showed both survivors entirely inside joinCooperativeGcRequest. Parent 88bd1b0 passed the isolated case in 1 s; the new runtime timed out at 30 s before this fix.

The fix preserves the normal protocol and closes only an abandoned generation: the unregistering collector clears its own published stop, and waiting peers independently verify that the collector remains in the active-interpreter registry before continuing to wait. Non-GC contexts retain the exact original push/pop fast path.

Validation:

  • formerly hanging heap-limit sibling-thread case: 2/2 passed in 1 s
  • cooperative normal focus (live collection + abandoned collector): 3/3 passed, 0 leaks
  • cooperative TSan focus: 3/3 passed, 0 reports/leaks
  • deterministic abandoned-generation cleanup test added

The full gate is restarting from this fix before documentation/publication.

9 days ago

Post-push full-gate follow-up: the normal matrix exposed one instrumentation assertion in the ArrayBuffer quota-recovery case. Shipping cooperative peers now freeze their stacks and acknowledge the stop generation; they do not use the older abort-safe collector's non-blocking root-handoff path.

Fixed in fff12f9 (test(gc): distinguish cooperative peer parks (#116)):

  • added a dedicated cooperative peer-park counter, keeping the two protocols' metrics semantically separate;
  • asserted actual peer parks in both the bounded-churn rendezvous and ArrayBuffer recovery tests;
  • preserved the old peer-publication counter for opt-in abort-safe root handoffs.

Validation after the fix:

  • focused ArrayBuffer recovery: 2/2 passed, 0 leaked;
  • focused cooperative rendezvous: 3/3 passed, 0 leaked;
  • full normal gate: 770/770 passed, 0 skipped, 0 failed, 0 leaked (4m runtime).

Commit is on origin/main with Chris-only authorship.

9 days ago

Full ThreadSanitizer gate on fff12f90 is green: 769 passed, 1 expected sustained-allocation soak skip, 0 failed, 0 leaked across 770 tests (34m runtime). The new cooperative rendezvous, abandoned-collector cleanup, GC-cell quota recovery, and ArrayBuffer quota recovery cases all executed and passed under TSan with no reports.

9 days ago

Completed on main. Runtime commits: 38f4daa6, 8332a814, fff12f90; publication commit: 0ada3601. Final gates: 770/770 normal with zero leaks; 769 TSan passes plus one expected soak skip with zero reports/leaks; 1,540 benchmark samples; 7/7 report guard; 24-page docs build. Exact shared-eight peak RSS is 1,216,266,240 bytes versus the original 7.8 GB profile, and paired throughput improved 60.1% with a flat one-lane result. Remaining sublinear shared object throughput is intentionally tracked by #97.

Sign in to comment on this issue.