perf(runtime): an Object.create birth is allocated as wide as its birth shape's descendants grow (#10905) - #11566
Conversation
…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
|
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 configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (7)
Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review. 📝 WalkthroughWalkthrough
ChangesAdaptive birth width
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
Merge Risk: ⚪ Minimal · up to The birth-width change is mergeable after normal checks; no actionable correctness or availability risk remains established. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to 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
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
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, theObject.createcolumn was 3–6x a literal receiver's on every operation.Fix: slack tracking as a fact of the birth shape
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.(P, [], width), the same (keys, width) birth-shape idea perf(codegen,runtime): key-adding stores are served inline from a per-site shape memo; construction is pre-sized #11360 uses for classes.Evidence
Acceptance matrix (release build, instructions per op, range across the 7 receiver provenances):
Object.createbeforeObject.createafterEvery cell is flat, valid and output-identical to node. The literal addkey cells drop 7 because they touch the changed spill store.
Real code:
Object.createcalls (verified with a counting build), so their A/B measures only the fix's overhead:Tests:
object::shapes_birth_width_testsadds 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.--test-threads=1: 4629 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.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 inprototype.rs.Not covered
Object.create(null).{}receivers that grow past their birth keys. That is a separate problem, and this change deliberately leaves it alone.spill.rsis a side table that could later move onto this same shape fact.Summary by CodeRabbit
Object.create(proto)performance by adapting object allocation to the fields commonly added by descendants, reducing overflow storage for those fields.