What the compiler emits
In cli_2.1.112.js (claude-code), N$6 is string-width. Its loop is the
single hottest thing in a claude-code turn: the allocation census ranks it
1/2/3 by count (172,032 segment records + 247,808 substrings per 400-character
reply, 58 % of the top-30 allocation count), and a sample puts 60-85 % of
active main-thread CPU inside the subtree it sits in (ink wrapText → JS
wrap-ansi → string-width).
Source:
for (let {segment: O} of rR_.segment(q)) {
let w = O.codePointAt(0);
...
}
O is a grapheme — a string. Here is what it lowers to:
Let { id: 118612, name: "w", ty: Any, mutable: true, init: Some(NativeMethodCall {
module: "child_process",
class_name: Some("Instance"),
object: Some(LocalGet(118611)), // the destructured segment binding `O`
method: "codePointAt",
args: [Integer(0)] }) }
Repro — one minute, no LLVM build
perry compile --no-auto-optimize --trace hir --focus 'N$6' \
<path>/cli_2.1.112.js -o /tmp/out
Find __destruct_118613 in the dump; the Let for w two statements later is
the node above. The whole-bundle HIR pass is ~1 minute, so this is cheap to
re-check.
Cause: UNKNOWN. One hypothesis was tested and refuted
I am reporting what the compiler emits, not why. My one hypothesis was that
lookup_native_instance is keyed by identifier name
(HashMap<String, (String, String)>, lower/context.rs:1587), and that in a
minified bundle the name O collides: cli_2.1.112.js has three
O = <child_process>.spawn/exec(…) bindings and 5,381 bindings named O.
That hypothesis does not reproduce. Controlled experiment: a small probe in
the bundle's module shape whose segment loop classifies correctly, plus one
added, unrelated, never-executed
function __unrelated_spawner(){ let O = require("child_process").spawn("true"); }.
The lowering was unchanged — the segment loop's codePointAt stayed a
generic Call(PropertyGet(O, "codePointAt"), [0]). So whatever tags local
118611 as a child_process Instance, it is not that route.
Note there is also an id-keyed twin of that table
(local_id_native_instances: HashMap<LocalId, …> in
js_transform/cross_module_natives.rs), which I did not investigate.
The way to settle it is a diagnostic at the decision site — which local, which
binding it resolved through, and what evidence it used — not another
hypothesis.
Both halves of this call are already known to be expensive
cc runs correctly, so this dispatch must be falling through to a generic path
on a string receiver — once per grapheme, in the dominant loop. That
fall-through has a cost independent of anything else here.
#9795 ("perf(runtime): dispatch String.prototype.codePointAt natively —
99k String wrappers per reply", open) is the runtime half of this same call.
This issue is the codegen half: the call does not even arrive at the runtime
labelled as a string method. Whoever picks up the resolver should know both
exist.
Why this was found, and the constraint it puts on any fix
Found by a compile-time counter for the segment-view for-of tier (#9843), whose
per-use classifier reconciles against a sound collect_local_refs_stmt count
and books every unrecognised occurrence as "must materialise". It reported
code_point_at=0, materialise=1 for this loop, which is what pointed at the
node.
A fix must not be "teach the classifier to match
NativeMethodCall{module:"child_process", method:"codePointAt"}". That would
launder a misclassification into a fast path. The lowering is the thing to fix.
What the compiler emits
In
cli_2.1.112.js(claude-code),N$6isstring-width. Its loop is thesingle hottest thing in a claude-code turn: the allocation census ranks it
1/2/3 by count (172,032 segment records + 247,808 substrings per 400-character
reply, 58 % of the top-30 allocation count), and a
sampleputs 60-85 % ofactive main-thread CPU inside the subtree it sits in (ink
wrapText→ JSwrap-ansi →
string-width).Source:
Ois a grapheme — a string. Here is what it lowers to:Repro — one minute, no LLVM build
Find
__destruct_118613in the dump; theLetforwtwo statements later isthe node above. The whole-bundle HIR pass is ~1 minute, so this is cheap to
re-check.
Cause: UNKNOWN. One hypothesis was tested and refuted
I am reporting what the compiler emits, not why. My one hypothesis was that
lookup_native_instanceis keyed by identifier name(
HashMap<String, (String, String)>,lower/context.rs:1587), and that in aminified bundle the name
Ocollides:cli_2.1.112.jshas threeO = <child_process>.spawn/exec(…)bindings and 5,381 bindings namedO.That hypothesis does not reproduce. Controlled experiment: a small probe in
the bundle's module shape whose segment loop classifies correctly, plus one
added, unrelated, never-executed
function __unrelated_spawner(){ let O = require("child_process").spawn("true"); }.The lowering was unchanged — the segment loop's
codePointAtstayed ageneric
Call(PropertyGet(O, "codePointAt"), [0]). So whatever tags local118611 as a
child_processInstance, it is not that route.Note there is also an id-keyed twin of that table
(
local_id_native_instances: HashMap<LocalId, …>injs_transform/cross_module_natives.rs), which I did not investigate.The way to settle it is a diagnostic at the decision site — which local, which
binding it resolved through, and what evidence it used — not another
hypothesis.
Both halves of this call are already known to be expensive
cc runs correctly, so this dispatch must be falling through to a generic path
on a string receiver — once per grapheme, in the dominant loop. That
fall-through has a cost independent of anything else here.
#9795 ("perf(runtime): dispatch
String.prototype.codePointAtnatively —99k String wrappers per reply", open) is the runtime half of this same call.
This issue is the codegen half: the call does not even arrive at the runtime
labelled as a string method. Whoever picks up the resolver should know both
exist.
Why this was found, and the constraint it puts on any fix
Found by a compile-time counter for the segment-view for-of tier (#9843), whose
per-use classifier reconciles against a sound
collect_local_refs_stmtcountand books every unrecognised occurrence as "must materialise". It reported
code_point_at=0, materialise=1for this loop, which is what pointed at thenode.
A fix must not be "teach the classifier to match
NativeMethodCall{module:"child_process", method:"codePointAt"}". That wouldlaunder a misclassification into a fast path. The lowering is the thing to fix.