diff --git a/.github/workflows/gc-moving-witnesses.yml b/.github/workflows/gc-moving-witnesses.yml new file mode 100644 index 0000000000..70b4050c49 --- /dev/null +++ b/.github/workflows/gc-moving-witnesses.yml @@ -0,0 +1,339 @@ +name: GC Moving Witnesses + +# Runs the stale-root REPRODUCERS against the moving collector, which is the +# only configuration in which they can fail. +# +# WHY THIS EXISTS +# +# `test-files/test_gap_gc_*.ts` are not ordinary parity tests. Each one was +# written to reproduce a specific stale-root defect — #6981, #7114, #7154, +# #7200/#7201/#7202, #7208/#7209, #7206, #7214, #7216 — and each was verified +# by its PR author by hand, compiled AND run with `PERRY_GC_MOVING_LOOP_POLLS=1`. +# Every one of those corpus notes says the same thing in different words: the +# file is clean on the shipped default and only bites on a `requires=move` arm. +# +# No CI job ran THEM with that flag. Two jobs already set it, and neither +# executes a witness: +# +# * `gc-root-dominance` compiles a corpus with it +# (scripts/gc_root_dominance_corpus.sh) because the flag is what puts +# `js_gc_loop_safepoint` in the IR — but it is a STATIC pass over that IR +# and never runs a program; +# * `gc-stress`'s instrument smoke (scripts/gc_instrument_smoke.sh) compiles +# and runs with it, but against its own small synthetic fixture, not the +# reproducers. +# +# The witnesses themselves reach CI only through `gc-stress`, which runs the +# matrix's PR arm subset — `default,evac_minor,verify_evac,force_verify, +# cons_scan_off,shipped_default` — and not one of those arms compiles with the +# flag. So on every pull request these files were compiled into IR in which +# their bug is not expressible, then run to completion, green, proving nothing. +# They were reproductions, not gates — the fourth of CLAUDE.md's "four ways a +# gate can be unable to fail": the job runs, but its subject never does. +# +# #7161 makes this sharper rather than softer. It flipped the evacuating minor +# default-OFF as a stopgap for #7154, so `PERRY_GC_MOVING_LOOP_POLLS=1` is now +# the ONLY configuration that exercises the moving collector end to end. Under +# the GC knob kill-policy an unexercised mode is a configuration nobody has +# verified, and this is the mode the whole #7154 fix campaign is aimed at. +# +# WHAT IT DOES +# +# One arm, one filter: +# +# scripts/gc_repsel_matrix.sh --arms loop_polls --filter test_gap_gc_ +# +# `loop_polls` is the existing matrix arm that puts `PERRY_GC_MOVING_LOOP_POLLS=1` +# in BOTH the compile env and the run env. Both halves are load-bearing and were +# verified to still be so at this commit: #7161 made the flag a compile-time gate +# as well as a runtime one (`moving_safepoint_polls_enabled()` in +# `perry-codegen/src/stmt/loops.rs` decides whether codegen emits the +# `js_gc_loop_safepoint` back-edge polls at all; `gc_moving_loop_polls_enabled()` +# in `perry-runtime/src/gc/policy.rs` decides whether the runtime defers to them). +# Setting it at run time only would defer collections to polls that were never +# emitted. The object cache keys the flag (`env_gc_moving_loop_polls` in +# `perry/src/commands/compile/object_cache.rs`), so a warm cache cannot serve +# objects built without it — see the `PERRY_DISABLE_BUILD_CACHE` note below for +# the one place that is still true only by accident. +# +# THIS JOB IS DESIGNED TO BE ABLE TO FAIL, checked against all four ways a gate +# can be unable to (CLAUDE.md): +# +# 1. no `continue-on-error`, no `|| true`, no pipe between a checker and the +# shell's exit status; +# 2. NOT in branch protection's required contexts yet, deliberately: a new +# gate has never been green, so promoting it immediately would block every +# open PR. Promotion is a follow-up, and the corollary in CLAUDE.md is that +# leaving that second step undone is itself hazard 2 — `gc-root-dominance` +# sat red on `main` for weeks because nobody took it. Promote this once it +# has a green run on `main`; +# 3. `concurrency` cancels pull-request runs only, and push runs are keyed on +# the commit so `main` runs cannot cancel each other while queued (#7205); +# 4. the subject is ASSERTED live, not assumed. The matrix reports a cell that +# matched the oracle under an inert arm as UNVER, never PASS — but its own +# exit status only counts FAIL, so an all-UNVER table exits 0. That is +# precisely a green gate over a subject that never ran, so the checker step +# below rejects UNVER as hard as it rejects FAIL. A witness that stops +# relocating anything is a finding, not a pass. +# +# WHAT IT DELIBERATELY DOES NOT COVER +# +# * `--filter test_gap_gc_` excludes the representation corpus, and with it +# #7194 — `test_gap_repsel_p4a3_ptr_numarray` is red on `main` across all +# ten `requires=move` arms (`evacuated=0`), pre-existing and untriaged. That +# red belongs to `gc-stress --arms all`; importing it here would make this +# gate unpromotable on day one for a reason that has nothing to do with the +# witnesses. +# * `--arms loop_polls` is the safepoint route only. #7217 reports +# `test_gap_gc_spread_accessor_rooting` still SIGSEGVing on `main` under the +# `evac_minor` arm env, which forces the collection at the +# register-imprecise ALLOCATION point instead. #7207 measured the same file +# clean 5/5 on the polls route and this arm reproduces that. So #7217's +# defect is real and out of scope here, not papered over: it needs the +# allocation-point arm, which cannot join this gate until it is fixed. +# +# Both exclusions are structural — they fall out of the arm and the filter — so +# there is no allowlist here to rot. A witness that goes red goes red. + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + # One group per main COMMIT, cancelling PR runs only. `cancel-in-progress: + # false` alone does not protect a `main` run: GitHub allows at most one + # PENDING run per group and cancels the previously pending one when a new run + # enters, regardless of that setting. That is #7205, measured on gc-ratchet — + # three consecutive `main` runs cancelled with `jobs: []`, zero executions. + # Keying push runs on the SHA gives every merged commit a group of its own. + group: gc-moving-witnesses-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + CARGO_TERM_COLOR: always + # NOTE the absence of `PERRY_NO_AUTO_OPTIMIZE`. gc-ratchet sets it for a + # deterministic link; scripts/gc_repsel_matrix.sh deliberately does NOT, and + # says why in its own header: the auto-optimizer decides whether the linked + # runtime carries the `diagnostics` feature, which changes the GC trace + # format the harness parses for its liveness evidence. Setting it here would + # override that choice from outside and could silently turn every cell UNVER. + # + # #7183: `PERRY_GC_MOVING_LOOP_POLLS` became a compile-time gate in #7161 but + # is still missing from `build_cache.rs`'s env key list, so the build-level + # no-op probe cannot tell a binary built with the flag from one built without + # it. Today nothing bites — the per-object cache DOES key it, the build + # manifest is keyed on the output path, and the matrix compiles into a fresh + # mktemp directory every run — but "safe by three accidents" is not a property + # this arm should depend on, since it is the one arm that must not go dark. + # Disabling the build cache costs nothing here (each witness is compiled once) + # and leaves the object cache, which keys the flag correctly, in place. + PERRY_DISABLE_BUILD_CACHE: "1" + +jobs: + gc-moving-witnesses: + # ubuntu-latest, matching `gc-stress` in test.yml: the matrix already runs + # there, the queue is shallower than the macOS pool, and the cargo cache is + # shared with that job (same `shared-key`) so the build is usually warm. + runs-on: ubuntu-latest + # 90 to match gc-stress: the witness run itself is minutes, but a cold + # cargo cache makes the compiler build the whole cost of this job. + timeout-minutes: 90 + permissions: + contents: read + # Read-only listing of the PR's changed files, to decide whether this + # change can reach the collector. Same approach `gc-ratchet` uses; it + # avoids a `fetch-depth: 0` clone of a large history. + pull-requests: read + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Decide whether this change can affect the moving collector + id: relevance + env: + GH_TOKEN: ${{ github.token }} + EVENT_NAME: ${{ github.event_name }} + REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + if [[ "$EVENT_NAME" != "pull_request" ]]; then + echo "run=true" >> "$GITHUB_OUTPUT" + echo "Not a pull request; running the witnesses." + exit 0 + fi + gh api "repos/$REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename' > changed.txt + # Deliberately broad: anything under crates/ can move the collector — + # a codegen change alters which roots are stored, a transform change + # alters what escapes, a runtime change alters when a cycle fires. + # The filter exists only to spare docs-only PRs a compiler build. If + # the listing is empty or the API failed, `set -e` already aborted, so + # the job cannot silently fall through to "not relevant". + if grep -qE '^(crates/|scripts/gc_repsel_matrix\.sh$|test-files/test_gap_gc_|test-parity/gc_repsel_|Cargo\.(toml|lock)$|\.node-version$|\.github/workflows/gc-moving-witnesses\.yml$)' changed.txt; then + echo "run=true" >> "$GITHUB_OUTPUT" + echo "Change touches collector-relevant paths; running the witnesses." + else + echo "run=false" >> "$GITHUB_OUTPUT" + echo "No collector-relevant paths changed." + fi + + - name: Install Rust toolchain + if: steps.relevance.outputs.run == 'true' + uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + if: steps.relevance.outputs.run == 'true' + with: + # Same key as `gc-stress`, which builds the identical package set, so + # this job usually starts from that job's warm cache instead of paying + # for a second cold build of the workspace. + shared-key: "${{ runner.os }}-perry" + save-if: ${{ github.ref == 'refs/heads/main' }} + + - name: Install clang + if: steps.relevance.outputs.run == 'true' + run: | + sudo apt-get update + sudo apt-get install -y clang + + - name: Setup Node oracle + if: steps.relevance.outputs.run == 'true' + uses: actions/setup-node@v7 + with: + # Single source of truth: .node-version. Node is a correctness input, + # not a peer benchmark — every witness's stdout is byte-diffed against + # it. scripts/gc_repsel_matrix.sh refuses to run when the running node + # disagrees with the pin, because a test the oracle cannot run would + # drop out of the gate silently. + node-version-file: .node-version + + - name: Build perry and the runtime archives + if: steps.relevance.outputs.run == 'true' + env: + # Match the cargo-test and gc-stress gates' linker workaround (lld + # SIGBUS on the shared runner during large links). + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS: "-C linker-features=-lld" + run: | + set -euo pipefail + # perry-runtime and perry-stdlib are rlib-only; libperry_runtime.a and + # libperry_stdlib.a come from the -static wrapper crates. Building + # without them links a stale archive and makes the whole measurement + # vacuous. The package set matches gc-stress exactly so cargo feature + # unification — and therefore the cache — is identical. + cargo build --release \ + -p perry -p perry-runtime -p perry-stdlib \ + -p perry-runtime-static -p perry-stdlib-static + for artifact in perry libperry_runtime.a libperry_stdlib.a; do + test -s "target/release/$artifact" \ + || { echo "::error::target/release/$artifact was not produced"; exit 1; } + done + + # GATING. No pipe, no `|| true`: this step's exit status is half the gate. + # It fails on any untriaged red cell. The other half is the liveness check + # below, which the matrix's own exit status does not cover. + - name: Run the stale-root witnesses on the moving collector + id: matrix + if: steps.relevance.outputs.run == 'true' + run: | + ./scripts/gc_repsel_matrix.sh \ + --no-build \ + --arms loop_polls \ + --filter test_gap_gc_ \ + --json gc-moving-witnesses.json + + # GATING, and the reason this file is a gate rather than a reproduction. + # + # The matrix exits 0 on an all-UNVER table: UNVER means "output matched + # the oracle but the arm was measurably inert here", which is the exact + # shape of a green gate whose subject never ran (#6942/#6946/#6950/#7025). + # For a `requires=move` arm it means nothing relocated, and a stale-root + # witness that never sees a relocation cannot fail no matter how broken + # the rooting is. So UNVER is rejected here as hard as FAIL. + # + # It also closes a registration hole. scripts/gc_repsel_matrix.sh + # auto-detects unregistered `test_gap_repsel_*` / `test_gap_specabi_*` + # files, but NOT this prefix — test-parity/gc_repsel_corpus.txt's own + # header says files outside those two prefixes must be registered by hand, + # and #6925 is the precedent for what happens when someone forgets. Every + # `test-files/test_gap_gc_*.ts` on disk must therefore appear as a cell. + - name: Assert the witnesses actually ran under a moving collector + if: always() && steps.relevance.outputs.run == 'true' + run: | + python3 - <<'PY' + import glob, json, os, sys + + REPORT = "gc-moving-witnesses.json" + if not os.path.exists(REPORT): + sys.exit("::error::%s was not produced — the matrix step never " + "reported; see its log above." % REPORT) + + report = json.load(open(REPORT)) + arms = report.get("arms", []) + if [(a["id"], a["requires"]) for a in arms] != [("loop_polls", "move")]: + sys.exit("::error::expected exactly the loop_polls/move arm, got %r" + % (arms,)) + + cells = report.get("cells", []) + seen = {c["test"] for c in cells} + on_disk = {os.path.basename(p)[:-3] + for p in glob.glob("test-files/test_gap_gc_*.ts")} + + problems = [] + + unregistered = sorted(on_disk - seen) + for name in unregistered: + problems.append( + "%s exists but was not run: register it in " + "test-parity/gc_repsel_corpus.txt. The matrix only " + "auto-detects the test_gap_repsel_*/test_gap_specabi_* " + "prefixes, so a test_gap_gc_* witness that is not registered " + "is silently dark (#6925 is the precedent)." % name) + + for c in sorted(cells, key=lambda c: c["test"]): + name, result, ev = c["test"], c["result"], c.get("evidence", "") + print("%-8s %-46s %s" % (result, name, ev)) + if result == "PASS": + continue + if result == "XFAIL": + # Triaged in test-parity/gc_repsel_triage.txt, which requires + # an issue number and a written reason. Loud, but not fatal. + print("::warning::%s is a triaged expected-red on this arm: %s" + % (name, ev)) + continue + if result == "UNVER": + problems.append( + "%s matched the oracle but NOTHING MOVED (%s). On a " + "requires=move arm that means the moving collector never " + "relocated anything, so this witness could not have " + "failed however broken its rooting is. Re-tune its churn " + "budget or fix the collector — do not read it as green." + % (name, ev)) + else: + problems.append("%s: %s (%s)" % (name, result, ev)) + + if not cells: + problems.append("no cells at all — the filter matched nothing.") + + if problems: + for p in problems: + print("::error::%s" % p) + sys.exit(1) + + print("\nOK: %d witnesses, every one relocated under " + "PERRY_GC_MOVING_LOOP_POLLS=1." % len(cells)) + PY + + - name: Upload the witness report + if: always() && steps.relevance.outputs.run == 'true' + uses: actions/upload-artifact@v7 + with: + name: gc-moving-witnesses-${{ github.sha }} + path: gc-moving-witnesses.json + if-no-files-found: ignore + retention-days: 30 diff --git a/.github/workflows/gc-ratchet.yml b/.github/workflows/gc-ratchet.yml index 0f83cd6428..820cd2aa4c 100644 --- a/.github/workflows/gc-ratchet.yml +++ b/.github/workflows/gc-ratchet.yml @@ -23,7 +23,7 @@ permissions: contents: read concurrency: - # Separate groups per event, and cancel ONLY pull-request runs. + # ONE GROUP PER MAIN COMMIT, and cancel ONLY pull-request runs. # # A shared group with unconditional cancel-in-progress looked right and was # wrong: `main` is busy and the macOS runner pool is deep enough that a run @@ -33,10 +33,24 @@ concurrency: # A gate that is always cancelled never fails, which is the same hole as # `continue-on-error: true` wearing a different hat. # - # Cancelling superseded PR runs is still correct: only the head commit's - # result gates the merge. Main-branch runs queue instead, so each merged - # commit eventually gets its own answer. - group: gc-ratchet-${{ github.event_name }}-${{ github.ref }} + # ***SCOPING `cancel-in-progress` TO PULL REQUESTS DID NOT FIX IT (#7205).*** + # It protects a run that has already STARTED. GitHub additionally allows at + # most one PENDING run per group: when a new run enters a group, "any + # previously pending job or workflow in the concurrency group will be + # cancelled" — regardless of `cancel-in-progress`. Every `main` run here is + # pending for tens of minutes, so a merge cadence faster than the queue depth + # kept cancelling all of them. Re-measured after the first fix: runs + # 30707891646 (#7192), 30708370282 (#7198) and 30707189162 (#7195) all ended + # `cancelled` with `jobs: []` — never reached a runner, zero executions in + # three merges. + # + # Keying the group on the COMMIT for push events is what actually fixes it: + # each merged commit gets a group of its own, so no two `main` runs can ever + # contend and none can supersede another. They queue independently and each + # eventually gets its own answer. PR runs keep sharing a per-ref group and + # keep superseding themselves, which is still correct — only the head + # commit's result gates the merge. + group: gc-ratchet-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: diff --git a/.github/workflows/gc-root-dominance.yml b/.github/workflows/gc-root-dominance.yml index 541f28c2c6..977d002790 100644 --- a/.github/workflows/gc-root-dominance.yml +++ b/.github/workflows/gc-root-dominance.yml @@ -61,11 +61,15 @@ permissions: contents: read concurrency: - # Per-event groups, cancelling PR runs only. A shared group with an - # unconditional cancel-in-progress starves `main`: on a deep runner queue - # every merge cancels the previous main run before it reaches a runner, and a - # gate that is always cancelled never fails. Same reasoning as gc-ratchet.yml. - group: gc-root-dominance-${{ github.event_name }}-${{ github.ref }} + # One group per main COMMIT, cancelling PR runs only. A shared group starves + # `main` on a deep runner queue, and a gate that is always cancelled never + # fails. `cancel-in-progress: false` is not enough on its own: GitHub allows + # at most one PENDING run per group and cancels the previously pending one + # when a new run enters, regardless of that setting (#7205 — measured on + # gc-ratchet, whose shape this file copied). Keying push runs on the SHA gives + # every merged commit a group of its own, so no two main runs can contend. + # Same reasoning as gc-ratchet.yml, which carries the full writeup. + group: gc-root-dominance-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: diff --git a/changelog.d/7228-gc-moving-witness-arm.md b/changelog.d/7228-gc-moving-witness-arm.md new file mode 100644 index 0000000000..77f7ce80f2 --- /dev/null +++ b/changelog.d/7228-gc-moving-witness-arm.md @@ -0,0 +1,29 @@ +### CI: the moving-GC stale-root witnesses can now fail + +`test-files/test_gap_gc_*.ts` are reproducers, not parity tests — each was written +for a specific stale-root defect (#6981, #7114, #7154, #7192, #7200/#7201/#7202, +#7206, #7208/#7209, #7214, #7216) and each is clean on the shipped default by +construction. Since #7161 flipped the evacuating minor default-OFF, the bug they +reproduce is only expressible under `PERRY_GC_MOVING_LOOP_POLLS=1` at compile +**and** run time, and no CI job ran them that way: `gc-root-dominance` compiles +with the flag but never runs a program, `gc_instrument_smoke.sh` runs with it +against its own fixture, and `gc-stress`'s PR arm subset contains no arm that +compiles with it. They went green on every PR while proving nothing. + +New non-required `gc-moving-witnesses` workflow runs +`gc_repsel_matrix.sh --arms loop_polls --filter test_gap_gc_` and then rejects +`UNVER` as hard as `FAIL` — on a `requires=move` arm an inert cell means nothing +relocated, so the witness could not have failed. Proven red-then-green by +reverting #7214's two production codegen files: 15/15 PASS → 12 PASS/3 FAIL on +exactly #7214's three witnesses → 15/15 PASS restored. + +Found two witnesses that were registered nowhere and therefore ran nowhere: +`test_gap_gc_new_instance_rooting` (#7192) and +`test_gap_gc_assign_string_source_rooting` (#7216). Both registered; the second +surfaced a pre-existing #7217 red on the allocation-point arms (a sharper +reproducer than the one that issue names), triaged with measurements. + +Also fixes #7205: `gc-ratchet` and `gc-root-dominance` key their push-event +concurrency group on the commit. `cancel-in-progress: false` does not protect a +*queued* `main` run — GitHub allows one pending run per group and cancels the +previous one — which is why `gc-ratchet` executed zero times across three merges. diff --git a/test-parity/gc_repsel_corpus.txt b/test-parity/gc_repsel_corpus.txt index 083b437818..9991520d51 100644 --- a/test-parity/gc_repsel_corpus.txt +++ b/test-parity/gc_repsel_corpus.txt @@ -262,3 +262,58 @@ test_gap_gc_index_get_receiver_rooting test_gap_gc_closure_call_callee_rooting test_gap_gc_closure_call_this_rooting test_gap_gc_closure_call_argument_rooting + +# --- Two witnesses that were registered nowhere (#7192, #7216) --------------- +# Both files exist in test-files/, both say in their own headers that they are +# LIVE BY CONSTRUCTION and bite only on the moving arms, and neither was in this +# manifest -- so neither ran anywhere, not in `gc-stress --arms pr` and not in +# `--arms all`. `new_instance_rooting` shipped with #7192 and +# `assign_string_source_rooting` with #7216; both PRs registered their siblings +# and missed these. +# +# The UNREGISTERED gate in scripts/gc_repsel_matrix.sh cannot catch this: it +# auto-detects only `test_gap_repsel_*` / `test_gap_specabi_*`, and this file's +# header rule for every other prefix ("register them explicitly") was enforced +# by nothing. It is enforced for `test_gap_gc_*` now -- +# .github/workflows/gc-moving-witnesses.yml requires every +# test-files/test_gap_gc_*.ts on disk to appear as a cell, which is how these +# two were found. +# +# Measured on this commit (7d1dc9ca2), `--arms loop_polls --filter test_gap_gc_`: +# both PASS with copy-minor > 0, i.e. they relocate and they are byte-exact. +# Also measured on the `gc-stress` PR arm set: PASS/UNVER only, no new red. +test_gap_gc_new_instance_rooting +test_gap_gc_assign_string_source_rooting + +# --- UNROOTED CACHES: a different failure class entirely (#7211) ------------- +# Every entry above is a stale REGISTER or a late/mis-indexed/missing root +# STORE. In all of them the value is reachable from something the collector +# scans and the defect is that one location did not get rewritten. These two are +# neither: a thread-local cache and a swapped-out root cell hold a GC pointer +# that NOTHING registers, so the FIRST collection kills it and the holder names +# abandoned memory for the rest of the process. +# +# The distinction decides which instrument can find them: +# +# stale register (#7154 class) | goes bad only when a collection lands in a +# | narrow window -- timing-dependent, needs a +# | workload plus repetition; the static IR pass +# | CAN see the shape +# unrooted cache (#7211) | goes bad at collection #0 and stays bad -- +# | reproduces 10/10; INVISIBLE to the static +# | pass, which reads emitted LLVM IR and cannot +# | see a runtime-side table at all +# +# So a CI arm that runs real workloads under PERRY_GC_MOVING_LOOP_POLLS=1 is the +# ONLY thing that catches this class. Registering them here is what puts them in +# front of it -- .github/workflows/gc-moving-witnesses.yml runs every registered +# `test_gap_gc_*` on the `loop_polls` arm and refuses to pass a cell that did +# not relocate. +# +# Measured on this branch, release, `--arms loop_polls --filter test_gap_gc_`: +# both PASS with the copying minor live (the arm reported copy-minor 16/16 across +# the whole gc-witness set). Measured on the #7227 branch, which is #7226 plus +# the regexp-receiver fix -- a different subsystem, and not something that could +# make a typeof cache or an implicit-this restore pass. +test_gap_gc_typeof_string_cache_rooting +test_gap_gc_closure_call_prev_this_rooting diff --git a/test-parity/gc_repsel_triage.txt b/test-parity/gc_repsel_triage.txt index a7c1437ab8..75fdba403f 100644 --- a/test-parity/gc_repsel_triage.txt +++ b/test-parity/gc_repsel_triage.txt @@ -15,3 +15,53 @@ # temporary. Do not re-triage it without a new issue number and a reason. test_gap_repsel_ptr_shape_locals | rep_ptr_shape_off | #6976 -- REGRESSION IN THE REPRESENTATION'S OWN OFF-SWITCH, not a defect in this PR. Bisected: passes at 8327ced52, fails at 1a533a3a8 (#6925, repsel Phase 5a proven `this`). With PERRY_PTR_SHAPE_LOCALS=0 the program dies partway with `TypeError: Cannot read properties of undefined (reading 'area')`, losing its last five output lines. It was invisible until now because #6925 also left test_gap_repsel_proven_this_frozen.ts unregistered, which makes this script exit 3 before it runs anything -- the gate was dark, not green. REMOVE THIS ENTRY when #6976 is fixed; an OFF arm is supposed to be the safest cell in the matrix. + +# --- #7216's Object.assign witness on the ALLOCATION-POINT arms (#7217) ------ +# `test_gap_gc_assign_string_source_rooting` shipped with #7216 but was never +# registered in the corpus, so it had never run anywhere. Registering it (see +# gc_repsel_corpus.txt) surfaced a pre-existing red on every arm that forces the +# collection at the register-imprecise ALLOCATION point (`%E%` without the +# compile-time `PERRY_GC_MOVING_LOOP_POLLS=1`). +# +# This is #7217, not a new defect and not a representation defect. #7217 already +# says the mechanism in words -- "an allocation-point collection inside the +# helper ... can fire between any two of the helper's own allocations, including +# ones inside `object_assign_set_string_key`'s interning and keys-array growth, +# where #7207 re-reads its handles only at the top of each key iteration" -- and +# this file is that sentence's reproducer, and a sharper one than the +# `spread_accessor_rooting` case #7217 names -- that one is load-dependent and +# passed idle here, while this one reproduces on demand at the same env. +# +# Measured on 7d1dc9ca2, release, idle host, oracle node 26.5.1 +# (`bad char 0 count 0`): +# PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off +# -> `bad char 2 count 2`, 5/5 +# shipped default -> `bad char 0 count 0`, 3/3 +# compiled+run PERRY_GC_MOVING_LOOP_POLLS=1 (`loop_polls`) -> PASS, copy-minor > 0 +# +# EVERY %E% ARM IS LISTED, INCLUDING ONE THAT WAS GREEN WHEN FIRST SAMPLED. +# `rep_str_off` PASSED on the first `--arms all` sweep and FAILED on the third -- +# same binary, same idle host. The first read ("PERRY_CANONICAL_STR_LOCALS=0 +# makes it pass, so the borrow is in a canonical Str local") was an artefact of a +# single sample. The window is timing-sensitive within the allocation point and +# the repsel knobs are not discriminators, so the entries cover the arm class +# rather than the arms that happened to be red on one run. Anyone bisecting +# #7217 with this file should repeat each candidate before believing a green. +# +# THE FILE IS NOT DARK WHILE THESE ENTRIES EXIST. It is a hard gate on +# `loop_polls`, which .github/workflows/gc-moving-witnesses.yml runs on every +# collector-touching PR and which requires it to relocate before it may pass. +# That arm was green 8/8 across these sweeps. +# +# DELETE ALL TEN when #7217 is fixed. An entry that matches nothing fails +# nothing here, but a stale triage is a gate quietly narrowed. +test_gap_gc_assign_string_source_rooting | evac_minor | #7217 -- allocation-point relocation inside js_object_assign_one / object_assign_string_source, pre-existing on main. Clean on the safepoint route (loop_polls) and on the shipped default. +test_gap_gc_assign_string_source_rooting | force_evac | #7217 -- same allocation-point window, with force-evacuate on top. +test_gap_gc_assign_string_source_rooting | force_verify | #7217 -- same allocation-point window, force + verify. +test_gap_gc_assign_string_source_rooting | rep_i32_off | #7217 -- %E% allocation-point window; the repsel knob is not the discriminator. +test_gap_gc_assign_string_source_rooting | rep_str_off | #7217 -- %E% allocation-point window. Green on the first sweep and red on the third, same binary and idle host: sampled, not fixed. +test_gap_gc_assign_string_source_rooting | rep_str_static_off | #7217 -- %E% allocation-point window; see rep_i32_off. +test_gap_gc_assign_string_source_rooting | rep_ptr_shape_off | #7217 -- %E% allocation-point window; see rep_i32_off. +test_gap_gc_assign_string_source_rooting | rep_ptr_numarray_off | #7217 -- %E% allocation-point window; see rep_i32_off. +test_gap_gc_assign_string_source_rooting | rep_spec_abi_off | #7217 -- %E% allocation-point window; see rep_i32_off. +test_gap_gc_assign_string_source_rooting | rep_int_valued_off | #7217 -- %E% allocation-point window; see rep_i32_off.