Skip to content

perf(runtime): an Object.create birth is allocated as wide as its birth shape's descendants grow (#10905) - #11566

Merged
proggeramlug merged 5 commits into
mainfrom
perf-ocreate-birth-width
Sep 27, 2026
Merged

proggeramlug merged 5 commits into
mainfrom
perf-ocreate-birth-width

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Closes #10905

Problem

Object.create(P) allocated every result with 2 inline slots (js_object_alloc(0, 0)). Any object that grew past 2 keys spilled to an overflow array, and every later read and write of those keys paid for the spill. In the acceptance matrix, the Object.create column was 3–6x a literal receiver's on every operation.

Fix: slack tracking as a fact of the birth shape

  • Where the fact lives: Object.create(P) is born on the keyless shape (P, []). That shape record now also holds how wide its descendants grow, and a birth counter for the learning phase. Both live in bits of its word that were already reserved, so the record costs no extra bytes and nothing is kept in a side table.
  • How it learns:
    • Every shape minted below the birth shape raises the width to its key count. Each such shape is minted exactly once, so the width is the true maximum over all descendants.
    • A descendant that spills also raises it, which covers shapes minted before the record existed.
  • How births use it:
  • What doesn't change: reads and writes never consult the width. Objects that already exist keep spilling as before.
  • GC lifetime: a birth shape that served a birth during a GC epoch survives the full collection. A prototype that goes a whole epoch without births forgets its width and relearns it.

Evidence

Acceptance matrix (release build, instructions per op, range across the 7 receiver provenances):

op Object.create before Object.create after literal (control)
overwrite 264–334 48–120 identical
read1 295–369 79–151 identical
read4 710–810 234–326 identical
addkey 452–528 79–151 452–528 → 445–521
inherited 525–598 311–380 –

Every cell is flat, valid and output-identical to node. The literal addkey cells drop 7 because they touch the changed spill store.

Real code:

  • tsc and Zod make zero Object.create calls (verified with a counting build), so their A/B measures only the fix's overhead:
    • Zod: instructions +0.03%.
    • tsc: instructions −0.3%, inside tsc's ±2.7% noise floor.
  • Peak RSS is within run-to-run spread on both, though the tsc runs were on a loaded host.
  • Measured n=5 interleaved, each arm linking its own runtime.

Tests:

  • object::shapes_birth_width_tests adds 3 tests. With the wider allocation switched off, all 3 fail. With mint-time learning switched off, 2 fail; the third still passes because spills teach the record too.
  • Runtime suite, --test-threads=1: 4629 passed, 0 failed.
  • Codegen suite: 2266 passed, 0 failed.
  • cargo fmt --check: clean.
  • scripts/run_lint_gates.sh: 98 of 102 pass. The failures are the Windows xwin check (not installed on the build host) and public-benchmark freshness, which fail on base too, plus the API-docs step, which only failed on the host's target-dir layout and passes when rerun with the expected layout.
  • GC root-dominance: the self-test and all audits are green.
  • Gap suite (node 26.5.1): all 1064 ids compared against the snapshot by id, 0 differences.

These results were measured on the pre-merge base (9d5a014cd). This branch merges current main; the only conflict was a line next to a main-side change in prototype.rs.

Not covered

  • Object.create(null).
  • Literal and {} receivers that grow past their birth keys. That is a separate problem, and this change deliberately leaves it alone.
  • The per-class learned-width table in spill.rs is a side table that could later move onto this same shape fact.

Summary by CodeRabbit

  • Performance
    • Improved Object.create(proto) performance by adapting object allocation to the fields commonly added by descendants, reducing overflow storage for those fields.
    • Objects with no observed growth retain the existing allocation behavior; reads and writes work as before.

Ralph Küpper added 5 commits September 27, 2026 03:42
…s publish its shape

Since #11166 an Object.create result is class-less (class_id 0) and nothing
marked it ordinary, so the static-key store site's receiver-kind test refused
it: every o.k = v on such a receiver missed the site cache and took the full
[[Set]] walk through js_put_value_set_packed_miss, even for an own data
property. The acceptance matrix's ocreate column moved ~1,300 instr/op
(overwrite 293 -> 1,588).

OrdinaryObjectCreate yields an ordinary object whose [[Prototype]] is a fact
of its shape (#11342), so it is born ordinary like the other ordinary birth
sites, and the store site publishes its ShapeId as for a literal or a class
instance. Regression test: the site word is primed from an Object.create
receiver and the emitted hit's receiver-kind half admits it (fails with the
mark removed).
…th shape's descendants grow (#10905)

Object.create(P) allocated its result at the two-slot floor, so the third own
field and every later one lived in overflow storage for the object's whole
life: ~220 instructions per spilled store and ~100 per spilled read over an
inline one.

In-object slack tracking, as a fact of the birth shape. The keyless shape
(P, []) that every Object.create(P) result is born on records, in bits its
record word already reserved, how wide its descendants grow (the largest key
count of any shape minted below it, raised by any descendant that spills) and
how many births it served while tracking. The first 8 births get 8 slots (or
the learned width, if larger); later births get exactly the learned width,
capped at 64, and the old floor when nothing grew. The width is capacity only
and is stamped as the birth shape (P, [], width), like a class born wide
(#11360). Reads and writes never consult it. The record survives a full
prune when a birth asked it during the epoch.
# Conflicts:
#	crates/perry-runtime/src/object/object_ops/prototype.rs
@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: e01f194e-51ea-42ee-8f00-6b9119beb74d

📥 Commits

Reviewing files that changed from the base of the PR and between 62ab592 and 452845b.

📒 Files selected for processing (7)
  • changelog.d/11566-ocreate-birth-width.md
  • crates/perry-runtime/src/object/object_ops/prototype.rs
  • crates/perry-runtime/src/object/shapes.rs
  • crates/perry-runtime/src/object/shapes_birth_width.rs
  • crates/perry-runtime/src/object/shapes_birth_width_tests.rs
  • crates/perry-runtime/src/object/shapes_store.rs
  • crates/perry-runtime/src/object/spill.rs

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

Object.create(proto) now allocates objects using width tracked for the prototype’s keyless birth shape. Descendant shape creation and spills update the learned width. Shape records retain tracking counts and learned-width data, and tests cover tracking, spills, and polymorphic descendant growth.

Changes

Adaptive birth width

Layer / File(s) Summary
Birth-width record metadata
crates/perry-runtime/src/object/shapes_store.rs
Shape records store tracked-birth counts, learned descendant width, and a birth-owner flag in existing packed fields. Accessors cap tracked births at 63 and learned width at 255.
Descendant-width learning and record retention
crates/perry-runtime/src/object/shapes_birth_width.rs, crates/perry-runtime/src/object/shapes.rs, crates/perry-runtime/src/object/spill.rs
Ordinary generation-zero descendant shapes and qualifying spills report widths for prototype birth records. Full-trace pruning retains birth records marked as owners.
Birth allocation and validation
crates/perry-runtime/src/object/object_ops/prototype.rs, crates/perry-runtime/src/object/shapes_birth_width_tests.rs, changelog.d/11566-ocreate-birth-width.md
Object.create uses the width returned for the prototype’s birth shape. Tests cover the tracking window, learned width, spills, and the width cap. The changelog reports instruction-count comparisons.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant js_object_create
  participant keyless_birth_width
  participant ShapeTable
  participant Shape_indexing
  participant note_learned_inline_fields
  participant note_spill_width
  js_object_create->>keyless_birth_width: request width for prototype
  keyless_birth_width->>ShapeTable: read or ensure keyless birth record
  ShapeTable-->>keyless_birth_width: tracked births and learned width
  keyless_birth_width-->>js_object_create: allocation width
  Shape_indexing->>ShapeTable: record descendant width
  note_learned_inline_fields->>note_spill_width: report required spill width
  note_spill_width->>ShapeTable: update learned width
Loading

Merge Risk: ⚪ Minimal · up to 45284

The birth-width change is mergeable after normal checks; no actionable correctness or availability risk remains established.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 45284

Allocation history can increase memory used by later objects sharing a prototype. The increase is capped per object, but its workload-wide impact and exposure to untrusted code are not established.

Retained concerns

  • Low · security · inferred: If untrusted code can grow descendants of a prototype used by other callers, later Object.create births using that prototype can consume more memory than before. The width is capped per object, but repeated births can compound the cost; cross-trust sharing and practical impact are unverified.
Security review details

Security Blast Radius

  • inferred — A caller able to grow descendants of a shared prototype can affect later Object.create allocation widths for that prototype. The inspected lookup does not transfer learned width to a different prototype.

Security Findings and Attack Paths

  • inferred — Where untrusted code and other callers share a prototype, descendant growth followed by repeated births could amplify allocation pressure. No deployment-level trust boundary or demonstrated exhaustion path was supplied.

Trust Boundaries and Controls

  • observed — The entrypoint validates its prototype before marking it; learning requires prototype-specific shape facts, and every allocated slot is initialized to undefined before shape publication.

Resilience and Maintainability Implications

  • inferred — Epoch-based ownership limits retention of inactive birth records, while repeated births can renew retention. The resulting aggregate lifetime under adversarial allocation was not measured.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed For directly linked issue [#10905], the PR targets the measured own-property cost for growing Object.create(PROTO) receivers. It allocates keyless births using learned descendant width, tracks spill…
Out of Scope Changes check ✅ Passed The changed files implement Object.create birth-width tracking, shape-record storage and pruning behavior, spill reporting, and focused runtime tests. These changes support the performance objective…
Docstring Coverage ✅ Passed Docstring coverage is 84.62% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 26 functions across 6 files. (1 skipped: 1 …
Title check ✅ Passed The title clearly and concisely identifies the main performance change: allocating Object.create births according to descendant growth.
Description check ✅ Passed The description is detailed and covers the problem, implementation, scope, linked issue, evidence, tests, and known limitations. It does not use the template headings and omits the checklist, but the …
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug merged commit 27fd6ad into main Sep 27, 2026
55 of 57 checks passed
@proggeramlug
proggeramlug deleted the perf-ocreate-birth-width branch September 27, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant