Repository navigation
perf(resolution): read a bare-recorded call's receiver on its own line first — graph byte-identical - #2431
Merged
Conversation
…e first — graph byte-identical bareCallReceiver joined the ref's line with the seven after it for every bare-recorded call, then cut the text at the ref's column. On a minified bundle that line is the whole file, so every call copied it: about 1 MB per call on go-ethereum's graphiql.min.js, 9 GB over that repo's call refs. It now searches the ref's own line from the column first. Every match of the call pattern starts with the name, so a match there with no earlier occurrence of the name on the line is where the joined lines match first too. With no match on the line (a call that continues on the next one), or with the name earlier on it (an earlier `name[…` or `name<…` can run past the line break and enclose the match), the joined lines decide as before. The result is the same for every input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 7, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
bareCallReceiver(src/resolution/name-matcher.ts) reads back what a call the extractor recorded by its bare name was written on, such asthis.render()orwindow.App.init(). For every such call it joined the ref's line with the seven lines after it, then cut the text at the ref's column:On a minified bundle that line is the whole file, so every call copied it. go-ethereum's
graphql/internal/graphiql/graphiql.min.jsis 3 lines (69 / 976,444 / 40 chars), so each call on its long line joined about 1 MB, and more than once, because several resolver checks callbareCallReceiver. Only a line followed by another line pays: V8 returns the element itself for a one-elementjoin, and here the sourcemap comment is the next line. This shipped in 1.6.2 (#2223).It now searches the ref's own line from the column first, and joins the lines only when the line can't settle it:
span[0].slice(from)is a sliced string (no copy), andcallis the same regex as before.Why the result is the same for every input
'', which falls back.iis therefore a match of the joined text ati. The regex has no end anchor or lookahead, its lookbehind sees the same preceding character, and the path it took reads only the line's characters.iwould start with the name beforei, which is still on the line, andindexOf(name) < at.indexrules that out. So the joined text's first match is exactlyi,before = text.slice(0, i)is the same string, and everything after it depends only onbefore.The joined text can match earlier than the line alone, so the
indexOfguard is load-bearing. The optional<[^<>()]*>and\[…\]groups match newlines, so an earliername<…orname[…can run past the line break and enclose the line's own match. Inwindow.App.foo[foo(1),\n2](3), the line alone matchesfoo(at 15, while the joined text matchesfoo[…](at 11. The receiver iswindow.App; without the guard it would benull. No real code in the 12 repos below does this, but the fuzz hit 886 cases, and the new pin test fails with the guard removed.Validation
Differential check, old vs new compiled function. Each arm's built
name-matcher.jswas loaded withbareCallReceiverexported, and both were run on the same inputs with deep-equal results asserted:callsrefs: every identifier of every line (sampled on long lines) at its own column, the start of its member chain, column 0, an earlier column, the line's end and past it, and under the next identifier's name<>[]), random line/column/name, incl. out-of-range lines and NaN/fractional/negative/past-the-end columnsCharacters joined, real refs: go-ethereum 9,056.9 M → 2.1 M (68,667 joins → 370), vscode 189.4 M → 0.7 M, play-samples 74.3 M → 0.0 M.
Graph identity. Full
codegraph init, thenscripts/dump-graph.mjs; dumps are byte-identical:d8a7f86 alone never finished go-ethereum on Node 22 (the stall #2424 fixed), so on that base both Node 22 arms carry #2424's source change. ed199e6 already includes #2424.
CPU, go-ethereum. The box is shared and at 100% load, so these are process CPU and profiles; wall times swing by tens of seconds.
Full
codegraph init, 4 interleaved rounds per arm with alternating order. Medians; every dump identical:A second Node 22 set with phase timings gave 89.5 → 85.3 s, and every run with the change was below every run without it.
--cpu-profon Node 22, all 18 threads:bareCallReceiverself time 6.29 → 1.04 s (2.61 → 0.25 s in the busiest thread); GC 5.48 → 3.00 s.One resolver replaying graphiql.min.js's 18,394 refs, interleaved ×3, with the same 8,863 results: CPU 9.7 → 5.4 s on Node 22 (+ fix(resolution): a large minified JS bundle no longer stalls resolving on Node 22 #2424) and 8.1 → 4.1 s on Node 24.
Whole-index wall time doesn't move measurably: Node 22 phase-timed medians were 66.1 vs 65.8 s, while the parse loop (identical code in both arms) alone varied 21–31 s between runs. The pool spreads the saved work across its workers.
Tests.
__tests__/js-resolution-work.test.tsputs 300window.AppN.mN(N)calls on one minified line. Every receiver is still read back. On main the calls join 2,077,500 characters (300 × the 6,925-char window) against a bound of one line's length (6,886); with this change they join 0.foo[foo(1),\n2](3)case above, and a column past its line's end. It passes on main and fails with theindexOfguard removed.kernel-tsjs-paritywith a kernel built from it.EBUSY, timing asserts). A serial rerun of those 191 files with 120 s timeouts passed 2,334 of 2,356 tests; the 6 that failed were timeouts and anEBUSYagain. Their 6 files then passed alone (159 tests), on main's code and on this branch alike.CHANGELOG
No entry, deliberately. On go-ethereum the whole index uses 6–8% less CPU, but its wall time doesn't move measurably, so users won't notice. The per-file effect is larger (graphiql.min.js resolves in about half the CPU).
Overlap
#2424 (merged as 8f0766c, in this PR's base) also edited
name-matcher.ts, injsCodeBindsName, far from this hunk. It fixed a separate Node 22 stall on the same file; this was the per-ref copy left over from that investigation. No open PR touchesbareCallReceiver.🤖 Generated with Claude Code