Skip to content

[perf regression] JSON.parse high-volume throughput cliff on 0.5.1265 #7162

Closed

Description

@dlee

What happened

Between 0.5.1260 and 0.5.1265, JSON.parse of a small nested object in a hot
loop developed a throughput cliff: ~1 µs/parse up to ~10k parses, then a step
change to ~250–375 µs/parse.

Measurements (min ms, from-source builds, same host)

Fixture: {"id":42,"name":"fixture","active":true,"tags":["alpha","beta","gamma",7],"meta":{"level":3,"score":512,"child":{"k":9,"items":[1,2,3,4,5]}}}

parses (N) 0.5.1260 0.5.1265
10,000 21 12 (faster)
20,000 42 5842
40,000 83 15023

Control at N=20,000: manual build of the identical graph (no parse) = 12 ms on
1265; parsing any simpler shape ({"a":1}, one array, one string, 3-deep
nesting) = 2–7 ms. The trigger needs the full multi-container graph and high
volume — i.e. how JSON.parse allocates/tenures, not the allocation count.

Minimal reproduction

var SRC = '{"id":42,"name":"fixture","active":true,"tags":["alpha","beta","gamma",7],"meta":{"level":3,"score":512,"child":{"k":9,"items":[1,2,3,4,5]}}}';
function work() {
  var chk = 0;
  for (var i = 0; i < 20000; i++) { var o = JSON.parse(SRC); chk = (chk + o.id + o.meta.score) | 0; }
  return chk | 0;
}
work();
var best = 1e9, c;
for (var r = 0; r < 3; r++) { var t = Date.now(); c = work(); var d = Date.now() - t; if (d < best) best = d; }
console.log(best + " ms");   // ~43 ms on 0.5.1260, ~5800 ms on 0.5.1265

perry compile m.js -o m && ./m. Bisecting across the N=10k→20k knee should
localize it (window has several perf(gc)/nursery changes: #7076/#7079/#7088/#7051/#7057, open #7056).

Environment

Perry 0.5.1260 (2e105ca) vs 0.5.1265 (a3b31c0d), cargo build --release -p perry
from tagged source, each linked to its own full runtime. macOS 27.0 arm64.

Activity

  1. proggeramlug commented on Aug 12, 2026

    @proggeramlug
    Contributor

    Revalidated the issue's exact JSON.parse loop with Perry 0.5.1482 on Windows x86_64 (AMD Ryzen 5 7640HS), using five measured runs for each GC setting after a warm-up.

    iterations moving-loop polls on moving-loop polls off
    10,000 10-12 ms 10-11 ms
    20,000 21-22 ms 20-22 ms
    40,000 44-46 ms 41-50 ms

    All runs produced the expected checksums. The reported 10k -> 20k jump from 12 ms to 5,842 ms is gone; throughput is approximately linear even with moving-loop polls explicitly enabled.

    The regression window included #7019, which made the evacuating young-generation path and moving-loop polls default-on. #7161 disabled that path by default only about 3.5 hours after this issue was filed, and subsequent GC work has since made the enabled poll path linear for this reproducer as well.

    Closing as already fixed; no code change or version bump is needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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