Skip to content

perf: object_deep_clone is 37.5x bun (657ms vs 17.5ms) — the worst row in the public artifact, newly visible now that it runs #7533

Description

@proggeramlug

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%.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions