Repository navigation
perf(codegen): specialize stable imported object-literal methods #8775
Description
Summary
Perry can now specialize imported class prototype methods that capture this (#8693/#8737), but stable methods on imported object-literal adapters still traverse generic method lookup and closure dispatch on every call. Public ECS adapters use exactly this pattern, so the outer calls remain generic and prevent profitable inlining across the adapter boundary.
Add guarded direct specialization/inlining for stable methods on imported object literals (including default exports), with an explicit generic fallback when the receiver shape or own method identity changes.
Self-contained reproduction
adapter.js:
class Store {
constructor() {
this.entities = [];
}
create(id) {
const entity = { id, value: 0 };
this.entities.push(entity);
return entity;
}
add(entity) {
entity.value += 1;
}
destroy(entity) {
const index = this.entities.indexOf(entity);
if (index !== -1) this.entities.splice(index, 1);
return entity.value;
}
}
export default {
store: null,
setup() {
this.store = new Store();
},
createEntity(id) {
return this.store.create(id);
},
addComponent(entity) {
this.store.add(entity);
},
destroyEntity(entity) {
return this.store.destroy(entity);
},
};main.js:
import adapter from "./adapter.js";
adapter.setup();
let checksum = 0;
const iterations = 200_000;
const start = performance.now();
for (let i = 0; i < iterations; i++) {
const entity = adapter.createEntity(i);
adapter.addComponent(entity);
adapter.addComponent(entity);
checksum += adapter.destroyEntity(entity);
}
console.log(JSON.stringify({
elapsedMs: performance.now() - start,
checksum,
remaining: adapter.store.entities.length,
}));Expected result: checksum: 400000, remaining: 0.
Build with the same release/runtime artifacts and retain --trace llvm --opt-report=json --explain-lowering --no-cache output. The public integration case is ddmills/js-ecs-benchmarks perform-ecs@0.7.8, suite Destroy: the suite and library contexts are imported object literals, and the timed method calls ctx.createEntity, ctx.addPositionComponent, ctx.addVelocityComponent, and ctx.destroyEntity.
Current evidence
On Perry 20a388974d7333ccaec9da9175fbcee54ad49902, the corrected public workload measures 13.609234 us/op versus Node 1.025681 us/op on the quiet Apple M1 protocol: 13.268x slower with exact component-ID and zero-retained-view parity.
A symbolized profile synchronized to the timed loop collected 701 main-thread samples. The imported adapter branches were:
createEntity: 317 samples (45.2% inclusive);addPositionComponent: 103 (14.7%);addVelocityComponent: 95 (13.6%);destroyEntity: 42 (6.0%).
Every branch enters js_typed_feedback_native_call_method_by_id / js_native_call_method, handle dispatch, js_native_call_value, and an adapter closure before reaching already-specialized class methods below it. The fresh lowering report selects 10 typed clones but still records 10 generic-method fallbacks, 14 runtime dynamic boundaries, and 232 typed-clone rejections.
#8693 is not this case: it publishes producer-authoritative this capabilities for imported class prototype methods. These receivers are stable imported ordinary object literals whose methods are own function-valued properties.
Proposed direction
- Publish producer-authoritative shape and own-method identity metadata for eligible imported object literals and their aliases/re-exports.
- At a proven receiver site, emit a guard for exact receiver/shape, own property descriptor, function identity, and relevant invalidation generation, then call a clone with the correct
thisbinding. - Inline small/medium adapter methods after direct-call selection so downstream imported-class specialization, field facts, and aggregate scalar replacement can see through the boundary.
- Keep generic lookup/call as a cold fallback and explain both selection and rejection in lowering artifacts.
An initial slice can require a module-initialized const/default-export object, static string method names, ordinary data-method properties, and no proxy/accessor in the proven chain.
Semantic constraints
- Preserve
thisfor concise methods and function-valued properties; distinguish arrows, extracted/rebound methods,call/apply, and bound functions. - Invalidate on own-method replacement/deletion/redefinition, descriptor/enumerability changes, receiver/prototype mutation, own shadowing, proxy insertion, module reevaluation, and alias writes.
- Preserve getters/setters, proxies, exceptions, inheritance/prototype lookup, and cross-module initialization order.
- Keep receiver/function values properly rooted and reload after collecting calls.
Acceptance criteria
- Register the two-file reproduction; Node and Perry print
checksum: 400000andremaining: 0, normally and under forced-moving GC. - Stable fast arms for
createEntity,addComponent, anddestroyEntitycall direct clones (or inline them) withoutjs_native_call_method_by_id,js_typed_feedback_native_call_method_by_id,js_native_call_value, or generic closure/vtable dispatch. - Explicit generic fallback remains and is exercised by method replacement, deletion/recreation, receiver/prototype mutation, extraction/rebinding, accessor, and proxy tests.
--explain-loweringidentifies the imported object-literal provenance, selected method identity, guards, and fallback.- Re-run
perform-ecs/destroyand the completeperform-ecsadapter set with exact state parity. On the quiet M1 alternating protocol, require at least a 10% median improvement and 9/11 wins; report generic-dispatch samples, executable size, and RSS.
Related work
- perf(codegen): specialize imported class methods that capture this #8693/fix(runtime): fence the dense subclass seqlock; specialize imported this-methods and proven Symbols #8737 covers imported class prototype methods capturing
this; this ticket extends the call-routing mechanism to imported object-literal own methods. - codegen: type-directed specialization — emit native ops where the type is statically known (#5334 Tier 3 / Lever E) #5497 is the broad type-directed specialization tracker; this is the concrete cross-module receiver/method-identity slice exposed by the ECS profile.
Activity