You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
perf: OBJ_FLAG_HAS_DESCRIPTORS is per-OBJECT, so one Object.defineProperty on one key takes every other key of that object off BOTH property ICs — 15.8x (126 -> 1991 instr/iteration, node 17.4 -> 20.8) #10871
OBJ_FLAG_HAS_DESCRIPTORS is a per-object header bit, and both property inline caches refuse any receiver that carries it — the read PIC (property_get/generic_dispatch.rs, bit 11 of _reserved) and the write PIC (proxy_reflect.rs, bit 11 of WRITE_PIC_BLOCKING_FLAGS = 0x1987). So one Object.defineProperty on ONE key takes every OTHER key of that object off both fast paths, permanently.
Measured (v0.5.1628, perf stat -e instructions:u, min of 3, per-iteration fitted between N=500k and N=5M, perry flat across the range, output byte-identical to node)
Two fixtures differing by one line. Loop body is O.a = k; h += O.a; in both — a is an ordinary writable data property in both, and is never the key the descriptor is installed on.
own_descriptors_skip_key at 75 per store is the tell: the runtime does ask per key whether the key being stored has a descriptor, and answers "no" every time — 200 000 times — while the emitted guard has already refused on the per-object bit before reaching it.
Why this is worth fixing rather than documenting
This is the pattern real code has. A library that calls Object.defineProperty(exports, '__esModule', {value: true}), or installs one non-enumerable name/length/toJSON, or freezes a single constant on a config object, makes every ordinary property of that object 15.8× dearer for the life of the process. The object does not have to be exotic in any way a reader would notice, and there is no diagnostic that points at it.
Design OBJECT_MODEL_SINGLE_PATH_DESIGN_2026-09-20.md §3 already names the fix: per-key attributes belong in the shape, not in an address-keyed side table with a per-object "somewhere in here there is a descriptor" bit. With per-key attributes in the shape, a shape match proves the stored key is a plain writable data property, and the sibling descriptor costs the object nothing. §8 step 3 has it as work; this issue is the measurement that says what it is worth.
Interim, much cheaper option if step 3 is far off: make the bit mean "this object has a descriptor on a key the site has not primed" by consulting the shape's descriptor key set at prime time and letting the emitted guard trust the ShapeId — i.e. move the per-key question to the miss, which runs once, from the hit, which runs 200 000 times. That needs #10824's guarantee that every descriptor install transitions the shape, which landed.
Bucket row 10, accessor / property-descriptor handling, is 3.3% of equal-weight excess. It is ≥5% in 6 packages. Share of each package's excess: commander 8, node-forge 7, decimal.js 7, date-fns 6, dayjs 6, axios 5.
What the bucket actually contains, by entry → dominant leaf:
workload
entry → leaf
instr/iter
% of excess
node-forge/hmac · aes_cbc · sha256
dispatcher → descriptor_state::get_accessor_descriptor (72–87% of the chain)
So most of row 10 is the address-keyed descriptor table being consulted on ordinary objects and method dispatch. That is #10871's mechanism. Class prototype getters, which are #10498's mechanism, show up in row 1 instead (property lookup): cron/luxon enters 48% of its instructions through getter reads (DateTime.isValid, zone); the evidence is posted on #10498.
Build caveat: the node-forge binaries (2febf42) predate #11411. The other packages were profiled at 36420d2, which includes #11411 but predates #11416.
What landed since the issue was filed
#11411 (merged 09-26), property attributes live with the keys, is the "per-key attributes in the shape" fix this issue asked for:
A keys array with non-default attributes points to a parallel attributes array.
IC primes are per key: a read IC declines only an accessor key, and a write IC declines only a key that is not plain writable.
The emitted write-PIC blocking mask went from 0x1987 to 0x1180, so bit 11 (OBJ_FLAG_HAS_DESCRIPTORS, 0x800) no longer refuses the receiver.
It does not reference this issue.
Reproducer, fresh numbers (the issue's fixture, instructions per iteration)
The 15.8× cliff is gone. An unrelated descriptor still costs 44 instructions per O.a = k; h += O.a pair, and neither side of that pair touches z. I have not profiled where the 44 go.
Acceptance target
descwithin 1.1× of plain (the unrelated descriptor free), and both ≤ 2× Node.
Activity
proggeramlug commentedon Sep 30, 2026
Current target (2026-09-30): largely fixed by #11411 (1,991 → 75 instr/iteration); a residual +44 remains
Package impact (#11464)
Bucket row 10, accessor / property-descriptor handling, is 3.3% of equal-weight excess. It is ≥5% in 6 packages. Share of each package's excess: commander 8, node-forge 7, decimal.js 7, date-fns 6, dayjs 6, axios 5.
What the bucket actually contains, by entry → dominant leaf:
descriptor_state::get_accessor_descriptor(72–87% of the chain)js_class_field_add→js_object_define_property(Commandconstructor)js_date_apply_setterget_accessor_descriptorjs_put_value_setjs_put_value_set→get_accessor_descriptor(big.mjs:393)So most of row 10 is the address-keyed descriptor table being consulted on ordinary objects and method dispatch. That is #10871's mechanism. Class prototype getters, which are #10498's mechanism, show up in row 1 instead (property lookup): cron/luxon enters 48% of its instructions through getter reads (
DateTime.isValid,zone); the evidence is posted on #10498.Build caveat: the node-forge binaries (2febf42) predate #11411. The other packages were profiled at 36420d2, which includes #11411 but predates #11416.
What landed since the issue was filed
#11411 (merged 09-26), property attributes live with the keys, is the "per-key attributes in the shape" fix this issue asked for:
0x1987to0x1180, so bit 11 (OBJ_FLAG_HAS_DESCRIPTORS,0x800) no longer refuses the receiver.It does not reference this issue.
Reproducer, fresh numbers (the issue's fixture, instructions per iteration)
z, never touchedThe 15.8× cliff is gone. An unrelated descriptor still costs 44 instructions per
O.a = k; h += O.apair, and neither side of that pair touchesz. I have not profiled where the 44 go.Acceptance target
descwithin 1.1× ofplain(the unrelated descriptor free), and both ≤ 2× Node.