Skip to content

perf(runtime): an ordinary object's prototype is a fact of its shape; new F() allocates no ObjectMeta - #11815

Merged
proggeramlug merged 2 commits into
mainfrom
perf-prototype-in-shape
Oct 3, 2026
Merged

proggeramlug merged 2 commits into
mainfrom
perf-prototype-in-shape

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

protoshape phase 1: prototype in the shape (ad87de06e, on origin/main 6820067)

Bundle: bundles/protoshape.bundle. Head: refs/heads/protoshape-phase1 = ad87de06e. Prerequisite:
6820067. Message: Refs #10507. Design: protoshape/DESIGN.md.

What it does

  • Every prototype identity a ShapeId names (serial, MIXED, UNIQUE) has one word holding the
    prototype. The words live in stable per-agent pages behind a thread-local directory. The
    ShapeRecord stays 64 bytes.
  • A function-constructor instance (new F()) carries no ObjectMeta. It reads its prototype from
    its ShapeId.
  • Receivers that have a meta record anyway (link flags, PER_OBJECT) keep the same bits there.
  • GC: the word is a carrier edge (a full trace emits it once per identity). Minors root young
    words. Rewrite passes repair every word. A dead-owner prune clears a word whose prototype died.
  • Fixes: Object.create(null) instanceof Object, Object.create(fn) instanceof Function, and the
    null-prototype shape identity of a re-parented Object.create(null) object.
  • Test repair: test_object_meta_null_prototype_survives_full_gc_on_live_owner never pushed a
    shadow frame, so it passed on freed memory.

Verification on the rebased tree (vs main 6820067, qb6)

  • Runtime tests (RUST_TEST_THREADS=1, --release): 4839 passed, 0 failed.
  • Gap tests matching proto|class|instanceof|create|new|inherit|symbol|super: 240 tests.
    • Main passes 236, the branch 239. No regressions. Fixed: test_gap_prototype_in_shape (new),
      3527 and 10454.
    • Under PERRY_GC_FORCE_EVACUATE + VERIFY_EVACUATION + MOVING_LOOP_POLLS: 0 failures.
  • Seeded GC with from-space protection, evacuation and the verifier (seeds 1, 2, 3, 7): MATCH for
    test_gap_prototype_in_shape and test_gap_10507.
  • fmt is clean. gc_call_effects --check: identical (3991 symbols).
  • Root holders: same 6 rule-T frontier lines as main, none from this lane. The PASS1_MARKED pin
    was re-audited and re-pinned for the gc/mod.rs scanner registration.
  • check_file_size fails only on main's own codegen/string_pool.rs (2068 lines), which this lane
    does not touch.
  • The codegen tests and the remaining seeds were still running when this was written. Codegen
    passed 2430/0 in the pre-rebase run. The rebased results land in logs/v3.SUMMARY on qb6.

Verification on the pre-rebase tree (main + #11787 + #11784, full batch)

  • Runtime 4831/0 and codegen 2430/0.
  • Gap subset: 0 regressions.
  • gc-root-dominance corpus: 222/222 compiled. The checker reports the same 15 findings on base
    and branch (regexp receiver rooting, pre-existing).
  • Workers: test_issue_protoshape_thread_prototypes (8 agents plus main) is correct, also under
    forced evacuation and the verifier.
  • Sabotage (drop proto_id from the shape key and the facts match):
    • 3 unit tests go RED: receivers_with_different_prototypes_never_share_a_shape,
      a_class_default_link_records_the_prototype_in_the_shape_only,
      a_null_prototype_is_a_shape_fact.
    • The gap test goes RED (fn 5 new new new new true true).
    • Restored, and rebuilt clean.

Numbers (pre-rebase final batch; instructions:u medians, n=5)

Row base protoshape delta
new F() (fnctor new_fn_empty, per op) 1,898 1,171 -38%
decimal.js-shaped x.plus(i) (per op) 5,034 4,355 -13%
new KE() class / ke instanceof KE 281 / 175 281 / 179 flat
#10498 accessor rows (6) 644 / 420 / 218 / 57 / 312 / 80 644 / 420 / 219 / 57 / 312 / 79 flat
tsc transpileModule x1 (fulls) 63.378 G (81) 63.055 G (82) -0.51%
Zod x200 1.0311 G 1.0365 G +0.52%
qs parse_nested 34.284 G 34.421 G +0.40%
commander parse_argv 18.779 G 18.904 G +0.66%

Refs #10507. Phase 2 (deleting the PROTO_DIVERGED / USER_PROTO_OVERRIDE / CLASS_EVALUATION_PROTO flags and moving the class-id shortcuts onto shapes) follows in a separate PR.

Ralph Küpper added 2 commits October 3, 2026 16:11
A ShapeId already names its receivers' prototype identity (`proto_id`: a
prototype serial, MIXED, UNIQUE, NULL, CLASS or PER_OBJECT), but every reader
went back to the per-instance `ObjectMeta.prototype` for the object itself, so
each `new F()` instance allocated a 152-byte meta record only to hold it.

The way back from the identity to the object is now one word per prototype
identity (object/shapes_prototype.rs): stable pages indexed by serial (plain
and MIXED identities share it) and by UNIQUE number, owned by the agent's
shape slab and published to a thread-local directory. The ShapeRecord stays
64 bytes.

- Writer: the one prototype funnel (`object_set_static_prototype_impl`)
  computes the identity, writes its word, and moves the receiver to the shape
  with that identity. A class-default link on a meta-less function-constructor
  instance (synthetic class) allocates no meta record; any other receiver
  already has one (link flags, PER_OBJECT identity) and gets the same bits
  there. #10507's birth replay no longer allocates meta and validates the
  birth ShapeId's facts instead of pinning the id.
- Readers: `shapes::object_prototype_word` (meta word if recorded, else the
  identity's word for a synthetic-class receiver, else nothing; null from the
  identity, born-null cells unchanged) behind `object_static_prototype`,
  `object_proto_id`, native_get, method_site, prop_plan, timer handles, the
  typed-feedback guard and the setter site.
- GC: the identity word is a traced edge of its carriers, beside the keys
  edge, in the generic walk and the copying drain's plain-object plan; a full
  trace emits it once per identity. A minor also roots every word naming a
  young object; rewrite passes repair every word; a word whose prototype died
  is cleared by a dead-owner prune. Links shade the prototype for an
  incremental mark. Worker seeds skip identities with a word.
- Fixes on the way: `Object.create(null) instanceof Object` was true; a class
  instance re-parented with setPrototypeOf stayed `instanceof` its class;
  `Object.create(fn) instanceof Function` was false; a re-parented
  `Object.create(null)` object kept a null-prototype shape identity.
- Test repair: test_object_meta_null_prototype_survives_full_gc_on_live_owner
  never pushed a shadow frame, so its owner was not rooted and it read freed
  memory.

`new F()` 1,898 -> 1,165 instructions per construction.

Refs #10507
@proggeramlug
proggeramlug merged commit f9a65b4 into main Oct 3, 2026
23 of 24 checks passed
@proggeramlug
proggeramlug deleted the perf-prototype-in-shape branch October 3, 2026 14:12
@coderabbitai

coderabbitai Bot commented Oct 3, 2026

Copy link
Copy Markdown

Review in Change Stack →

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

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a14cbd4b-f370-4fac-8e32-5422005698a7
📥 Commits

Reviewing files that changed from the base of the PR and between 2572d26 and e450e48.

📒 Files selected for processing (31)
  • changelog.d/PENDING-prototype-in-shape.md
  • crates/perry-runtime/src/gc/barrier_store.rs
  • crates/perry-runtime/src/gc/copying_object_scan.rs
  • crates/perry-runtime/src/gc/dead_owner.rs
  • crates/perry-runtime/src/gc/full_trace.rs
  • crates/perry-runtime/src/gc/layout_slot_visit.rs
  • crates/perry-runtime/src/gc/mod.rs
  • crates/perry-runtime/src/gc/tests/dead_owner_side_tables/meta_and_shape_records.rs
  • crates/perry-runtime/src/object/class_registry/construct/compiled_function.rs
  • crates/perry-runtime/src/object/class_registry/construct/compiled_function_tests.rs
  • crates/perry-runtime/src/object/gc_slots.rs
  • crates/perry-runtime/src/object/instanceof/static_dispatch.rs
  • crates/perry-runtime/src/object/method_site.rs
  • crates/perry-runtime/src/object/mod.rs
  • crates/perry-runtime/src/object/native_get.rs
  • crates/perry-runtime/src/object/prop_plan.rs
  • crates/perry-runtime/src/object/proto_validity.rs
  • crates/perry-runtime/src/object/prototype_chain.rs
  • crates/perry-runtime/src/object/shapes.rs
  • crates/perry-runtime/src/object/shapes_prototype.rs
  • crates/perry-runtime/src/object/shapes_prototype_tests.rs
  • crates/perry-runtime/src/object/shapes_slot_list.rs
  • crates/perry-runtime/src/object/shapes_store.rs
  • crates/perry-runtime/src/object/shapes_store_tests.rs
  • crates/perry-runtime/src/object/shapes_worker_seed.rs
  • crates/perry-runtime/src/proxy/put_value/setter_site.rs
  • crates/perry-runtime/src/timer/handle_object.rs
  • crates/perry-runtime/src/typed_feedback/guards.rs
  • scripts/gc_runtime_root_holders.json
  • test-files/test_gap_prototype_in_shape.ts
  • test-files/test_issue_protoshape_thread_prototypes.ts
 _____________________________________
< I don't chase bugs; they surrender. >
 -------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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 pushed a commit that referenced this pull request Oct 3, 2026
… prototype

With the prototype a fact of the shape (#11815), every reader of
OBJECT_META_FLAG_PROTO_DIVERGED was a cache gate whose key already carries
the prototype, so the per-instance flag only duplicated the ShapeId:

- store plans (field_set_by_name, its tail, the proxy set path) are keyed by
  (class id, key, receiver prototype bits);
- array-subclass dense layouts (array/subclass.rs, subclass_loop_guard.rs)
  are keyed by (class id, ShapeId) and validated by the receiver word, which
  carries the ShapeId;
- the defineProperty key-add tail (keys_array.rs) replays ShapeId-keyed
  transition edges;
- `x instanceof <class object>` (instanceof/dynamic_dispatch.rs) now asks
  whether the receiver has a recorded prototype instead.

The flag, its funnel write, the class-object-template set/clear and its
tests are deleted; bit 0 of ObjectMeta.flags is free. A runtime-wiring link
on a function-constructor instance no longer allocates a meta record.

Tests: `new F()` and `Object.create(F.prototype)` receivers share one shape;
the dense array-subclass test now asserts the receiver moves to another
ShapeId across a prototype override and its own element still reads the same
(it asserted the old per-instance decline).

Refs #10507
proggeramlug pushed a commit that referenced this pull request Oct 3, 2026
…s.json from #11815

Keeps main's PASS1_MARKED audit, appends the prototype-in-shape re-audit
and pins gc/mod.rs at its current hash.
proggeramlug pushed a commit that referenced this pull request Oct 3, 2026
…s.json from #11815

Keeps main's PASS1_MARKED audit, appends the prototype-in-shape re-audit
and pins gc/mod.rs at its current hash.
proggeramlug pushed a commit that referenced this pull request Oct 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant