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
this binding.
- 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
this for 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: 400000 and remaining: 0, normally and under forced-moving GC.
- Stable fast arms for
createEntity, addComponent, and destroyEntity call direct clones (or inline them) without js_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-lowering identifies the imported object-literal provenance, selected method identity, guards, and fallback.
- Re-run
perform-ecs/destroy and the complete perform-ecs adapter 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
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:main.js:Expected result:
checksum: 400000,remaining: 0.Build with the same release/runtime artifacts and retain
--trace llvm --opt-report=json --explain-lowering --no-cacheoutput. The public integration case isddmills/js-ecs-benchmarksperform-ecs@0.7.8, suiteDestroy: the suite and library contexts are imported object literals, and the timed method callsctx.createEntity,ctx.addPositionComponent,ctx.addVelocityComponent, andctx.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
thiscapabilities for imported class prototype methods. These receivers are stable imported ordinary object literals whose methods are own function-valued properties.Proposed direction
thisbinding.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
thisfor concise methods and function-valued properties; distinguish arrows, extracted/rebound methods,call/apply, and bound functions.Acceptance criteria
checksum: 400000andremaining: 0, normally and under forced-moving GC.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.--explain-loweringidentifies the imported object-literal provenance, selected method identity, guards, and fallback.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
this; this ticket extends the call-routing mechanism to imported object-literal own methods.