Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions changelog.d/9214-decl-prototype-reverse-index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
**"Which class's `.prototype` is this object?" is now O(1)** (#9180). Every
`Object.defineProperty`, `Object.getOwnPropertyDescriptor` and `delete` asked
`class_id_for_decl_prototype_object` about its receiver, and the answer was a
linear scan of every materialized declared-class prototype. The comment above
it explained that the table was small and the path was a cold reflection path;
a bundled application falsifies both — esbuild's `__export(exports, { … })`
runs `defineProperty` thousands of times during module init, the receivers are
never prototypes, and a miss walked the whole table. It was 3.10% of
`cc --help`.

The registry now carries its own inverse. `CLASS_DECL_PROTOTYPE_OBJECTS` holds
a `DeclPrototypeTable` whose two maps are private to one file, so the six
existing mutation sites — the store, both GC root scanners, the per-slot GC
step, the test reset and the test seeds — go through methods that update both
directions together, and a seventh cannot be written without editing that file.
That matters more than it sounds: an earlier pointer-keyed cache with
hand-placed invalidation went stale at the sites it missed, and the symptom was
not a crash but `getOwnPropertyDescriptor(C.prototype, "g")` quietly returning
`undefined` where node returns an accessor descriptor.

Two things keep it honest beyond privacy. The reverse map is an exact inverse
only while one address belongs to one class id and is never re-pointed;
`insert` is where either could break, checks both while inserting with one
extra reverse lookup, and on anything unusual abandons the index for good and
answers from the same authoritative forward-table scan as before. In a
`debug_assertions` build every reverse lookup is compared against that scan, so
the whole runtime test suite is checking the index rather than trusting it.

Measured on Linux with 400 declared-class prototypes materialized, per
operation on non-prototype receivers: `getOwnPropertyDescriptor` 420 → 200 ns
(2.10×), `defineProperty` 1585 → 1055 ns (1.50×), `delete` 2685 → 2445 ns. The
scan's signature was the slope — `getOwnPropertyDescriptor` cost 180/210/305/420
ns at 0/50/200/400 prototypes before, and is flat at ~200 ns after.
15 changes: 8 additions & 7 deletions crates/perry-runtime/src/object/class_gc_roots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ pub fn scan_class_inheritance_roots_mut(visitor: &mut crate::gc::RuntimeRootVisi
CLASS_DECL_PROTOTYPE_OBJECTS.with(|table| {
if let Ok(mut guard) = table.write() {
if let Some(map) = guard.as_mut() {
for ptr in map.values_mut() {
map.visit_root_slots(|ptr| {
visitor.visit_usize_slot(ptr);
}
});
}
}
});
Expand Down Expand Up @@ -85,9 +85,10 @@ pub(crate) fn test_seed_class_inheritance_roots(proto_cid: u32, proto_ptr: usize
#[cfg(test)]
pub(crate) fn test_seed_decl_class_prototype_root(class_id: u32, proto_ptr: usize) {
CLASS_DECL_PROTOTYPE_OBJECTS.with(|table| {
let mut guard = table.write().unwrap();
guard
.get_or_insert_with(std::collections::HashMap::new)
table
.write()
.unwrap()
.get_or_insert_with(Default::default)
.insert(class_id, proto_ptr);
});
}
Expand Down Expand Up @@ -122,7 +123,7 @@ pub(crate) fn test_decl_class_prototype_root(class_id: u32) -> usize {
.read()
.unwrap()
.as_ref()
.and_then(|m| m.get(&class_id).copied())
.and_then(|m| m.get(class_id))
.unwrap_or(0)
})
}
Expand All @@ -148,7 +149,7 @@ pub(crate) fn test_clear_class_inheritance_roots(proto_cid: u32, closure_cid: u3
});
CLASS_DECL_PROTOTYPE_OBJECTS.with(|table| {
if let Some(m) = table.write().unwrap().as_mut() {
m.remove(&proto_cid);
m.remove(proto_cid);
}
});
CLASS_PARENT_CLOSURES.with(|table| {
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/object/class_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ mod builtin_alias_construct;
mod class_meta;
mod construct;
pub(crate) use construct::scan_current_new_target_root_mut;
pub mod decl_prototype_table;
mod dispatch;
mod function_prototype;
mod gc_roots;
Expand Down
Loading
Loading