ad5300a601
Two first-run UX bugs surfaced by indexing a real 1,342-file C repo: 1. After 'Resolving refs' hit 100%, the ~40 dynamic-dispatch synthesis passes ran with no progress surface, so the bar sat frozen at 100% long enough to read as a hang (the C fn-pointer pass alone can hold for a while on C-heavy repos). Synthesis now reports per-pass progress through a new 'linking' IndexProgress phase, rendered as 'Linking dynamic dispatch'. The step total is pinned by a test to the synthesizer's actual __mark() count so adding a pass without bumping it fails loudly. 2. node:sqlite's ExperimentalWarning is emitted once per THREAD, so the main process plus every parse worker printed it mid-index, interleaved with the progress UI. All launch paths now pass --disable-warning=ExperimentalWarning: both bundle launchers, the Windows npm-shim invocation, and the CLI self-relaunch (NODE_RUNTIME_FLAGS, deliberately excluded from the re-exec gate so an older installed launcher never triggers a pointless re-exec, and version-gated off nodes older than the flag). Verified end-to-end on the same repo: zero warnings, live linking bar, byte-identical graph (50,520 nodes / 148,232 edges). Full suite green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
131 lines
5.3 KiB
TypeScript
131 lines
5.3 KiB
TypeScript
/**
|
|
* WASM runtime flags — the workaround for the V8 turboshaft WASM Zone OOM
|
|
* (`Fatal process out of memory: Zone`) that crashed `codegraph index` on large
|
|
* polyglot repos under Node >= 22. See issues #293 and #298.
|
|
*
|
|
* The crash was reproduced with the real indexer on the bundled Node 24 runtime;
|
|
* empirically only `--liftoff-only` prevents it (`--no-wasm-tier-up` /
|
|
* `--no-wasm-dynamic-tiering` do not), and the flag must be on node's command
|
|
* line — `setFlagsFromString`, worker `execArgv`, and `NODE_OPTIONS` all fail.
|
|
* These tests pin that contract so it can't silently regress.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { spawnSync } from 'child_process';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import {
|
|
WASM_RUNTIME_FLAGS,
|
|
NODE_RUNTIME_FLAGS,
|
|
nodeRuntimeFlagsFor,
|
|
processHasWasmRuntimeFlags,
|
|
buildRelaunchArgv,
|
|
} from '../src/extraction/wasm-runtime-flags';
|
|
|
|
describe('WASM_RUNTIME_FLAGS', () => {
|
|
it('pins --liftoff-only (the only flag shown to stop the turboshaft Zone OOM)', () => {
|
|
// On Node 24, --no-wasm-tier-up and --no-wasm-dynamic-tiering both still
|
|
// crash; only --liftoff-only forces grammars onto the Liftoff baseline and
|
|
// off the optimizing tier. Pin it so it can't be swapped for an ineffective
|
|
// flag.
|
|
expect(WASM_RUNTIME_FLAGS).toContain('--liftoff-only');
|
|
});
|
|
|
|
it('every flag is a real, accepted flag on the running Node/V8 runtime', () => {
|
|
// node rejects unknown CLI flags at startup, so a renamed/removed flag would
|
|
// break the bundled launcher and make the relaunch guard a silent no-op.
|
|
// Prove each flag actually launches node here.
|
|
const res = spawnSync(
|
|
process.execPath,
|
|
[...WASM_RUNTIME_FLAGS, '-e', 'process.exit(0)'],
|
|
{ encoding: 'utf8' }
|
|
);
|
|
expect(res.status, `node rejected ${WASM_RUNTIME_FLAGS.join(' ')}:\n${res.stderr}`).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('NODE_RUNTIME_FLAGS', () => {
|
|
it('suppresses the node:sqlite ExperimentalWarning on this runtime', () => {
|
|
// The warning is emitted once per THREAD (main + every parse worker), so
|
|
// during indexing it repeatedly interleaves with the progress UI. Prove
|
|
// the flag both launches node and actually silences the warning.
|
|
expect(NODE_RUNTIME_FLAGS).toContain('--disable-warning=ExperimentalWarning');
|
|
const res = spawnSync(
|
|
process.execPath,
|
|
[...NODE_RUNTIME_FLAGS, '-e', "require('node:sqlite'); process.exit(0)"],
|
|
{ encoding: 'utf8' }
|
|
);
|
|
expect(res.status, res.stderr).toBe(0);
|
|
expect(res.stderr).not.toMatch(/ExperimentalWarning/);
|
|
});
|
|
|
|
it('is empty on nodes too old for --disable-warning (fatal "bad option" there)', () => {
|
|
expect(nodeRuntimeFlagsFor('20.10.0')).toEqual([]);
|
|
expect(nodeRuntimeFlagsFor('21.2.0')).toEqual([]);
|
|
expect(nodeRuntimeFlagsFor('20.11.0')).toContain('--disable-warning=ExperimentalWarning');
|
|
expect(nodeRuntimeFlagsFor('21.3.0')).toContain('--disable-warning=ExperimentalWarning');
|
|
expect(nodeRuntimeFlagsFor('22.5.0')).toContain('--disable-warning=ExperimentalWarning');
|
|
});
|
|
|
|
it('is NOT required by the re-exec gate (old-launcher compat)', () => {
|
|
// An installed bundle launcher that passes only the WASM flags must not
|
|
// trigger a pointless re-exec over a cosmetic warning flag.
|
|
expect(processHasWasmRuntimeFlags(['--liftoff-only'])).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('processHasWasmRuntimeFlags', () => {
|
|
it('is true only when every required flag is present', () => {
|
|
expect(processHasWasmRuntimeFlags(['--liftoff-only'])).toBe(true);
|
|
expect(processHasWasmRuntimeFlags(['--liftoff-only', '--enable-source-maps'])).toBe(true);
|
|
});
|
|
|
|
it('is false when the flags are absent', () => {
|
|
expect(processHasWasmRuntimeFlags([])).toBe(false);
|
|
expect(processHasWasmRuntimeFlags(['--max-old-space-size=4096'])).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('buildRelaunchArgv', () => {
|
|
it('places our flags first, then the script and its args', () => {
|
|
expect(buildRelaunchArgv('/x/codegraph.js', ['index', '/repo'], [])).toEqual([
|
|
...NODE_RUNTIME_FLAGS,
|
|
'--liftoff-only',
|
|
'/x/codegraph.js',
|
|
'index',
|
|
'/repo',
|
|
]);
|
|
});
|
|
|
|
it('preserves other existing node flags without duplicating ours', () => {
|
|
expect(
|
|
buildRelaunchArgv('/x/codegraph.js', ['status'], [
|
|
'--liftoff-only',
|
|
...NODE_RUNTIME_FLAGS,
|
|
'--enable-source-maps',
|
|
])
|
|
).toEqual([
|
|
...NODE_RUNTIME_FLAGS,
|
|
'--liftoff-only',
|
|
'--enable-source-maps',
|
|
'/x/codegraph.js',
|
|
'status',
|
|
]);
|
|
});
|
|
|
|
it('produces an argv that actually launches node WITH the flag applied', () => {
|
|
// End-to-end proof of the delivery mechanism without needing the crash:
|
|
// run the constructed argv and confirm the child sees the flag in execArgv.
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-relaunch-'));
|
|
try {
|
|
const harness = path.join(dir, 'harness.cjs');
|
|
fs.writeFileSync(harness, 'process.stdout.write(JSON.stringify(process.execArgv));');
|
|
const res = spawnSync(process.execPath, buildRelaunchArgv(harness, []), { encoding: 'utf8' });
|
|
expect(res.status, res.stderr).toBe(0);
|
|
expect(JSON.parse(res.stdout)).toContain('--liftoff-only');
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|