diff --git a/.github/workflows/gc-native-roots.yml b/.github/workflows/gc-native-roots.yml index 24a63890c8..94c59c4277 100644 --- a/.github/workflows/gc-native-roots.yml +++ b/.github/workflows/gc-native-roots.yml @@ -133,6 +133,7 @@ jobs: otool -l "/tmp/$name" | grep -q "sectname __llvm_stackmaps" \ && { echo "::error::$name still carries __llvm_stackmaps — the compact rewrite did not run"; exit 1; } PERRY_STATEPOINTS=1 PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 \ + PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off \ "/tmp/$name" > "/tmp/$name.out" 2> "/tmp/$name.err" diff "/tmp/$name.oracle" "/tmp/$name.out" \ || { echo "::error::$name output diverged from the pinned oracle"; exit 1; } @@ -257,6 +258,7 @@ jobs: # holding, not the enforcement being absent. PERRY_STATEPOINTS=1 PERRY_GC_SAFEPOINT_ONLY=strict \ PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 \ + PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off \ "/tmp/so-$name" > "/tmp/so-$name.out" 2> "/tmp/so-$name.err" diff "/tmp/so-$name.oracle" "/tmp/so-$name.out" \ || { echo "::error::$name diverged from the oracle under the safepoint-only contract"; exit 1; } @@ -303,6 +305,7 @@ jobs: # cannot see a slot the walker never reached. PERRY_STACKMAP_WALKER=verify PERRY_GC_TRACE=1 \ PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 \ + PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off \ "/tmp/w-$name" > "/tmp/w-$name.verify.out" 2> "/tmp/w-$name.verify.err" diff "/tmp/w-$name.oracle" "/tmp/w-$name.verify.out" \ || { echo "::error::$name diverged from the oracle under PERRY_STACKMAP_WALKER=verify"; exit 1; } @@ -311,6 +314,7 @@ jobs: # unwind: the bisection control. Same roots, platform unwinder only. PERRY_STACKMAP_WALKER=unwind PERRY_GC_TRACE=1 \ PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 \ + PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off \ "/tmp/w-$name" > "/tmp/w-$name.unwind.out" 2> "/tmp/w-$name.unwind.err" diff "/tmp/w-$name.oracle" "/tmp/w-$name.unwind.out" \ || { echo "::error::$name diverged from the oracle under PERRY_STACKMAP_WALKER=unwind"; exit 1; } @@ -351,6 +355,34 @@ jobs: fi echo "bridge refused the try-carrying probe, as expected (#7327)" + # #7336: the evacuation arm was VACUOUS. The probes drive collection with + # `gc()`, which takes `manual_collect` — a full mark-sweep behind a forced + # conservative scan — and `PERRY_GC_FORCE_EVACUATE` is read only on the + # MINOR path. Measured: `copied_objects` and `moved_objects` were 0 on + # every probe, while `--require-fp-walks` passed because it asserts a walk + # HAPPENED, not that it FOUND anything. That is #6942/#6946 repeating, the + # one CLAUDE.md records as costing months of meaningless green. + # + # The arms above now drive the minor path. This asserts they actually + # moved something, so the gate fails if it ever goes inert again. + - name: The evacuation arm must actually evacuate + if: ${{ !cancelled() }} + run: | + set -uo pipefail + export PERRY_RUNTIME_DIR="$PWD/target/perry-dev" + export PERRY_NO_AUTO_OPTIMIZE=1 + fail=0 + for probe in benchmarks/gc_ratchet/probes/*.ts; do + name=$(basename "$probe" .ts) + [ "$name" = "09_try_catch_roots" ] && continue + PERRY_STATEPOINTS=1 ./target/perry-dev/perry "$probe" -o "/tmp/ev-$name" >/dev/null 2>&1 || continue + PERRY_STATEPOINTS=1 PERRY_GC_FORCE_EVACUATE=1 \ + PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off \ + PERRY_GC_DIAG=1 "/tmp/ev-$name" > /dev/null 2> "/tmp/ev-$name.err" || true + python3 scripts/gc_evacuation_liveness_assert.py "/tmp/ev-$name.err" --probe "$name" || fail=1 + done + exit $fail + native-roots-rs4gc-aarch64: runs-on: macos-14 timeout-minutes: 90 @@ -406,6 +438,7 @@ jobs: otool -l "/tmp/rs4gc-$name" | grep -q "sectname __llvm_stackmaps" \ && { echo "::error::$name still carries __llvm_stackmaps — the compact rewrite did not run"; exit 1; } PERRY_RS4GC=1 PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 \ + PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off \ "/tmp/rs4gc-$name" > "/tmp/rs4gc-$name.out" 2> "/tmp/rs4gc-$name.err" diff "/tmp/rs4gc-$name.oracle" "/tmp/rs4gc-$name.out" \ || { echo "::error::$name diverged from the pinned oracle under RS4GC"; exit 1; } diff --git a/changelog.d/7338-evacuation-arm-was-vacuous.md b/changelog.d/7338-evacuation-arm-was-vacuous.md new file mode 100644 index 0000000000..38f7a3c01a --- /dev/null +++ b/changelog.d/7338-evacuation-arm-was-vacuous.md @@ -0,0 +1,20 @@ +`gc-native-roots`' forced-evacuation arm evacuated nothing, on every probe. + +The probes drive collection with `gc()`, which takes `manual_collect` — a full +mark-sweep behind a forced conservative scan — and `PERRY_GC_FORCE_EVACUATE` is +read only on the *minor* path. Measured: `copied_objects` and `moved_objects` +were 0 on all eight probes, and 5 of 8 matched zero stack-map records while the +conservative scan did the rooting. The existing `--require-fp-walks` assert +passed throughout, because it checks that a walk *happened*, not that it *found* +anything. + +This is #6942/#6946 repeating — the case CLAUDE.md records as costing months of +"passes under evacuation" that meant nothing — inside the gate whose results were +being used to argue for making statepoints the default. + +The evacuation arms now drive the minor path +(`PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off`), +under which all 8 probes move: 5,946 to 90,271 objects copied each. A new +`scripts/gc_evacuation_liveness_assert.py` requires at least one copying minor +that copied at least one object, and names the cause when it finds none — +`manual_collect` (wrong path) or an ineligible copying minor (#7255). diff --git a/scripts/gc_evacuation_liveness_assert.py b/scripts/gc_evacuation_liveness_assert.py new file mode 100755 index 0000000000..5501bf25ef --- /dev/null +++ b/scripts/gc_evacuation_liveness_assert.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Assert a forced-evacuation run actually evacuated something (#7336). + +`PERRY_GC_FORCE_EVACUATE=1` is read only on the *minor* path. A probe that +drives collection with `gc()` takes `manual_collect`, a full mark-sweep behind a +forced conservative scan, and evacuates nothing — the run is green and the arm +measured no moving collector at all. + +That is not hypothetical: it is #6942/#6946, which CLAUDE.md records as costing +months of "passes under evacuation" that meant nothing. The `gc-native-roots` +evacuation arm was repeating it — `copied_objects` and `moved_objects` were 0 on +every probe, while `--require-fp-walks` passed because it asserts that a walk +*happened*, not that it *found* anything. + +So this asserts the subject was live: at least one copying minor ran, and it +copied at least one object. Reads `PERRY_GC_DIAG=1` output on stderr. + + gc_evacuation_liveness_assert.py trace.err --probe 01_nursery_churn +""" +from __future__ import annotations + +import argparse +import re +import sys + +COPIED = re.compile(r"copied_objects=(\d+)") +ELIGIBLE = re.compile(r"\[gc-copy-minor\] eligible=(\w+)(?: fallback=(\S+))?") +MANUAL = re.compile(r"\[gc-scan-fallback\] site=manual_collect") + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("trace") + ap.add_argument("--probe", default="") + ap.add_argument("--min-copied", type=int, default=1) + args = ap.parse_args() + + text = open(args.trace, "r", errors="replace").read() + copied = sum(int(m) for m in COPIED.findall(text)) + ran = text.count("[gc-copy-minor] ran ") + ineligible = [m for m in ELIGIBLE.findall(text) if m[0] != "true"] + + if copied >= args.min_copied and ran > 0: + print(f"{args.probe}: evacuation live — {ran} copying minor(s), {copied} objects copied") + return 0 + + print(f"::error::{args.probe}: the forced-evacuation arm evacuated NOTHING " + f"({ran} copying minors, {copied} objects copied). The arm is vacuous: " + f"it proves the program ran, not that a moving collector did (#7336).") + if MANUAL.search(text): + print("::error::Saw `site=manual_collect` — this probe drives GC with `gc()`, " + "which is a full mark-sweep behind a forced conservative scan. " + "PERRY_GC_FORCE_EVACUATE is read only on the MINOR path (#6942/#6946). " + "Drive the minor path instead: PERRY_GC_HEAP_LIMIT=8 " + "PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off.") + if ineligible: + kinds = sorted({f or '?' for _, f in ineligible}) + print(f"::error::Copying minor was ineligible; fallback(s): {', '.join(kinds)}. " + "PERRY_CONSERVATIVE_STACK_SCAN=off is usually what makes it eligible (#7255).") + return 1 + + +if __name__ == "__main__": + sys.exit(main())