Skip to content

perf(codegen): specialize imported class methods that capture this #8693

Description

@proggeramlug

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-cache

The 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 this representation, in lowering artifacts.

Semantic constraints

  • Preserve this binding 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/remove and Group.pushEntity/removeEntity directly or through typed clones, without js_native_call_method_by_id, js_typed_feedback_native_call_method_by_id, or generic closure/vtable dispatch.
  • At least one method that captures this is selected as a typed clone, and --explain-lowering no longer records captures_this as 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-benchmarks perform-ecs/destroy with 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions