PERRY_GC_ZEAL=1 is the primary instrument for catching moving-GC correctness bugs (paired with PERRY_GC_PROTECT_FROMSPACE=1 it produced the precise fault that diagnosed #7682). It no longer completes on a real workload.
Repro
gc-handoff/apps/iso_miss.ts — a tree-walking interpreter, 40 rounds of fib 21.
| build |
config |
wall |
ca8c0d617 (0.5.1418) |
no zeal |
19 s, checksum 437840 misses 0 |
ca8c0d617 |
PERRY_GC_ZEAL=1 |
timeout at 240 s, no output |
f3e14a61e (0.5.1389) |
PERRY_GC_ZEAL=1 |
instant, correct |
It is not a livelock, and f3e14a61e was not "working"
Measured on the pinned quiet host, scaling the round count:
| rounds |
loop_polls |
forced collections |
wall |
| 1 |
70,963 |
70,968 |
36.3 s |
| 2 |
141,926 |
141,931 |
72.7 s |
Perfectly linear, so 40 rounds is ~24 minutes rather than a hang. Zeal forces a collection at every back-edge poll: ~511 µs of fixed per-collection cost (root scan over the shadow stack plus ~55 side-table scanners) to relocate a mean of 5.9 objects. Practically all the work is the collection's fixed overhead, not the relocation zeal exists to stress.
The f3e14a61e endpoint was fast because it was vacuous: PERRY_GC_MOVING_LOOP_POLLS was default-OFF there (#7161), so a compute-only program reached no loop safepoint and zeal forced nothing. That build predates the #7604 exit verdict, so it exited 0 in silence. #7721 flipped the poll default ON — correctly, it is a large collector win — and in the same commit turned zeal from free-and-vacuous into correct-but-unusable.
Confirmed by isolation: the old f3e14a61e compiler with PERRY_GC_MOVING_LOOP_POLLS=1 forced at compile+run time already takes 35.8 s for one round under zeal (0.62 s without). So no commit "broke" zeal; zeal was never paced, and the poll default is what exposed it.
#7254 already logged "a striking concentration of multi-minute-plus runs" under this pairing and left the population untriaged. This is that triage.
Why it matters
An instrument that does not terminate is an instrument nobody switches on, and this bug class (a stale pointer across a moving collection, #7154 family) has shipped more than once. scripts/gc_instrument_smoke.sh cannot see the problem because its fixture is deliberately sized at ~1200 polls "so the zeal arm costs seconds rather than minutes" — at that size, every-poll and paced are indistinguishable.
Fix direction
Pace zeal by allocation, the way V8 (--gc-interval) and SpiderMonkey (gcZeal(mode, frequency)) do, with an escape hatch restoring literal every-poll semantics, plus a budgeted CI arm at a realistic poll count so this cannot regress silently again.
PERRY_GC_ZEAL=1is the primary instrument for catching moving-GC correctness bugs (paired withPERRY_GC_PROTECT_FROMSPACE=1it produced the precise fault that diagnosed #7682). It no longer completes on a real workload.Repro
gc-handoff/apps/iso_miss.ts— a tree-walking interpreter, 40 rounds offib 21.ca8c0d617(0.5.1418)checksum 437840 misses 0ca8c0d617PERRY_GC_ZEAL=1f3e14a61e(0.5.1389)PERRY_GC_ZEAL=1It is not a livelock, and
f3e14a61ewas not "working"Measured on the pinned quiet host, scaling the round count:
Perfectly linear, so 40 rounds is ~24 minutes rather than a hang. Zeal forces a collection at every back-edge poll: ~511 µs of fixed per-collection cost (root scan over the shadow stack plus ~55 side-table scanners) to relocate a mean of 5.9 objects. Practically all the work is the collection's fixed overhead, not the relocation zeal exists to stress.
The
f3e14a61eendpoint was fast because it was vacuous:PERRY_GC_MOVING_LOOP_POLLSwas default-OFF there (#7161), so a compute-only program reached no loop safepoint and zeal forced nothing. That build predates the #7604 exit verdict, so it exited 0 in silence. #7721 flipped the poll default ON — correctly, it is a large collector win — and in the same commit turned zeal from free-and-vacuous into correct-but-unusable.Confirmed by isolation: the old
f3e14a61ecompiler withPERRY_GC_MOVING_LOOP_POLLS=1forced at compile+run time already takes 35.8 s for one round under zeal (0.62 s without). So no commit "broke" zeal; zeal was never paced, and the poll default is what exposed it.#7254 already logged "a striking concentration of multi-minute-plus runs" under this pairing and left the population untriaged. This is that triage.
Why it matters
An instrument that does not terminate is an instrument nobody switches on, and this bug class (a stale pointer across a moving collection, #7154 family) has shipped more than once.
scripts/gc_instrument_smoke.shcannot see the problem because its fixture is deliberately sized at ~1200 polls "so the zeal arm costs seconds rather than minutes" — at that size, every-poll and paced are indistinguishable.Fix direction
Pace zeal by allocation, the way V8 (
--gc-interval) and SpiderMonkey (gcZeal(mode, frequency)) do, with an escape hatch restoring literal every-poll semantics, plus a budgeted CI arm at a realistic poll count so this cannot regress silently again.