Summary
Perry's Ptr<Shape> proof stops when a known-shape object is passed as a non-receiver call argument. The callee therefore performs guarded field ICs and repeated GC-header/shape-descriptor reads even when a monomorphic call site proves the argument's class.
Generalize the existing clone-and-route mechanism for proven this receivers to selected object argument positions: a guarded call should route a known-shape argument into a callee clone that can directly access its declared fields while retaining the tagged ABI, shadow rooting, and generic fallback.
Self-contained reproduction
class Entity {
constructor(id) {
this.id = id;
this.components = [];
}
}
class Registry {
add(entity, component) {
entity.components.push(component);
}
hash(entity) {
let value = entity.id;
for (let i = 0; i < entity.components.length; i++) {
value += entity.components[i];
}
return value;
}
clear(entity) {
entity.components.length = 0;
}
}
const registry = new Registry();
let checksum = 0;
const iterations = 200_000;
const start = performance.now();
for (let i = 0; i < iterations; i++) {
const entity = new Entity(i);
registry.add(entity, 1);
registry.add(entity, 2);
checksum += registry.hash(entity);
registry.clear(entity);
}
console.log(JSON.stringify({
elapsedMs: performance.now() - start,
checksum,
}));
Expected checksum: 20000500000.
Build with release 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.
Current evidence
The fresh lowering report for the real workload on Perry 20a388974d7333ccaec9da9175fbcee54ad49902 diagnoses the exact loss in perform-ecs/src/ECS.ts, closure#7:
local: entity
candidate class: Entity
outcome: denied
rule: rule 2 (containment)
reason: passed as a call argument. There is no mechanism yet by which a
shape fact at a call site becomes a fact about the callee's parameter.
The timed M1 profile collected 701 main-thread samples. shape_descriptor_by_id was the top leaf in 68 samples (9.7%) and try_read_tracked_gc_header in 62 (8.8%). Within ECS.addComponentsToEntity$pshape, field-get IC misses accounted for 47 samples in the create path plus 14 and 22 in the two later add paths (83 inclusive samples, 11.8% of the profile). The full case remains 13.268x slower than Node.
The closed design/scoping issue #7034 explicitly identified argument-position clone-and-route as the next Ptr<Shape> phase; it did not implement it. #8693/#8737 applies the same general idea to the receiver (this) position only.
Proposed direction
- For a direct/guarded call whose argument has a proven class/shape, build or select a callee clone annotated with a
Ptr<Shape> fact for that parameter.
- Start with non-exported or producer-known callees and monomorphic argument positions; require all routed call sites to agree, or guard each call site and retain the boxed generic entry.
- Keep the public machine ABI tagged. On clone entry, shadow-bind the tagged parameter slot exactly as proven-
this clones do, and re-derive the raw pointer from the rewritten slot after every safepoint.
- Consume the parameter proof in direct declared-field loads/stores, then allow downstream inlining and scalar replacement to use the same fact.
- Record argument index, producer provenance, selected shape/class, guard, clone, and fallback in
--explain-lowering.
Semantic and GC constraints
- A runtime TypeScript annotation is not proof; wrong-class, subclass, proxy, cross-realm, or mutated-shape values must take the generic fallback.
- Preserve aliasing, identity, accessors, private brands, prototype mutation, exceptions, and method replacement semantics.
- A shadow root must provide liveness, relocation, and the value actually observed after GC. Do not copy the non-movable typed-array-pointer shortcut: ordinary objects move.
- Do not keep raw object pointers at rest or across calls/safepoints; write barriers remain required for pointer stores.
Acceptance criteria
- Register the reproduction as semantic/compiler-output coverage; Node and Perry print checksum
20000500000, normally and under forced-moving GC.
- The stable
Registry.add/hash/clear clones carry a proven Entity argument and access id / components directly without field-get IC diamonds or shape_descriptor_by_id on the fast arm.
- The tagged generic entry and explicit guard-failure fallback remain present and are exercised.
- Negative tests cover multiple caller shapes, subclassing, shape mutation, accessors, proxies, alias/reassignment, exceptions, imports/re-exports, and forced-moving GC.
--explain-lowering no longer reports call-argument containment as the denial for the valid fixture and names every selected argument clone.
- Re-run
perform-ecs/destroy with exact component-ID/view-count parity. Require at least a 10% median Perry improvement and 9/11 wins on the quiet M1 protocol; report shape/header/IC profile samples, RSS, and executable size.
Related work
Summary
Perry's
Ptr<Shape>proof stops when a known-shape object is passed as a non-receiver call argument. The callee therefore performs guarded field ICs and repeated GC-header/shape-descriptor reads even when a monomorphic call site proves the argument's class.Generalize the existing clone-and-route mechanism for proven
thisreceivers to selected object argument positions: a guarded call should route a known-shape argument into a callee clone that can directly access its declared fields while retaining the tagged ABI, shadow rooting, and generic fallback.Self-contained reproduction
Expected checksum:
20000500000.Build with release artifacts and retain
--trace llvm --opt-report=json --explain-lowering --no-cacheoutput. The public integration case isddmills/js-ecs-benchmarks,perform-ecs@0.7.8, suiteDestroy.Current evidence
The fresh lowering report for the real workload on Perry
20a388974d7333ccaec9da9175fbcee54ad49902diagnoses the exact loss inperform-ecs/src/ECS.ts,closure#7:The timed M1 profile collected 701 main-thread samples.
shape_descriptor_by_idwas the top leaf in 68 samples (9.7%) andtry_read_tracked_gc_headerin 62 (8.8%). WithinECS.addComponentsToEntity$pshape, field-get IC misses accounted for 47 samples in the create path plus 14 and 22 in the two later add paths (83 inclusive samples, 11.8% of the profile). The full case remains 13.268x slower than Node.The closed design/scoping issue #7034 explicitly identified argument-position clone-and-route as the next
Ptr<Shape>phase; it did not implement it. #8693/#8737 applies the same general idea to the receiver (this) position only.Proposed direction
Ptr<Shape>fact for that parameter.thisclones do, and re-derive the raw pointer from the rewritten slot after every safepoint.--explain-lowering.Semantic and GC constraints
Acceptance criteria
20000500000, normally and under forced-moving GC.Registry.add/hash/clearclones carry a provenEntityargument and accessid/componentsdirectly without field-get IC diamonds orshape_descriptor_by_idon the fast arm.--explain-loweringno longer reports call-argument containment as the denial for the valid fixture and names every selected argument clone.perform-ecs/destroywith exact component-ID/view-count parity. Require at least a 10% median Perry improvement and 9/11 wins on the quiet M1 protocol; report shape/header/IC profile samples, RSS, and executable size.Related work
thisrouting for imported class methods; this ticket generalizes it to non-receiver object arguments.