The worst row in the public artifact
App-pattern ratios from the public baseline at v0.5.1299 (pinned quiet mini,
AC power, CPU-quiet gate passed, node 22.23.1 / bun 1.3.14, 11 runs/cell):
| kernel |
perry |
bun |
node |
perry/bun |
| object_deep_clone |
657.0 ms |
17.5 ms |
56.9 ms |
37.54× |
| promise_all_chains |
259.7 |
22.7 |
64.0 |
11.44× |
| json_parse_1mb |
438.2 |
68.1 |
127.1 |
6.43× |
| map_1m |
1233.7 |
256.5 |
320.1 |
4.81× |
| batch |
127.8 |
26.5 |
74.8 |
4.82× |
| json_stringify_1mb |
97.3 |
38.5 |
95.1 |
2.53× |
| buffer_transcode |
58.2 |
43.9 |
85.8 |
1.33× |
| date_format_parse |
36.0 |
44.8 |
116.3 |
0.80× (win) |
object_deep_clone is 37.5× bun and 11.5× node — far the worst cell, and
also the worst absolute gap (657 ms against 17.5 ms).
Why it is new to the backlog
It was not slow before; it was broken. Both this kernel and
promise_all_chains failed outright until today (#7475 → fixed by #7495,
#7497 → fixed by #7516/#7529), so neither has ever appeared in a performance
sweep. The engine plan's backlog table — which lists json_parse_1mb 6.27×
as the worst row — predates their working at all.
So these two rows are not regressions from the fixes; they are the first
measurement of code that previously crashed. But that leads directly to the
first thing to check.
First hypothesis to test, and it is a self-suspicion
The kernel is deep clone: spread- and iteration-heavy. That is exactly the
code the same fixes rooted, and rooting means re-reading through a handle at
every use:
Each is individually cheap and all are necessary — they fix real
use-after-frees, and none of them may be reverted. But on a workload that does
nothing except spread and copy objects, "individually cheap" is the wrong unit.
Measure whether the re-reads are material here before assuming the 37.5× is all
pre-existing: build at f06270d06 (before today's rooting stack) and compare.
If the gap is largely pre-existing, that is the answer and the rooting work is
exonerated on the record.
If the re-reads ARE material, the fix is not to remove them. It is to hoist:
re-read once per loop iteration rather than per use, or keep the value in a
shadow-stack slot the collector rewrites in place so no re-read is needed at
all. #7487's pooled frame allocas are the precedent — they turned three FFI
calls per temporary into a store and a load.
Where to look after that
js_object_deep_clone / the structured-clone path, js_array_clone
(array/flat_clone.rs), and the property enumeration tower. Note
object_deep_clone at 657 ms vs promise_all_chains at 260 ms suggests the
cost is in the cloning itself rather than anything promise-shaped.
Acceptance
A real profile first (symbolicated, PERRY_DEBUG_SYMBOLS=1, on the pinned
host) saying where the 657 ms goes. No fix should be attempted before that
number exists — the #7510 experience is that a profile more than a few weeks
old sends people at the wrong 14%.
The worst row in the public artifact
App-pattern ratios from the public baseline at v0.5.1299 (pinned quiet mini,
AC power, CPU-quiet gate passed, node 22.23.1 / bun 1.3.14, 11 runs/cell):
object_deep_cloneis 37.5× bun and 11.5× node — far the worst cell, andalso the worst absolute gap (657 ms against 17.5 ms).
Why it is new to the backlog
It was not slow before; it was broken. Both this kernel and
promise_all_chainsfailed outright until today (#7475 → fixed by #7495,#7497 → fixed by #7516/#7529), so neither has ever appeared in a performance
sweep. The engine plan's backlog table — which lists
json_parse_1mb6.27×as the worst row — predates their working at all.
So these two rows are not regressions from the fixes; they are the first
measurement of code that previously crashed. But that leads directly to the
first thing to check.
First hypothesis to test, and it is a self-suspicion
The kernel is deep clone: spread- and iteration-heavy. That is exactly the
code the same fixes rooted, and rooting means re-reading through a handle at
every use:
js_iterator_to_array's drain loop — a loopbounded at 100 000 iterations, now re-reading each per turn.
[Symbol.iterator]walk (#7498) #7527 replaced a borrowed&strkey withHeapKeyBytes, a 64-byte copy perproperty-name read on the prototype-walk tower.
js_array_clone(every[...arr]/Array.from).Each is individually cheap and all are necessary — they fix real
use-after-frees, and none of them may be reverted. But on a workload that does
nothing except spread and copy objects, "individually cheap" is the wrong unit.
Measure whether the re-reads are material here before assuming the 37.5× is all
pre-existing: build at
f06270d06(before today's rooting stack) and compare.If the gap is largely pre-existing, that is the answer and the rooting work is
exonerated on the record.
If the re-reads ARE material, the fix is not to remove them. It is to hoist:
re-read once per loop iteration rather than per use, or keep the value in a
shadow-stack slot the collector rewrites in place so no re-read is needed at
all. #7487's pooled frame allocas are the precedent — they turned three FFI
calls per temporary into a store and a load.
Where to look after that
js_object_deep_clone/ the structured-clone path,js_array_clone(
array/flat_clone.rs), and the property enumeration tower. Noteobject_deep_cloneat 657 ms vspromise_all_chainsat 260 ms suggests thecost is in the cloning itself rather than anything promise-shaped.
Acceptance
A real profile first (symbolicated,
PERRY_DEBUG_SYMBOLS=1, on the pinnedhost) saying where the 657 ms goes. No fix should be attempted before that
number exists — the #7510 experience is that a profile more than a few weeks
old sends people at the wrong 14%.