Skip to content

perf: json_pipeline at 500k records is 97.6x bun (60.4s vs 618ms) while the same workload at 100 records BEATS bun — a scaling cliff, not a constant factor #7592

Description

@proggeramlug

Measured in today's public-baseline regeneration (v0.5.1335, 2ba59501b, pinned quiet M1 Max)

honest_bench's JSON pipeline, 20 runs per cell, output verified against the
Bun reference 20/20 for every language
— so this is a pure performance
finding, not a correctness one.

workload perry bun node rust zig perry RSS
json_pipeline_small (100 records) 77.0 ms 90.8 128.1 73.4 72.8 8.5 MB
json_pipeline_full (500,000 records, 107.5 MB) 60,358.5 ms 618.1 1,004.3 650.0 875.5 1,064 MB
image_convolution (4K, 5×5) 302.5 ms 947.2 1,284.8 429.8 279.8 56.2 MB

At 100 records Perry beats bun and node and lands within 5% of Rust and Zig,
on a quarter of bun's memory. At 500k records it is 97.6× bun.
Same source
file, same binary, same harness — only the fixture size changes.

That is the shape of the finding: not slowness, a scaling cliff. Across the
5,000× increase in input, bun's time grows 6.8× (startup-dominated at the small
end); Perry's grows 784×. Rust 8.9×, Zig 12.0×, node 7.8×. Perry is the only
implementation whose growth is superlinear in the data, which points at an
algorithmic or GC-pathological term rather than a constant-factor gap.

RSS is 1,064 MB against bun's 579 MB — 1.8×, and notably not 97×. The memory
is not exploding proportionally to the time, which argues against "it simply
swapped" and for real work being repeated.

Where to look

The workload is small and does five things (workloads/1_json_pipeline/perry/json_pipeline.ts):
fs.readFileSync a 107.5 MB string → JSON.parse to 500k objects → a filter +
field-derive loop building outJSON.stringify(out)fs.writeFileSync,
then an FNV-1a hash over the serialized string.

Each of those is separately suspect at this scale and they should be timed
individually before anything is optimised — this campaign has repeatedly paid
for working from an unattributed headline number. Candidates worth ruling in or
out first:

A leaf profile (PERRY_DEBUG_SYMBOLS=1 + sample) will separate these in one
run; at 60 s per iteration there is plenty of signal.

Two stale claims in the workload source, now disproved

json_pipeline.ts carries a comment block from v0.5.29 stating that (a) the
driver "runs this binary on the 100-record fixture only", and (b) iterating a
large JSON.parse result "triggers a GC-scan issue at scale — records allocated
by the JSON parser get swept mid-iteration… above [~200 records] output is
non-deterministic".

Both are now false. The driver does run Perry on the full 500k fixture
(run.sh:268), and the output matched the Bun reference on 20 of 20 runs.
The correctness half was fixed at some point without the comment being updated.
Worth deleting so the next reader does not attribute the slowness to a
corruption bug that no longer exists.

Why this matters beyond the number

README.md cites this harness by name for its JSON row and quotes the
100-record result. The same report's 500k row is 97.6× bun. Whatever the
intent, the README also says "We publish everything, including the workloads
where V8's JIT still beats us — no cherry-picked table can survive an open
harness."
Those two things need to be reconciled; tracked separately.

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