You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A declared DocNode[] class field loses the benefit of existing direct array-call lowering: node.children.push(makeTree(...)) uses js_typed_feedback_native_call_method_by_id, while explicitly binding the same array to a typed local unlocks direct lowering.
Measured 2026-10-01 on macOS arm64, Perry main d40ed1a47019bcae547819ebb5756bcd96ba1057 (v0.5.1655), with matching compiler and runtime archives. This snapshot already includes #11645. These are diagnostic probes; no compiler/runtime fix was applied and the original comparison remains unchanged.
classDocNode{children: DocNode[]=[];}// Original recursive construction:node.children.push(makeTree(depth-1,seed*4+i+1,node));// Diagnostic probe: bind once after constructing node, before the loop:constchildren: DocNode[]=node.children;children.push(makeTree(depth-1,seed*4+i+1,node));
Arm
Median CPU seconds
Median peak RSS MiB
Original/default
10.64
131.63
Typed array local/default
4.90
131.55
Three sequential repeats per arm; CPU = child user + system time from wait4, RSS = peak resident memory. Every run matched the original full stdout and exit status, including parent identity and closure/cache checks. Default, nursery-8, nursery-4 and typed-local arms rotated order across three rounds; the combined arm ran afterward. Same constants, node count and 1 GiB limit.
Three 8-second external stack captures at 1 ms intervals (19,648 main-thread observations) put 55.70% of samples through js_typed_feedback_native_call_method_by_id; all sampled calls originate in makeTree child-array pushes. Nested js_native_call_method accounts for 50.89%. These are inclusive stack observations, including actual push and triggered GC, not isolated dispatch overhead. Original/typed-probe disassembly confirms the lowering change.
Scope
Related to #10505 (generic dispatch overhead) and #5497 (type-directed specialization), but this issue is the compiler-side opportunity: preserve array receiver information through class-field method calls or provide an equivalent guarded fast path. The exact compiler pass losing the proof has not been traced. Inspect HIR/class-field typing, codegen type_analysis/predicates.rs, and the native-call bridge before assigning a cause.
The typed-local probe also removes repeated field reads. It demonstrates an opportunity, not a proof that the entire time difference is dispatch alone or that arbitrary property reads can be hoisted.
Acceptance
The original class-field spelling reaches the appropriate existing guarded array fast path, with checked outputs and a repeatable CPU improvement on this workload; a typed local is no longer needed to unlock it.
Include emitted-IR coverage for the original spelling and semantic negative cases, plus allocating recursive arguments under moving-GC stress. Report CPU and RSS separately; this probe does not lower RSS materially.
Reproducer
Compile/run this unchanged workload and compare against the one-local rewrite above, with the same compiler/runtime artifacts. There are no explicit GC calls in the original workload.
Complete cyclic workload
constCYCLIC=true;constEPOCHS=96;constBATCH=128;constDEPTH=5;constWINDOW=8;// Reference nodes: child-to-parent cycles are observed during every traversal.constTEXT='A collaborative document revision contains paragraphs, comments, edits, and history. '.repeat(3);classDocNode{id: number;text: string;children: DocNode[];parent: DocNode|null;constructor(seed: number,parent: DocNode|null){this.id=seed%100003;this.text=TEXT+seed;this.children=[];this.parent=CYCLIC ? parent : null;}}functionmakeTree(depth: number,seed: number,parent: DocNode|null): DocNode{constnode=newDocNode(seed,parent);if(depth>0){for(leti=0;i<4;i++){node.children.push(makeTree(depth-1,seed*4+i+1,node));}}returnnode;}functionscan(node: DocNode): number{letvalue=node.id+node.text.length+node.text.charCodeAt(node.text.length-1);for(leti=0;i<node.children.length;i++){constchild=node.children[i];if(CYCLIC&&child.parent!==node)thrownewError('parent identity mismatch');if(!CYCLIC&&child.parent!==null)thrownewError('unexpected parent');value+=scan(child);}returnvalue;}functioncapture(root: DocNode): ()=>number{return()=>scan(root);}// A bounded revision cache keeps old graphs and closures alive across churn.constroots: DocNode[]=[];letlatestRoot: DocNode|null=null;letreadLatest: ()=>number=()=>0;letchecksum=0;lettreesBuilt=0;letnodesBuilt=0;functionnodeCount(depth: number): number{letcount=1;for(leti=0;i<depth;i++)count=count*4+1;returncount;}functionbuild(depth: number,seed: number,parent: DocNode|null): DocNode{treesBuilt++;nodesBuilt+=nodeCount(depth);returnmakeTree(depth,seed,parent);}functionepoch(index: number,retain: boolean): void{for(leti=0;i<BATCH;i++){constroot=build(DEPTH,(index*BATCH+i)%4000,null);checksum+=scan(root);if(retain&&i===0){constslot=index%WINDOW;if(roots.length<WINDOW)roots.push(root);elseroots[slot]=root;latestRoot=root;readLatest=capture(root);}}for(leti=0;i<roots.length;i++){// An older cached object now points at newly allocated children.roots[i].children[0]=build(DEPTH-1,index*13+i,roots[i]);checksum+=scan(roots[i]);}}functionverifyCapture(): void{if(latestRoot!==null&&scan(latestRoot)!==readLatest()){thrownewError('closure/cache identity mismatch');}checksum+=readLatest();}// Fill, churn, release, then reuse. No forced GC or runtime-specific API.functioncheckpoint(phase: string,index: number): void{verifyCapture();console.log('phase='+phase+' epoch='+index+' checksum='+checksum+' roots='+roots.length+' trees='+treesBuilt+' nodes='+nodesBuilt);}for(leti=0;i<WINDOW;i++){epoch(i,true);checkpoint('fill',i);}for(leti=WINDOW;i<WINDOW+EPOCHS;i++){epoch(i,true);checkpoint('churn',i);}roots.length=0;latestRoot=null;readLatest=()=>0;checkpoint('released',WINDOW+EPOCHS);for(leti=WINDOW+EPOCHS;i<WINDOW+EPOCHS*2;i++){epoch(i,true);checkpoint('reuse',i);}roots.length=0;latestRoot=null;readLatest=()=>0;checkpoint('released-again',WINDOW+EPOCHS*2);for(leti=WINDOW+EPOCHS*2;i<WINDOW+EPOCHS*2+WINDOW;i++){epoch(i,false);checkpoint('drain',i);}console.log('done checksum='+checksum+' nodes='+nodesBuilt);
Evidence in the project-comparison workspace: demo/results/stress/perry-profile/summary.md, profile-manifest.json, cpu-summary.json, experiment-summary.json, experiments.json, gc-diagnostics.stderr, nursery-4-gc-diagnostics.stderr, census.jsonl, vmmap-*.txt, and the original/typed-receiver makeTree disassemblies. These paths are local artifacts, not publicly hosted links.
Problem and demonstrated opportunity
A declared
DocNode[]class field loses the benefit of existing direct array-call lowering:node.children.push(makeTree(...))usesjs_typed_feedback_native_call_method_by_id, while explicitly binding the same array to a typed local unlocks direct lowering.Measured 2026-10-01 on macOS arm64, Perry main
d40ed1a47019bcae547819ebb5756bcd96ba1057(v0.5.1655), with matching compiler and runtime archives. This snapshot already includes #11645. These are diagnostic probes; no compiler/runtime fix was applied and the original comparison remains unchanged.Three sequential repeats per arm; CPU = child user + system time from wait4, RSS = peak resident memory. Every run matched the original full stdout and exit status, including parent identity and closure/cache checks. Default, nursery-8, nursery-4 and typed-local arms rotated order across three rounds; the combined arm ran afterward. Same constants, node count and 1 GiB limit.
Three 8-second external stack captures at 1 ms intervals (19,648 main-thread observations) put 55.70% of samples through
js_typed_feedback_native_call_method_by_id; all sampled calls originate in makeTree child-array pushes. Nestedjs_native_call_methodaccounts for 50.89%. These are inclusive stack observations, including actual push and triggered GC, not isolated dispatch overhead. Original/typed-probe disassembly confirms the lowering change.Scope
Related to #10505 (generic dispatch overhead) and #5497 (type-directed specialization), but this issue is the compiler-side opportunity: preserve array receiver information through class-field method calls or provide an equivalent guarded fast path. The exact compiler pass losing the proof has not been traced. Inspect HIR/class-field typing, codegen
type_analysis/predicates.rs, and the native-call bridge before assigning a cause.The typed-local probe also removes repeated field reads. It demonstrates an opportunity, not a proof that the entire time difference is dispatch alone or that arbitrary property reads can be hoisted.
Acceptance
const a = [1]; a.push = fnarms neither #11021, Patched built-in prototype methods are ignored: Array.prototype.push / Map.prototype.get / Function.prototype.bind = f never calls f #11394.Reproducer
Compile/run this unchanged workload and compare against the one-local rewrite above, with the same compiler/runtime artifacts. There are no explicit GC calls in the original workload.
Complete cyclic workload
Evidence in the project-comparison workspace:
demo/results/stress/perry-profile/summary.md,profile-manifest.json,cpu-summary.json,experiment-summary.json,experiments.json,gc-diagnostics.stderr,nursery-4-gc-diagnostics.stderr,census.jsonl,vmmap-*.txt, and the original/typed-receivermakeTreedisassemblies. These paths are local artifacts, not publicly hosted links.