Skip to content

gc: PERRY_GC_ZEAL forces a collection at EVERY back-edge poll, so zeal no longer terminates on real workloads #7728

Description

@proggeramlug

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions