Improve app pattern performance gaps - #1296
Conversation
Post-review fixes (7 commits + fmt)Applied the items raised in the self-review above:
Before / after — full app-pattern matrixAbsolute numbers on the post-fix sweep are noticeably higher across all three runtimes (Bun and Node are 1.5-3× slower too) — that's machine noise / thermal on the rerun host, not code. The honest comparison is the perry/bun ratio column:
Summary: 5 better, 5 same, 1 worse, 0 in the slow bucket (≥2×) — same as the PR baseline. Trade-off:
|
) `crates/perry-codegen/src/collectors/escape_news.rs` was 2034 lines and failed the 2,000-line file-size CI gate. Splitting it into topical sibling modules under `collectors/` — no behavior change; function bodies move verbatim and the same public/internal API is re-exported from `collectors/mod.rs`. Moves: - `this_as_value.rs` — `class_uses_this_as_value`, `class_chain_extends_builtin_error`, `stmts_use_this_as_value`, `expr_uses_this_as_value`. - `class_accessors.rs` — `is_class_getter`, `is_class_setter`. - `escape_check.rs` — `find_new_candidates`, `check_escapes_in_stmts`, `check_escapes_in_expr`. - `local_refs.rs` — `expr_contains_local_get`, `mark_all_candidate_refs_in_expr`. `escape_news.rs` keeps the escape-collection entry points (`collect_non_escaping_news`, `collect_non_escaping_new_used_fields` and the two private helpers) plus the trailing `MAX_SCALAR_ARRAY_LEN` const. Final line counts: escape_news 693, escape_check 870, this_as_value 337, local_refs 114, class_accessors 50 — all comfortably under the 2,000 gate.
…is shared When the same pending promise is fed into multiple Promise.all([...]) calls (legal JS), only the first registered state was being drained on settle because promise_all_take_handler used swap_remove on the first match. The second Promise.all would never resolve. Replace the take handler with promise_all_take_all_handlers, which drains every PROMISE_ALL_STATES entry keyed on the settling promise. Both js_promise_resolve and js_promise_reject in then.rs now enqueue one PromiseAll task per drained state. Adds a unit test in promise/combinators.rs (promise_all_with_shared_pending_input_resolves_both) that registers two Promise.all calls sharing a pending input and verifies both settle after the shared promise resolves.
…d path With GC_THRESHOLD_INITIAL_BYTES now equal to GC_TRIGGER_ABSOLUTE_CEILING (both 128 MB after app-pattern tuning), the C4b-delta-tune hard cap on next_trigger supersedes any adaptive doubling. Add an inline note above the doubling branch explaining the cap and preserving the 1 GB GC_THRESHOLD_MAX_BYTES bound as an intentional escape hatch for future threshold raises or env-driven bisection.
The length<=64 fast path skips the generational write barrier and only records layout metadata, on the assumption that the freshly-allocated result array stays in the nursery for the life of the loop. Document the invariant explicitly so a future GC policy change that tenures aggressively (or PERRY_GC_FORCE_EVACUATE stress runs) doesn't silently break the remembered set for this code path.
TypeScript classes can declare `get` accessors with arbitrary side
effects, and the discard-map optimization has no general way to prove
that a PropertyGet on the callback parameter targets a plain data slot.
Remove PropertyGet from the discard_pure_expr arms — anonymous-shape
constructions still flow through the Expr::New { __AnonShape_… } arm,
which is the intended target of the optimization.
Per ES2015 §12.2.5, `[...x]` invokes GetIterator(x) which throws a
TypeError when x is null or undefined. The single-spread fast path
previously routed straight through js_array_clone, which silently
returns an empty array for those inputs (kept for back-compat with
Array.from's not-iterable behavior).
Add a dedicated runtime entry point js_array_clone_for_spread that
inspects the NaN-box tag bits on the raw boxed value and throws a
TypeError("<receiver> is not iterable") for null/undefined before
forwarding to js_array_clone. Update the codegen single-spread arm
to call it. Array.from's behavior is unchanged.
The (target_has_slots, slot) tuple where target_has_slots was always literally true was a noisy leftover from an earlier shape of this branch. Collapse it to a plain Option<slot> lookup.
- escape_news.rs: clarify why the trailing `_ => {}` arm is kept after
the explicit Expr-variant list — new allocating variants should be
added explicitly above rather than relying on the fallthrough.
- json/parser.rs: explain the 96-bytes/object pre-size heuristic for
arrays-of-objects (empirical average for small JSON objects with a
handful of short string keys), plus the 16..16_384 clamp rationale.
) `crates/perry-codegen/src/collectors/escape_news.rs` was 2034 lines and failed the 2,000-line file-size CI gate. Splitting it into topical sibling modules under `collectors/` — no behavior change; function bodies move verbatim and the same public/internal API is re-exported from `collectors/mod.rs`. Moves: - `this_as_value.rs` — `class_uses_this_as_value`, `class_chain_extends_builtin_error`, `stmts_use_this_as_value`, `expr_uses_this_as_value`. - `class_accessors.rs` — `is_class_getter`, `is_class_setter`. - `escape_check.rs` — `find_new_candidates`, `check_escapes_in_stmts`, `check_escapes_in_expr`. - `local_refs.rs` — `expr_contains_local_get`, `mark_all_candidate_refs_in_expr`. `escape_news.rs` keeps the escape-collection entry points (`collect_non_escaping_news`, `collect_non_escaping_new_used_fields` and the two private helpers) plus the trailing `MAX_SCALAR_ARRAY_LEN` const. Final line counts: escape_news 693, escape_check 870, this_as_value 337, local_refs 114, class_accessors 50 — all comfortably under the 2,000 gate.
dc4571f to
328ec5d
Compare
…sweep (#1414) Rolls up 26 PRs that merged to main post-v0.5.1023 without version bumps: - node:crypto gap-fixes (#1386 #1393 #1394 #1402 #1405): randomInt, timingSafeEqual, getHashes/getCiphers, sha224/sha384, base64 digest, Buffer hash input, no-arg digest() → Buffer, pbkdf2Sync digest arg, scryptSync. - node:perf_hooks (#1321 + #1328 #1342 coverage): performance + User Timing + PerformanceObserver native impl, granular node-suite + edge-case coverage. - #1090 GC checkpoint runtime work (#1324). - #1311 geisterhand on iOS (#1316 #1383 #1384 #1385). - #1312 process.env.X (unset) is nullish undefined (#1314). - #1319 thread-safety hardening for cross-thread runtime statics. - #1322 exact-head GC evidence packet. - #1323 wasm timers dispatch through mem_call bridge (#1329). - #1317 node:timers/promises shadow-segfault fix (#1326). - #1330 node:process suite (#1331). - #1292 bcrypt.hash() returns String (#1307). - #1293 fastify .json()/.body external-fastify dispatch (#1308). - #1296 app pattern performance gaps. - #1297 diagnostics_channel parity. - #1301 iOS App Groups capability (#1313). - #1318 #1325 os/methods/modern-methods static dispatch. - #1315 expanded Node parity test coverage. - #1382 ui-ios stdlib pump for async fetch. - #1392 ui-wasm reactive state + setText (#1404).
Summary
This PR closes the slow app-pattern performance gaps found during the benchmark audit. It adds targeted runtime and codegen optimizations for JSON parsing, scalar-replaced object literals, discarded
Array.mapwork,Promise.all, async-step microtasks, buffer transcode allocation pressure, and array/string hot paths.What changed
JSON.parsepaths with hot shape reuse, parse-owned fast stores, key-cache improvements, lazy tape selection, stack-backed small object fields, and decimal fast paths.Array.mapresults and skips pure discarded map callbacks that only build unused anonymous objects.Promise.allby replacing per-input closures with a directTask::PromiseAllpath and GC-scanned state.Before / after
Initial focused baseline:
json_parse_1mbobject_deep_clonepromise_all_chainsbuffer_transcodeFull app-pattern matrix after this PR:
buffer_transcodedate_format_parsejson_parse_1mbjson_stringify_1mbmap_1mobject_deep_clonepromise_all_chainsregex_replacestring_concat_csvstring_split_map_joinstring_template_interpNo app-pattern kernel remains in the
slowbucket.Remaining gaps
promise_all_chainsis still borderline; the remaining cost appears to be async state-machine and microtask overhead rather thanPromise.allitself.string_concat_csvis just over the OK threshold and should be a focused follow-up for mixed string concat chains.date_format_parsestill reports a tiny checksum mismatch against Bun/Node; that is a correctness follow-up, not a performance blocker for this PR.Validation