From 421ab49339d42f8b26d83f05593117ed77508d40 Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 31 Jul 2026 22:08:27 -0400 Subject: [PATCH] ci(compiler-output-regression): account for #7088 inline root-shading barrier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The native-region-proof gate went red on main after #7088 moved the shadow-slot root store — and its incremental-mark root-shading barrier — from a js_shadow_slot_bind/js_shadow_slot_set runtime call to inline IR. The barrier was always emitted; it lived inside the runtime function and was invisible to the harness's static call counter. Inlining exposed the js_write_barrier_root_nanbox call site, so write_barriers_static jumped (h1_native_rep_equivalence 0->3, one per rooted Buffer local) and every affected workload tripped its heap-barrier budget. The same lowering adds ss.* blocks ahead of the module-init loops, shifting the deterministic per-function block counter by 12 and blanking the region labels (for.body.2/6/10 -> 14/18/22). Not a real regression: the barrier is gated behind PERRY_INCREMENTAL_MARK_BARRIER_ACTIVE_COUNT, never fires here (write_barriers_traced stays 0), sits in guarded ss.barrier blocks at root-bind sites (never inside the native loops, which keep raw load/store i8 + alias metadata and no runtime calls), and #7088 proves it observationally identical to the call it replaced. - structural_counters scores write_barriers_static on the optimizer-controlled heap barriers (js_write_barrier, js_write_barrier_slot) only; the shadow-stack root-shading barriers (js_write_barrier_root_nanbox, js_write_barrier_root_heap_word) move to a new, reported-but-non-gating root_shading_barriers_static field. Real regressions stay caught: heap barriers are still counted, and a root barrier that actually fires is caught by the write_barriers_traced budget. - h1_native_rep_equivalence region selectors follow the renumbered loop bodies. Bisected to 91f1e7c82 (#7088). Verified: harness unit tests green; h1_native_rep_equivalence + scalar_replacement_literals gate green; the full suite now matches the #7088 parent (CI-green) locally, the only residual failures being pre-existing macOS/clang-16 env noise (smax.i32, clang loop safepoint placement) identical on both. --- benchmarks/compiler_output/workloads.toml | 16 +++++++--- ...native-region-proof-inline-root-barrier.md | 32 +++++++++++++++++++ scripts/compiler_output_harness/analyzers.py | 19 +++++++++-- tests/test_compiler_output_regression.py | 18 +++++++---- 4 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 changelog.d/7136-native-region-proof-inline-root-barrier.md diff --git a/benchmarks/compiler_output/workloads.toml b/benchmarks/compiler_output/workloads.toml index 1848c96946..0b043e65d9 100644 --- a/benchmarks/compiler_output/workloads.toml +++ b/benchmarks/compiler_output/workloads.toml @@ -407,7 +407,10 @@ allowed_missed_reason_kinds = [ [workloads.h1_native_rep_equivalence.runtime_budgets] allocations_traced = 0 gc_collections_traced = 0 -# Includes root barrier setup now counted by the static analyzer. +# Heap write barriers only. The inline shadow-stack root-shading barriers +# added by #7088 (one per rooted Buffer local: src, dst, same) are counted +# under root_shading_barriers_static, not here; they are gated behind +# incremental marking and never fire in this workload (write_barriers_traced=0). write_barriers_static = 1 write_barriers_traced = 0 boxed_number_allocations_static = 0 @@ -445,7 +448,10 @@ no_conversions = true [[workloads.h1_native_rep_equivalence.named_regions.selectors]] function_contains = "main" -label_any = ["for.body.2"] +# for.body.2 -> for.body.14 after #7088: the inline shadow-slot stores add +# ss.* blocks ahead of the module-init loops, shifting the deterministic +# per-function block counter by 12 (2/6/10 -> 14/18/22). +label_any = ["for.body.14"] counter_min = { load_i8 = 1, store_i8 = 1 } [[workloads.h1_native_rep_equivalence.named_regions]] @@ -457,7 +463,8 @@ no_conversions = true [[workloads.h1_native_rep_equivalence.named_regions.selectors]] function_contains = "main" -label_any = ["for.body.6"] +# for.body.6 -> for.body.18 after #7088 (see direct_bounded). +label_any = ["for.body.18"] counter_min = { load_i8 = 1, store_i8 = 1 } [[workloads.h1_native_rep_equivalence.named_regions]] @@ -469,7 +476,8 @@ no_conversions = true [[workloads.h1_native_rep_equivalence.named_regions.selectors]] function_contains = "main" -label_any = ["for.body.10"] +# for.body.10 -> for.body.22 after #7088 (see direct_bounded). +label_any = ["for.body.22"] counter_min = { load_i8 = 1, store_i8 = 1 } [[workloads.h1_native_rep_equivalence.named_regions]] diff --git a/changelog.d/7136-native-region-proof-inline-root-barrier.md b/changelog.d/7136-native-region-proof-inline-root-barrier.md new file mode 100644 index 0000000000..d38268d6d0 --- /dev/null +++ b/changelog.d/7136-native-region-proof-inline-root-barrier.md @@ -0,0 +1,32 @@ +**ci(compiler-output-regression):** the `native-region-proof` gate accounts for +#7088's inline shadow-slot root barrier, restoring green on `main`. + +#7088 moved the per-store shadow-stack root store — and its +incremental-mark root-shading barrier — from a `js_shadow_slot_bind` / +`js_shadow_slot_set` runtime call to inline IR. That barrier was always +emitted; it just lived *inside* the runtime function, invisible to the +harness's static call counter. Inlining made the `js_write_barrier_root_nanbox` +call site visible, so `write_barriers_static` jumped (e.g. h1_native_rep_equivalence +0→3, one per rooted Buffer local) and every affected `native-region-proof` +workload tripped its heap-barrier budget. The same inline lowering inserts +`ss.*` blocks ahead of the module-init loops, shifting the deterministic +per-function block counter by 12 and blanking the `direct_bounded` / +`local_cast` / `helper_index` region labels (`for.body.2/6/10` → `14/18/22`). + +Neither is a real regression: the root-shading barrier is gated behind +`PERRY_INCREMENTAL_MARK_BARRIER_ACTIVE_COUNT`, never fires in these workloads +(`write_barriers_traced` stays 0), and #7088 proves it observationally +identical to the call it replaced. The barriers sit in guarded `ss.barrier` +blocks at root-bind sites, never inside the native loops, which still carry +raw `load i8`/`store i8` with alias metadata and no runtime calls. + +- `structural_counters` now scores `write_barriers_static` on the + optimizer-controlled *heap* barriers (`js_write_barrier`, + `js_write_barrier_slot`) only. The shadow-stack root-shading barriers + (`js_write_barrier_root_nanbox`, `js_write_barrier_root_heap_word`) are + reported under a new `root_shading_barriers_static` field — still visible, + no longer inflating the heap-barrier budget. Real regressions stay caught: + heap barriers are still counted, and a root barrier that actually *fires* + is caught by the `write_barriers_traced` budget. +- `h1_native_rep_equivalence`'s region selectors follow the renumbered loop + bodies (`for.body.14/18/22`). diff --git a/scripts/compiler_output_harness/analyzers.py b/scripts/compiler_output_harness/analyzers.py index eee5151b68..69a6bf72b1 100644 --- a/scripts/compiler_output_harness/analyzers.py +++ b/scripts/compiler_output_harness/analyzers.py @@ -221,9 +221,23 @@ def structural_counters(ir_before: str, ir_after: str, assembly: str) -> dict[st "ptrtoint": ir_after.count(" ptrtoint "), "runtime_calls": runtime_calls, "boxed_number_allocations": after_calls.get("js_boxed_number_new", 0), + # Heap write barriers — the perf-relevant, optimizer-controlled + # barriers this gate exists to catch. Counted statically because a + # native region that stores a GC pointer into a heap object needs + # one; the proof budgets bound how many. "write_barriers": after_calls.get("js_write_barrier", 0) - + after_calls.get("js_write_barrier_slot", 0) - + after_calls.get("js_write_barrier_root_nanbox", 0) + + after_calls.get("js_write_barrier_slot", 0), + # GC shadow-stack root-shading barriers (#7088). Before #7088 these + # lived inside the `js_shadow_slot_bind` / `js_shadow_slot_set` + # runtime calls and were invisible to this static IR counter; #7088 + # emits the shadow-slot store — and its barrier — inline, so the + # call site is now visible here. Each is emitted once per rooted + # pointer-capable local (a structural constant, not an optimizer + # choice) and is gated behind PERRY_INCREMENTAL_MARK_BARRIER_ACTIVE_COUNT, + # so it never fires unless incremental marking is live — meaning it + # is caught, if it ever regresses at runtime, by write_barriers_traced. + # Tracked separately so it does not inflate the heap-barrier budget. + "root_shading_barriers": after_calls.get("js_write_barrier_root_nanbox", 0) + after_calls.get("js_write_barrier_root_heap_word", 0), "buffer_slow_path_calls": sum( count @@ -445,6 +459,7 @@ def runtime_counter_summary( "allocations_traced": traced_allocations, "gc_collections_traced": gc_collections, "write_barriers_static": int(after.get("write_barriers", 0) or 0), + "root_shading_barriers_static": int(after.get("root_shading_barriers", 0) or 0), "write_barriers_traced": traced_write_barriers, "boxed_number_allocations_static": int( after.get("boxed_number_allocations", 0) or 0 diff --git a/tests/test_compiler_output_regression.py b/tests/test_compiler_output_regression.py index 8cdd624223..587dab2c04 100644 --- a/tests/test_compiler_output_regression.py +++ b/tests/test_compiler_output_regression.py @@ -80,21 +80,21 @@ H1_MIN_IR = """ define i32 @main() { entry: - br label %for.body.2 -for.body.2: + br label %for.body.14 +for.body.14: %i = load i32, ptr %slot store i32 %i, ptr %slot %ok = icmp slt i32 %i, %n %p0 = getelementptr i8, ptr %src, i32 %i %b = load i8, ptr %p0 store i8 %b, ptr %p0 - br label %for.body.6 -for.body.6: + br label %for.body.18 +for.body.18: %p1 = getelementptr i8, ptr %src, i32 %i %b1 = load i8, ptr %p1 store i8 %b1, ptr %p1 - br label %for.body.10 -for.body.10: + br label %for.body.22 +for.body.22: %p2 = getelementptr i8, ptr %src, i32 %i %b2 = load i8, ptr %p2 store i8 %b2, ptr %p2 @@ -1556,7 +1556,11 @@ def test_runtime_counter_summary_combines_static_and_trace_counts(self): self.assertEqual(summary["gc_collections_traced"], 2) self.assertEqual(summary["allocations_traced"], 4) self.assertEqual(summary["write_barriers_traced"], 3) - self.assertEqual(summary["write_barriers_static"], 4) + # write_barriers_static counts only heap barriers (js_write_barrier + + # js_write_barrier_slot); the inline shadow-stack root-shading barriers + # (#7088) are tracked separately so they do not inflate the budget. + self.assertEqual(summary["write_barriers_static"], 2) + self.assertEqual(summary["root_shading_barriers_static"], 2) self.assertEqual(summary["boxed_number_allocations_static"], 1) self.assertEqual(summary["buffer_slow_path_accesses_static"], 2) self.assertEqual(summary["array_slow_path_accesses_static"], 2)