Summary
is_registered_class_prototype_object (class_registry/parent_static.rs:1742) answers "is this object some class's registered prototype?" with map.values().any(…) over CLASS_PROTOTYPE_OBJECTS. It sits in the || term immediately before the scan #9214 replaces, in the same predicate — so with #9214 merged, this becomes the remaining half of the same cost.
Measured on the fixed (#9214) build, defineProperty against non-prototype receivers:
| synthetic prototypes |
0 |
100 |
400 |
| ns |
1035 |
1090 |
1265 |
+0.575 ns/entry — essentially the slope #9214 removed, still there. It also explains the residual delete slope (+0.657 ns/entry) that survives #9214.
Why the table grows
CLASS_PROTOTYPE_OBJECTS mints a synthetic class id per distinct F.prototype = obj (prototype_objects.rs:293) — i.e. one per ES5-transpiled constructor in a bundle. Any downlevelled dependency graph populates it linearly with its own size, which is exactly the regime where an O(n) scan per property operation stops being free.
Why it is NOT the same fix as #9214
#9214's table can use abandon-on-repoint: its forward map is injective and never re-pointed, so the one place that could break the inverse (insert) checks it and permanently falls back to the authoritative scan.
This table IS re-pointed — prototype_objects.rs:272 and object_ops/prototype.rs:117 both rewrite an existing entry. So an inverse here needs rebuild-on-repoint, not abandon-on-repoint, or it degrades to the scan the first time any program reassigns a .prototype — which is the common case rather than the rare one.
Everything else from #9214 transfers directly, and should be reused rather than reinvented: privacy so no seventh writer can exist, the degraded mode being the pre-existing scan verbatim, the debug_assert_eq! against that scan on every lookup, and the GC visit helpers that re-key from what the visitor left behind.
A false comment to delete while in here
disable_inline_guards_for_descriptor_target documents itself as: descriptor installs are rare and "never on the hot property path, so the scan cost is acceptable".
esbuild's __export(exports, {…}) falsifies that for every bundle — claude-code's has 1,526 Object.defineProperty occurrences. The comment is the reason the scan was left in place, and it should not outlive the premise.
Instrument
The slope table above is the test: a fix must flatten ns-per-op against synthetic-prototype count, not merely lower the constant. #9214's differential harness and its sabotage check are directly reusable.
Summary
is_registered_class_prototype_object(class_registry/parent_static.rs:1742) answers "is this object some class's registered prototype?" withmap.values().any(…)overCLASS_PROTOTYPE_OBJECTS. It sits in the||term immediately before the scan #9214 replaces, in the same predicate — so with #9214 merged, this becomes the remaining half of the same cost.Measured on the fixed (#9214) build,
definePropertyagainst non-prototype receivers:+0.575 ns/entry — essentially the slope #9214 removed, still there. It also explains the residual
deleteslope (+0.657 ns/entry) that survives #9214.Why the table grows
CLASS_PROTOTYPE_OBJECTSmints a synthetic class id per distinctF.prototype = obj(prototype_objects.rs:293) — i.e. one per ES5-transpiled constructor in a bundle. Any downlevelled dependency graph populates it linearly with its own size, which is exactly the regime where an O(n) scan per property operation stops being free.Why it is NOT the same fix as #9214
#9214's table can use abandon-on-repoint: its forward map is injective and never re-pointed, so the one place that could break the inverse (
insert) checks it and permanently falls back to the authoritative scan.This table IS re-pointed —
prototype_objects.rs:272andobject_ops/prototype.rs:117both rewrite an existing entry. So an inverse here needs rebuild-on-repoint, not abandon-on-repoint, or it degrades to the scan the first time any program reassigns a.prototype— which is the common case rather than the rare one.Everything else from #9214 transfers directly, and should be reused rather than reinvented: privacy so no seventh writer can exist, the degraded mode being the pre-existing scan verbatim, the
debug_assert_eq!against that scan on every lookup, and the GC visit helpers that re-key from what the visitor left behind.A false comment to delete while in here
disable_inline_guards_for_descriptor_targetdocuments itself as: descriptor installs are rare and "never on the hot property path, so the scan cost is acceptable".esbuild's
__export(exports, {…})falsifies that for every bundle — claude-code's has 1,526Object.definePropertyoccurrences. The comment is the reason the scan was left in place, and it should not outlive the premise.Instrument
The slope table above is the test: a fix must flatten ns-per-op against synthetic-prototype count, not merely lower the constant. #9214's differential harness and its sabotage check are directly reusable.