Summary
100% of shapes' measured self time is two runtime helpers — no user code appears at
all. shapes currently runs at 1.06x Node with an instruction ratio of 1.25, so this is
work being executed, not scheduling.
Profile evidence
Measured on 8bfdac447, symbols kept, aggregated over 40 runs (the program is short, ~89 ms):
67.7% value::addr_class::try_read_tracked_gc_header
32.3% object::shapes::shape_descriptor_by_id
That is the entire attributed profile. The benchmark's own functions never appear above the
sampling floor.
try_read_tracked_gc_header also shows up on churn (3.2%), so this is not confined to one row.
Why this looks addressable
shapes exercises polymorphic property access across many object shapes — exactly the path
these two helpers serve. Two questions worth answering with measurements:
shape_descriptor_by_id — is this a map lookup on every property access? A shape
descriptor is immutable once created, so a per-callsite inline cache (shape id -> descriptor
pointer, invalidated on shape transition) should turn most lookups into a compare-and-branch.
Check whether an inline cache already exists and is simply missing here, or whether the id
-> descriptor step is unavoidably dynamic.
try_read_tracked_gc_header — being the single largest frame in a property-access
benchmark suggests a validation read on a very hot path. Find out what it is guarding and
whether the check can be hoisted out of the access loop, or proven redundant when the
receiver was already validated earlier in the same operation.
Relevant background: crates/perry-runtime/src/value/addr_class.rs centralizes
handle-vs-pointer address classification (#4899), and CLAUDE.md notes that class instances
carry no separate shape word — the shape word is parent_class_id, valid only when
class_id == 0.
Where to look
crates/perry-runtime/src/value/addr_class.rs — try_read_tracked_gc_header
crates/perry-runtime/src/object/shapes.rs — shape_descriptor_by_id
- the property-get/set lowering in
crates/perry-codegen/src/expr/property_get/
Acceptance
shapes beats Node (< 1.00x), no other row regressed, all 19 byte-exact, RSS not increased.
How to build and measure (shared setup)
Build with the static wrappers or you link a stale runtime and both A/B arms behave
identically — a vacuous result:
cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
export PERRY_RUNTIME_DIR=<your target>/release
The benchmark corpus and Node-oracle expected output are staged at
/Users/amlug/projects/perry/sweep-artifacts-0819b/ (sources/*.ts, expected/*.stdout).
export PERRY_NO_AUTO_OPTIMIZE=1 PERRY_NO_CACHE=1
perry sources/<row>.ts -o /tmp/x --no-cache
cmp <(/tmp/x) expected/<row>.stdout # verify BEFORE timing
/usr/bin/time -l /tmp/x # instructions retired + peak RSS
Profiling: Perry strips its output binaries, so sample attributes everything to
???. Set PERRY_KEEP_SYMBOLS=1 PERRY_DEBUG_SYMBOLS=1 at compile time — the binary keeps
~1900 symbols and stays byte-exact — then read sample's "Sort by top of stack" section.
These programs are short; aggregate across ~14 runs for a usable sample count.
Constraints that apply to any fix here
Summary
100% of
shapes' measured self time is two runtime helpers — no user code appears atall.
shapescurrently runs at 1.06x Node with an instruction ratio of 1.25, so this iswork being executed, not scheduling.
Profile evidence
Measured on
8bfdac447, symbols kept, aggregated over 40 runs (the program is short, ~89 ms):That is the entire attributed profile. The benchmark's own functions never appear above the
sampling floor.
try_read_tracked_gc_headeralso shows up onchurn(3.2%), so this is not confined to one row.Why this looks addressable
shapesexercises polymorphic property access across many object shapes — exactly the paththese two helpers serve. Two questions worth answering with measurements:
shape_descriptor_by_id— is this a map lookup on every property access? A shapedescriptor is immutable once created, so a per-callsite inline cache (shape id -> descriptor
pointer, invalidated on shape transition) should turn most lookups into a compare-and-branch.
Check whether an inline cache already exists and is simply missing here, or whether the id
-> descriptor step is unavoidably dynamic.
try_read_tracked_gc_header— being the single largest frame in a property-accessbenchmark suggests a validation read on a very hot path. Find out what it is guarding and
whether the check can be hoisted out of the access loop, or proven redundant when the
receiver was already validated earlier in the same operation.
Relevant background:
crates/perry-runtime/src/value/addr_class.rscentralizeshandle-vs-pointer address classification (#4899), and CLAUDE.md notes that class instances
carry no separate shape word — the shape word is
parent_class_id, valid only whenclass_id == 0.Where to look
crates/perry-runtime/src/value/addr_class.rs—try_read_tracked_gc_headercrates/perry-runtime/src/object/shapes.rs—shape_descriptor_by_idcrates/perry-codegen/src/expr/property_get/Acceptance
shapesbeats Node (< 1.00x), no other row regressed, all 19 byte-exact, RSS not increased.How to build and measure (shared setup)
Build with the static wrappers or you link a stale runtime and both A/B arms behave
identically — a vacuous result:
The benchmark corpus and Node-oracle expected output are staged at
/Users/amlug/projects/perry/sweep-artifacts-0819b/(sources/*.ts,expected/*.stdout).Profiling: Perry strips its output binaries, so
sampleattributes everything to???. SetPERRY_KEEP_SYMBOLS=1 PERRY_DEBUG_SYMBOLS=1at compile time — the binary keeps~1900 symbols and stays byte-exact — then read
sample's "Sort by top of stack" section.These programs are short; aggregate across ~14 runs for a usable sample count.
Constraints that apply to any fix here
minimize RSS and keep best compute — never trade one for the other silently.
cargo test --release -p perry-runtime --lib,-p perry --bin perry, andbash scripts/run_lint_gates.sh(all 50) must pass.obviously guilty on this exact corpus and measured +0.2%.)
box::release_tests::the_intrusive_free_list_round_trips_a_whole_cohort(flaky: the_intrusive_free_list_round_trips_a_whole_cohort asserts a delta on a process-global counter under parallel tests #8401). If it fails once and reruns clean, it is not yours.