Repository navigation
perf(codegen): specialize imported class methods that capture this #8693
Description
Summary
Perry rejects typed specialization for many imported class methods solely because they capture this. In the public perform-ecs/destroy workload, all major adapter/ECS calls therefore traverse generic runtime method lookup, closure dispatch, and ABI/vtable trampolines even though the receiver classes and method identities are stable.
This blocks both the direct call win and downstream inlining, representation propagation, constant folding, scalar replacement, and allocation removal.
This ticket is distinct from #8672: that PR preserves already-proven direct-method guards across unrelated prototype writes. This issue covers sites where Perry never selects a typed/direct clone because the imported method captures this or its receiver proof is not propagated through the package graph.
Self-contained reproduction
Create two ESM files:
// registry.js
export class Group {
entities = [];
pushEntity(entity) {
this.entities.push(entity);
}
removeEntity(entity) {
const index = this.entities.indexOf(entity);
if (index !== -1) this.entities.splice(index, 1);
}
}
export class Registry {
group = new Group();
add(entity) {
this.group.pushEntity(entity);
}
remove(entity) {
this.group.removeEntity(entity);
}
}// main.js
import { Registry } from './registry.js';
const registry = new Registry();
const iterations = 200_000;
const start = performance.now();
for (let i = 0; i < iterations; i++) {
const entity = { id: i };
registry.add(entity);
registry.remove(entity);
}
console.log(JSON.stringify({
elapsedMs: performance.now() - start,
remaining: registry.group.entities.length,
}));{
"type": "module"
}Build with:
cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
PERRY_NO_AUTO_OPTIMIZE=1 \
PERRY_RUNTIME_DIR=target/release \
target/release/perry compile main.js -o repro-perry \
--trace llvm --opt-report=json --explain-lowering --no-cacheThe output must end with remaining: 0 in Node and Perry.
The public integration reproduction is ddmills/js-ecs-benchmarks with perform-ecs@0.7.8, suite Destroy:
perform(ctx) {
const entity = ctx.createEntity();
ctx.addPositionComponent(entity);
ctx.addVelocityComponent(entity);
ctx.destroyEntity(entity);
}Current evidence
Measured on Perry 7ad718ab4287641cb2b29dce3a056edc45d4c7f8, Node 26.5.1, Apple M1 Mac mini.
| Runtime | median |
|---|---|
| Node | 1.024601 us/op |
| Perry | 33.904990 us/op |
Perry/Node: 33.091x. Every process returned component IDs ["0", "1"] and zero retained view entries.
A fresh three-second Perry profile attributed the top-level workload branches as follows:
- entity creation: 33.4%
- add position: 26.0%
- add velocity: 22.7%
- destroy: 16.0%
Thus approximately 82% is create/add work, not destruction. Every top-level branch enters js_typed_feedback_native_call_method_by_id / js_native_call_method, then runtime handle dispatch, js_native_call_value, and a closure/vtable trampoline.
The lowering report contains 494 records, zero typed-path selections, zero scalar replacements, 61 inserted boxes, and 227 typed-path rejections. captures_this alone rejects 56 typed-clone candidates.
Proposed direction
- Treat a known class receiver plus stable method identity as sufficient to build a guarded specialized clone even when the body reads/writes
this. - Pass a typed/native receiver representation to the clone and preserve field/shape facts across the call.
- Propagate class and method metadata through ESM/npm import/export boundaries and through stable adapter objects.
- Emit a guarded direct call when the receiver class/shape, prototype method identity, and relevant invalidation generations match.
- Inline profitable small/medium methods after direct-call selection so downstream field, array, and allocation optimizations can see the body.
- Retain generic lookup/call fallback when any receiver or method proof fails.
- Explain selections/rejections, including receiver provenance and
thisrepresentation, in lowering artifacts.
Semantic constraints
- Preserve
thisbinding for extracted, rebound,call/apply, arrow, accessor, proxy, and bound methods. - Correctly invalidate on method replacement/deletion/recreation, prototype replacement/mutation, own-property shadowing, descriptor changes, proxy insertion, and class reevaluation.
- Preserve inheritance,
super, private brands, derived constructors, and exceptions. - Do not confuse native-module constructor metadata with arbitrary bound native exports; perf: array forwarding compression, shared add-tree guards, packed shape guards #8682 explicitly kept that distinction when holding perf(runtime): restore method-scoped prototype guards #8672.
- Remain correct under moving/forced GC and refresh receiver/captured roots across collecting calls.
Acceptance criteria
- Add the two-file reproduction as a registered semantic and compiler-output fixture; Node and Perry must finish with
remaining: 0. - The stable fast arm calls
Registry.add/removeandGroup.pushEntity/removeEntitydirectly or through typed clones, withoutjs_native_call_method_by_id,js_typed_feedback_native_call_method_by_id, or generic closure/vtable dispatch. - At least one method that captures
thisis selected as a typed clone, and--explain-loweringno longer recordscaptures_thisas the rejection for that valid site. - Explicit fallback remains in artifacts and is exercised by receiver/method/prototype mutation fixtures.
- Tests cover imports/re-exports, inheritance, own shadowing, extraction/rebinding, accessors/proxies, private fields, exceptions, and forced-moving GC.
- Re-run
ddmills/js-ecs-benchmarksperform-ecs/destroywith the exact state oracle. Require at least a 10% median improvement, at least 9/11 paired wins on the quiet M1 protocol, and no regression in the complete ECS adapter suite. - Report profile deltas for generic method-dispatch samples, executable size, peak RSS, and lowering counts for typed clone selections/rejections.
Related work
- perf(runtime): restore method-scoped prototype guards #8672 improves invalidation granularity for direct-method guards already selected; it does not address the
captures_thisselection failure demonstrated here. - Typed-feedback profile replay with freshness validation and guarded specialization #8504 may eventually supply advisory profile facts, but this exact imported class graph is statically visible and should not require profile replay for correctness or selection.
- perf(codegen): scalar-replace non-escaping aggregate literals #8691 is the scalar-replacement follow-up that should consume the inlined bodies from this work rather than expanding this ticket into a general escape-analysis change.
- perf(codegen/runtime): fast-path for-in over stable monomorphic keys #8694 independently removes stable
for...inenumeration overhead inside the registry methods exposed by this work.
Activity
fix(runtime): fence the dense subclass seqlock; specialize imported t…