Skip to content

bench: drop the windows closure_per_call overlay - #1043

Merged
youknowone merged 1 commit into
mainfrom
buitlins
Aug 5, 2026
Merged

bench: drop the windows closure_per_call overlay#1043
youknowone merged 1 commit into
mainfrom
buitlins

Conversation

@youknowone

@youknowone youknowone commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Reverts the overlay added in #1042. It recorded a value the host does not hold
steady, so it moved the failure rather than removing it.

run shared file overlay windows cranelift observed result
check.py's overlay comment (two runs) 415 416
#1028 CI 415 416 FAIL, regressed
#1042 CI 415 416 415 FAIL, improved

Whichever number is recorded, the other one shows up.

The same run moved the second fixture that comment names, in the opposite
direction relative to its shared file: recursive_call_frame_relocation
reported guard_failures=637 on windows dynasm against a shared 638, having
agreed at 638 one run earlier. One counter drifting toward its shared value
while another drifts away from it in the same run is not a per-host fact, and a
per-host file cannot hold it.

JITSTATS_STABILITY_RUNS does not see this: it re-runs one binary inside one
job, and the disagreement is between jobs.

That leaves the two fixtures gated on a counter that disagrees with itself
across windows runs. Nothing here decides what to do about that — an overlay
just is not the mechanism.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Removed obsolete JIT statistics data from the Windows 32-bit benchmark output.

The counter it recorded is not stable on that host. The overlay was written
from three readings of 416 (two named in check.py's overlay comment, one on
#1028's CI run); the next windows cranelift run reported 415 against it, so
each value fails whenever the other one is recorded.

`recursive_call_frame_relocation` moved the same way in the same run and in the
opposite direction relative to its shared file — guard_failures 638 -> 637 on
windows dynasm, where it had agreed at 638 one run earlier. Two counters
disagreeing with themselves across runs, one toward its shared value and one
away from it, is not a per-host fact an overlay can hold.

The stability re-runs re-run one binary inside one job, so an oscillation
between jobs is invisible to them.

Assisted-by: Claude
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1da4db8a-945d-4065-99bc-9d4640108174

📥 Commits

Reviewing files that changed from the base of the PR and between a8677f4 and ec86e94.

📒 Files selected for processing (1)
  • pyre/bench/synth/closure_per_call.cranelift.win32.jitstats
💤 Files with no reviewable changes (1)
  • pyre/bench/synth/closure_per_call.cranelift.win32.jitstats

Walkthrough

The change removes all contents from the Windows Cranelift closure_per_call JIT statistics baseline file.

Changes

Cohort / File(s) Summary
JIT statistics baseline
pyre/bench/synth/closure_per_call.cranelift.win32.jitstats
Removes bridge, descriptor, guard-failure, panic, loop, and compilation counters.

Estimated code review effort: 1 (Trivial) | ~2 minutes

Possibly related PRs

Poem

A rabbit found the counters gone,
No bridge or loop remained by dawn.
The Windows baseline now sleeps light,
With empty stats and fields of white.
Hop, hop—clean files shine bright!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: removing the Windows closure_per_call overlay.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch buitlins

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit ec86e94).
Updated: 2026-08-05T03:55:51.032Z

The diff against the review base changes only generated jit-stats baselines (1 file(s)); nothing to review.

@youknowone
youknowone merged commit 2f61280 into main Aug 5, 2026
16 of 17 checks passed
@youknowone
youknowone deleted the buitlins branch August 5, 2026 04:17
youknowone added a commit that referenced this pull request Aug 5, 2026
…failures

windows reports guard_failures=637 against a shared 638, on three consecutive
runs at three different base shas (#1042 at 4fecf86, #1043 at a8677f4,
main at 2f61280), with every other counter identical. check.py's overlay
comment names the same value from two earlier runs. macos-latest and
ubuntu-24.04 pass their dynasm legs against the shared file, so it keeps
describing them.

The single contrary reading is one run before that sequence, where windows
reported 638 and `closure_per_call` reported 416. Both counters moved down by
one at the same point and have held since; the move left `closure_per_call`
agreeing with its shared file, which is why the overlay dropped in the previous
commit is not needed and this one is.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 5, 2026
Its cranelift guard_failures is not the same on every host. macos-latest and
ubuntu-24.04 both report 5980; windows-latest reported 5979 against that
recording. #1043 removed the closure_per_call overlay after two counters in
that band disagreed with themselves across jobs, so a
`.cranelift.win32.jitstats` overlay cannot hold this one either, and a missing
baseline is a hard fail rather than an opt-out.

The walker guard the file was promoted for therefore has no suite gate. The
file stays as the reproduction, with the measurement and the reason in its
header.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 5, 2026
…nd token fixes; journal the walk-time traceback attaches (#1050)

* jit: carry the virtualizable `debugdata` static as a box, not a recording-time constant

`MIFrame::flush_to_frame` republished all six virtualizable statics as
constants read out of the live frame. `DELETE_NAME` resolves the name
through `debugdata.w_locals`, so the compiled loop deleted through the
recording frame's mapping once `exec(code, globals)` reused the same code
object with a fresh globals dict, raising NameError. Keep the box
`read_boxes` seeded for that field; the remaining pointer statics stay
promoted constants.

Adds pyre/bench/synth/exec_fresh_globals_delete_name.py with its dynasm,
cranelift and wasm jit-stats baselines.

Assisted-by: Claude

* jit: keep the pointer flag through the BhFieldSpec round trip

`BhFieldSpec::from_field_descr` mapped every pointer field to
`ArrayFlag::Unsigned`. `field_spec_from_bh` copies that flag into the
rebuilt `SimpleFieldDescrSpec`, whose `is_pointer_field()` is
`flag == ArrayFlag::Pointer` (descr.py:173), so a pointer field that
traversed the round trip reported itself as non-pointer and
`handle_write_barrier_setfield` declined its write barrier.

The codewriter's other two producers, `value_type_to_field_flag` and
`bh_field_flag_from_descr`, already map a pointer field to
`ArrayFlag::Pointer`.

Assisted-by: Claude

* jit: settle the virtualizable token on the top-level raise exit

`compile_exit_frame_with_exception` (pyjitpl.py:3261) opens with
`store_token_in_vable()`, the same as `compile_done_with_this_frame`
(:3223). pyre's value and void arms settle the token via
`fbw_force_virtualizable_before_return`; the top-level raise arm only
published `last_instr`. Every residual call arms the token in
`walker_vable_and_vrefs_before_residual_call`, so the exception exit left
the frame naming a jitframe that dynasm frees in `execute_token`, and the
next `is_force_token_armed` walked `jf_forward` off freed memory.

pyre settles by storing back, which zeroes the slot, rather than by arming
the token as upstream does: upstream's FORCE_TOKEN is the heap-allocated
GC `JITFRAME`, pyre's is the machine frame pointer.

Adds `top_level_raise_settles_the_vable_token`, which asserts the recorded
tail op is a `SetfieldGc` of const-0 at the token offset.

Assisted-by: Claude

* jit: journal the walk-time traceback attaches so an aborted walk undoes them

`record_top_level_application_traceback`,
`record_inline_application_traceback` and
`record_exc_edge_discarded_tracebacks` apply their node to the live exception
while the walk records. Only the bridge handler entry wrapped that mutation in
the fbw undo log, so a walk that did not commit left the other three sites'
nodes attached and the interpreter's replay recorded the same frames again.
Move the journaling into the three attach sites and drop the bridge arm's own
wrapper, which is now redundant.

A freshly raised exception hides this — the discarded walk's object is
unreachable — so the observable case is a `raise` that re-uses one exception
object: `tb_frame.f_locals` on the catching frame aborts the walk and the
handler then reads a four-node chain where the interpreter builds two.

Adds `pyre/bench/synth/exception_reused_object_tb_not_doubled.py` and a unit
test for the multi-node rollback.

Assisted-by: Claude

* gc: skip the PyObject-shaped raw walks for a virtual ref in a frame slot

`walk_pyframe_roots_area` handed every `locals_cells_stack_w` slot to
`walk_raw_exception_roots`, which calls `is_exception` and so dereferences the
slot's leading word as an `ob_type`. A slot can hold a `JitVirtualRef`, whose
leading word is the `JIT_VIRTUAL_REF_VTABLE` magic, and the dereference then
faults. The same function already tested `ptr_is_virtual_ref` on the `f_backref`
slot one block earlier; extract that arm as `forward_virtual_ref_forced` and
apply it to the value-stack slots too, forwarding `forced` and skipping the raw
walks.

`walk_raw_immortal_roots` was already safe for this input: `walk_immortal_rec`
guards its `ob_type` read behind `try_gc_owns_object`, and a vref is allocated
with `alloc_oldgen_typed`.

Reproduced on cranelift with a hot `raise`/`except` loop whose handler reads
`tb_frame.f_locals` on the catching frame; the collection that faults is a minor
one triggered by a nursery allocation from compiled code. dynasm and the
interpreter are unaffected.

Assisted-by: Claude

* bench: park two cranelift crash witnesses under synth/_pending

`exception_tb_f_locals_vref_root_walk.py` is the oracle for the virtual-ref
root-walk fix: pre-fix it SIGSEGVs 3/3 on cranelift at `15000 head`, post-fix it
matches dynasm, `PYRE_NO_JIT=1` and pypy. It stays out of the suite because the
same file run with no arguments hits an unrelated open abort.

`gc_varsize_item_const_shape_witness.py` is that abort:
`GC BUG ... site=minor_varsize_item_target`, cranelift 3/3, dynasm and the
interpreter clean. Its docstring records the decoded holder fields and the
allocation-layout trigger.

Assisted-by: Claude

* jit: give the miframe raise test a real exception object

`journaled_concrete_traceback_attach` reads the concrete exception's traceback
head before the attach, so the walk now dereferences the pointer that
`dispatch_via_miframe_mirrors_last_exc_value_back_into_sym` passed as a bare
`0xDEAD_BEEF` sentinel. The sentinel survived only because the recording hook is
not installed under test; the production path dereferences the same pointer.
Build the operand from `w_exception_new` instead.

Assisted-by: Claude

* gc: report the root path that reached an object in the trace panics

`trace_and_update_object` receives a `site` naming the root path it was reached
from — `minor_remembered_set`, `minor_jitframe_root`, and the rest — but when it
recursed into `copy_nursery_object` or `assert_traced_slot_initialized` it
passed a hardcoded site naming only the SLOT KIND, so the invalid-type_id and
nursery-poison panics could not say which root path produced the bad holder.

Thread the caller's site down as `parent_site` and add it to both messages. The
parameter is `&'static str` so no allocation happens on a traced slot.

On the `minor_varsize_item_target` witness under `synth/_pending` this reports
`parent_site=minor_remembered_set` 3/3, which rules out the jitframe
shadow-stack root walk as that abort's path.

Assisted-by: Claude

* jit: allocate traced traceback nodes non-moving

`w_pytraceback_new` allocates a traceback node through the stable old-gen
allocator and states the obligation in place: raw `*mut PyTraceback` readers
and the exception `w_traceback` chain hold bare pointers no root walker
reaches. `PYTRACEBACK_DESCR_GROUP` never stamped `set_non_moving`, so a node
built by compiled code was a nursery object a minor collection could relocate.

Stamp the group's size descr, as the `W_ObjectObject` group and
`mapdict_storage_gcarray_descr` already do for the same reason.

`gc_varsize_item_const_shape_witness.py` and four stack-depth variants of it
abort with `GC BUG ... site=minor_varsize_item_target` at 18 of 320
(fixture, nursery size) pairs over a 512K-32M `PYPY_GC_NURSERY` sweep before
this change, and at none of them after; a finer 256K-step sweep to 51M over
two of the fixtures is also clean.

Assisted-by: Claude

* jit: assert the non-moving stamp on the three raw-pointer descr groups

`PYTRACEBACK_DESCR_GROUP`, the `W_ObjectObject` group and
`mapdict_storage_gcarray_descr` each carry `set_non_moving(true)` to match an
interpreter allocator that is non-moving because raw pointers reach the object
without being rooted. Nothing checked the flags, and a missing one shows up
only as a GC abort at particular nursery sizes — `check.py` pins
`PYPY_GC_NURSERY` to 4MB, which is not one of them.

Assert the three flags. Removing the traceback stamp fails the test.

Assisted-by: Claude

* bench: re-record the guard_failures baselines the traceback descr stamp moved

Fourteen `.jitstats` baselines, `guard_failures` only; `loops_compiled` and
`bridges_compiled` are unchanged in every one.

Assisted-by: Claude

* bench: record the three count-valued counters in the wasm delete-name baseline

The committed baseline named only the five badness fields. `_jit_stats_change`
reads a field absent from either side as 0, so `loops_compiled`,
`bridges_compiled` and `guard_failures` compared against 0 and the wasm run of
this fixture now reports 1, 0 and 4 — the same three values its cranelift and
dynasm baselines carry.

Assisted-by: Claude

* bench: promote the virtual-ref root-walk fixture out of _pending

The file was parked because running it with no arguments hit
`GC BUG ... site=minor_varsize_item_target` instead of its own subject. With
traced traceback nodes allocated non-moving it is clean at all 64 sizes of a
512K-32M `PYPY_GC_NURSERY` sweep, and cranelift, dynasm and `PYRE_NO_JIT=1`
print the same line, so it can gate the walker guard from the suite.

wasm is exempted. It loses `e` from the catching frame's `f_locals` over part
of the loop and prints a second tuple; that is compiled-code only (clean at
N=4000, wrong from N=8000) and reproduces with the whole source tree reset to
the merge base, so it is neither this fixture's subject nor this branch's. Same
stale-`f_locals`-from-compiled-code class as
getframe_caller_locals_nested_compiled_callee, which carries the same exemption.

Assisted-by: Claude

* bench: park the virtual-ref root-walk fixture back under _pending

Its cranelift guard_failures is not the same on every host. macos-latest and
ubuntu-24.04 both report 5980; windows-latest reported 5979 against that
recording. #1043 removed the closure_per_call overlay after two counters in
that band disagreed with themselves across jobs, so a
`.cranelift.win32.jitstats` overlay cannot hold this one either, and a missing
baseline is a hard fail rather than an opt-out.

The walker guard the file was promoted for therefore has no suite gate. The
file stays as the reproduction, with the measurement and the reason in its
header.

Assisted-by: Claude

* jit: replay the folded inline-callee locals when a traceback node takes the frame

A `STORE_FAST` on a strictly-inlined callee's own fresh frame takes the fold in
`setarrayitem_vable_via_metainterp`: it updates the walker shadow, pokes the
recording-time frame through `store_live_frame_array_slot`, and records no
`SETARRAYITEM_GC`. `disarm_folded_inline_callee_after_escape` is the replay that
writes those slots back and disarms the fold, and it had one call site — the
branch where a residual call forced the virtualizable.

`emit_traceback_node` stores the callee frame box into `PyTraceback.frame`,
which is a second escape: the frame outlives the trace with its locals array
holding only the parameters `emit_new_pyframe_inline_with_params` seeded. Call
the replay from `traceback_node_site` before handing the box out, declining the
site on `Err` as both callers already do for an unresolved frame. It is inert at
the top level, where there is no inline callee.

`exception_inline_callee_tb_frame_locals` reads
`tb.tb_frame.f_locals` off the callee's node. dynasm and cranelift both answered
`('i',)` on 2758 of 4000 iterations before this and `('i', 'marker')` on all
4000 after, matching pypy3, CPython and PYRE_NO_JIT=1.

Assisted-by: Claude
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant