Skip to content

codegen: a string's .codePointAt in cc's hottest loop lowers to NativeMethodCall{module:"child_process", class_name:"Instance"} #9847

Description

@proggeramlug

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions