mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
8f0766cc50
go-ethereum's `codegraph init` sat in "Resolving refs" for good on Node 22 (killed after 1 h 55 min and 9,203 CPU-seconds). It was not Go: two resolver pool workers each sat on one 500-ref chunk of graphql/internal/graphiql/graphiql.min.js, a 980 KB bundle on one line (18,388 refs, 2,234 functions). Whether a JS function binds a name itself (jsFunctionLocalScope, #2226) is read off its comment-stripped code with a parameter-list regex that starts with a `(?<!\b(?:if|while|for|switch|with)\s*)` lookbehind. Node 22's V8 compiles new regexes without optimization once a process has generated about a megabyte of regex code and holds 16 MB of executable memory, which a pool worker reaches after some 30,000 refs. Unoptimized, the lookbehind runs at every position and its `\s*` reads back through the whole run of blanks before it, so a run of n blanks costs n^2/2 steps. The comment stripper, which doesn't know regex literals, reads the `//` that closes `/Trident\//` as a comment and blanks the remaining 961,968 characters of the line. In a worker's state the regex took 36 ms over 16 K characters of that line, 5.1 s over 32 K and 54 s over 64 K, against 0.23 ms over 128 K in a fresh process. Compiling 20,000 throwaway regexes first reproduces it in a bare Node 22 process, and --no-regexp-optimization reproduces it anywhere. Node 24's V8, the runtime release bundles ship, kept optimizing after 60,000, so released installs finish: 1.6.2 indexes go-ethereum on its bundled Node 24 and stalls the same way on Node 22. The stall is as old as #2226. - A leading `(?=\()` keeps the lookbehind to where a parameter list opens. Every match starts with `(` anyway, so the regex matches exactly what it did. - The jsx-render pass split the file, sliced a function's lines and scanned them for tags once per function: 2,234 times over the bundle's line, 25 of the 28 s its linking passes took on Node 22 (9 of 13 on Node 24), for no edges. Functions that span the same lines now share one scan, and the file is split once. Graph: go-ethereum dumps (nodes, edges, unresolved refs, files) are byte-identical between main on Node 24 (main never finishes on Node 22) and this change on Node 22 and on Node 24. Main and this change also match byte for byte on bootstrap4, legend-state, takenote, mantis, excalidraw (482 jsx-render edges), qwik, gin, prometheus, etcd and hugo. Time, go-ethereum: on Node 22 main never finishes and this change takes 36 s; on Node 24 (three interleaved rounds, medians) 42.1 s -> 38.3 s wall, with resolution 17.7 s -> 12.7 s and the linking passes 6.5 s -> 1.5 s. Tests: js-local-binding-backtracking forces V8's unoptimized mode and indexes a function with a 60,000-character comment before a call (69 s on main, 1.4 s now); jsx-render-work counts the tag scans of 60 components on one line (60 on main, 1 now). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
3.4 KiB
TypeScript
85 lines
3.4 KiB
TypeScript
/**
|
|
* The jsx-render pass reads each function's own lines for the component tags
|
|
* it renders. Every function of a minified bundle spans the bundle's one line,
|
|
* so it split, sliced and scanned the whole file once per function: 2,234
|
|
* times over go-ethereum's 980 KB `graphiql.min.js`, 25 of the 28 seconds its
|
|
* linking passes took on Node 22 (9 of 13 on Node 24). Functions that span
|
|
* the same lines render the same tags, so they share one scan. Counted, never
|
|
* timed.
|
|
*/
|
|
import { describe, it, expect, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { CodeGraph } from '../src';
|
|
|
|
/** The source of the pass's tag pattern, to count its scans by. */
|
|
const JSX_TAG_SOURCE = String.raw`<([A-Z][A-Za-z0-9_]*)[\s/>]`;
|
|
|
|
describe('jsx-render pass work', () => {
|
|
let tmpDir: string | undefined;
|
|
let cg: CodeGraph | undefined;
|
|
const originalExec = RegExp.prototype.exec;
|
|
|
|
afterEach(() => {
|
|
RegExp.prototype.exec = originalExec;
|
|
cg?.close();
|
|
cg = undefined;
|
|
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5 });
|
|
tmpDir = undefined;
|
|
});
|
|
|
|
async function index(files: Record<string, string>): Promise<CodeGraph> {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-jsx-work-'));
|
|
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"dependencies":{"react":"^18.0.0"}}');
|
|
for (const [file, content] of Object.entries(files)) fs.writeFileSync(path.join(tmpDir, file), content);
|
|
cg = CodeGraph.initSync(tmpDir);
|
|
await cg.indexAll();
|
|
return cg;
|
|
}
|
|
|
|
/** What each named function renders, by jsx-render edge. */
|
|
const renders = (graph: CodeGraph, name: string): string[] => {
|
|
const from = graph.getNodesByName(name).find((n) => n.kind === 'function' || n.kind === 'component')!;
|
|
return graph.getOutgoingEdges(from.id)
|
|
.filter((e) => (e.metadata as { synthesizedBy?: string } | undefined)?.synthesizedBy === 'jsx-render')
|
|
.map((e) => graph.getNode(e.target)!.name);
|
|
};
|
|
|
|
it('scans a line that many components share once, not once per component', async () => {
|
|
const COMPONENTS = 60;
|
|
const bundle = Array.from({ length: COMPONENTS }, (_, i) => `function Card${i}(){return <Badge/>}`).join('');
|
|
// A scan of the bundle's line: the tag pattern run over it from the start.
|
|
let scans = 0;
|
|
RegExp.prototype.exec = function (this: RegExp, input: string) {
|
|
if (this.source === JSX_TAG_SOURCE && this.lastIndex === 0 && typeof input === 'string' &&
|
|
input.includes(`Card${COMPONENTS - 1}(`)) scans++;
|
|
return originalExec.call(this, input);
|
|
};
|
|
const graph = await index({
|
|
'bundle.jsx': `${bundle}\n`,
|
|
'badge.jsx': 'export function Badge(){return null}\n',
|
|
});
|
|
RegExp.prototype.exec = originalExec;
|
|
|
|
for (const i of [0, 31, COMPONENTS - 1]) expect(renders(graph, `Card${i}`)).toEqual(['Badge']);
|
|
expect(scans).toBeGreaterThan(0);
|
|
expect(scans).toBeLessThan(COMPONENTS / 4);
|
|
});
|
|
|
|
it('keeps the tags of functions on different lines apart', async () => {
|
|
const graph = await index({
|
|
'page.jsx': [
|
|
'export function Page() { return <Header/>; }',
|
|
'export function Footer() {',
|
|
' return <Links/>;',
|
|
'}',
|
|
'function Header(){return null} function Links(){return null}',
|
|
'',
|
|
].join('\n'),
|
|
});
|
|
expect(renders(graph, 'Page')).toEqual(['Header']);
|
|
expect(renders(graph, 'Footer')).toEqual(['Links']);
|
|
});
|
|
});
|