Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/gc-native-roots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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; }
Expand All @@ -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; }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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; }
Expand Down
20 changes: 20 additions & 0 deletions changelog.d/7338-evacuation-arm-was-vacuous.md
Original file line number Diff line number Diff line change
@@ -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).
64 changes: 64 additions & 0 deletions scripts/gc_evacuation_liveness_assert.py
Original file line number Diff line number Diff line change
@@ -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="<probe>")
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())
Loading