Three sweep-corpus benchmarks regressed between 73df2a6e7 and 905017b1c (the nine commits landed on 2026-08-23). shapes is 3× the instruction count.
Evidence
Reproduced on two machines with two different methods, so this is not bench-host noise.
Bench mini, perry/node ratio (same scriptc/porffor/node pins, two independent runs):
| bench |
baseline 73df2a6e7 |
run 1 |
run 2 |
shapes |
0.931x |
2.917x |
2.912x |
cycles |
0.842x |
1.331x |
1.363x |
deeplist |
0.474x |
0.569x |
0.580x |
pipeline |
0.919x |
1.114x |
1.083x |
The other 16 rows sit on baseline in both runs. Both runs were flagged DIRTY on load, but contamination picks different victims each time — these reproduce to within ~2%, so the load flag is not the explanation.
Local Mac, wall clock, best-of-3 on the compiled binaries directly:
shapes old=0.09s new=0.38s 4.22x SLOWER
cycles old=0.07s new=0.12s 1.71x SLOWER
deeplist old=0.05s new=0.06s 1.20x SLOWER
fib40 old=0.65s new=0.52s 0.80x (faster)
tree old=0.30s new=0.28s 0.93x (fine)
It is extra mutator work, not GC and not scheduling (/usr/bin/time -l, p_shapes):
|
old |
new |
|
| instructions retired |
1,186,944,293 |
3,667,840,774 |
3.09x |
| cycles elapsed |
319,225,659 |
970,790,835 |
3.04x |
| max RSS |
33.5 MB |
46.0 MB |
1.37x |
GC diag lines (PERRY_GC_DIAG=1) |
26 |
26 |
identical |
Same instruction/cycle ratio either side, identical collection count — the program simply executes ~2.5 billion more instructions. That is the shape of a per-operation linear scan being added to a hot path.
What it is not
Leading suspect
shapes is by far the most class-heavy row in the corpus (7 classes, inheritance, super() chains, static members, getters, instanceof, virtual dispatch), and severity tracks class complexity rather than mere class presence — tree, tree_wide and push_cls each use one class and are unaffected.
That points at #8630 (class semantics tail, landed 905017b1c). Its receiver_has_own_key guard added to set_field_by_name_object_tail is a keys-array scan on the by-name store path, which is exactly a per-operation linear scan. The #8630 author already noticed the cost and moved own_key_present to be evaluated last and only on the slow path — the question is whether class-instance stores (this.x = x in a constructor, run 120,000 times here) still reach it.
Candidate window, most to least likely: #8630 (class semantics) > #8638 (node:test) > #8632/#8637 (HTTP) > #8633/#8623 (spill, largely excluded above).
Not yet done
A proper bisect. I built 00bddb34b (immediately pre-#8630) to A/B it, but the build was interrupted and the host is at load ~45, so I stopped rather than produce a measurement I could not trust. The decisive next step is a perry built at 00bddb34b timed against 905017b1c on shapes.
Repro:
# old binary is preserved in the sweep artifacts
./sweep-artifacts-0822e/perry/p_shapes # 73df2a6e7
./sweep-artifacts-0823b/perry/p_shapes # 905017b1c
/usr/bin/time -l ./…/p_shapes 2>&1 | grep -E "instructions retired|cycles elapsed"
Three sweep-corpus benchmarks regressed between
73df2a6e7and905017b1c(the nine commits landed on 2026-08-23).shapesis 3× the instruction count.Evidence
Reproduced on two machines with two different methods, so this is not bench-host noise.
Bench mini, perry/node ratio (same scriptc/porffor/node pins, two independent runs):
73df2a6e7shapescyclesdeeplistpipelineThe other 16 rows sit on baseline in both runs. Both runs were flagged
DIRTYon load, but contamination picks different victims each time — these reproduce to within ~2%, so the load flag is not the explanation.Local Mac, wall clock, best-of-3 on the compiled binaries directly:
It is extra mutator work, not GC and not scheduling (
/usr/bin/time -l,p_shapes):PERRY_GC_DIAG=1)Same instruction/cycle ratio either side, identical collection count — the program simply executes ~2.5 billion more instructions. That is the shape of a per-operation linear scan being added to a hot path.
What it is not
shapes,cycles,pipeline,deeplist,fib40emits a spill notice on the new compiler, so no function crossed the threshold. This was my first hypothesis and it is wrong.Leading suspect
shapesis by far the most class-heavy row in the corpus (7 classes, inheritance,super()chains, static members, getters,instanceof, virtual dispatch), and severity tracks class complexity rather than mere class presence —tree,tree_wideandpush_clseach use one class and are unaffected.That points at #8630 (class semantics tail, landed
905017b1c). Itsreceiver_has_own_keyguard added toset_field_by_name_object_tailis a keys-array scan on the by-name store path, which is exactly a per-operation linear scan. The #8630 author already noticed the cost and movedown_key_presentto be evaluated last and only on the slow path — the question is whether class-instance stores (this.x = xin a constructor, run 120,000 times here) still reach it.Candidate window, most to least likely: #8630 (class semantics) > #8638 (node:test) > #8632/#8637 (HTTP) > #8633/#8623 (spill, largely excluded above).
Not yet done
A proper bisect. I built
00bddb34b(immediately pre-#8630) to A/B it, but the build was interrupted and the host is at load ~45, so I stopped rather than produce a measurement I could not trust. The decisive next step is a perry built at00bddb34btimed against905017b1conshapes.Repro: