fix(ui): show synthesis as a 'Linking dynamic dispatch' phase; mute node:sqlite warning spam (#1299)
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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
243ef1d3e2
commit
ad5300a601
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Progress reporting for the callback-edge synthesis tail.
|
||||
*
|
||||
* Synthesis runs AFTER the resolution bar reaches 100%, so before this it had
|
||||
* no progress surface at all — on synthesizer-heavy repos (e.g. large C
|
||||
* codebases hitting the fn-pointer pass) the CLI sat frozen at
|
||||
* "Resolving refs 100%" long enough that users concluded the index hung and
|
||||
* killed it. These tests pin (a) that indexing emits the dedicated 'linking'
|
||||
* phase with monotonic per-pass progress, and (b) that the advertised step
|
||||
* total stays in sync with the synthesizer's actual pass list.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph, IndexProgress } from '../src/index';
|
||||
import { SYNTH_PROGRESS_STEPS } from '../src/resolution/callback-synthesizer';
|
||||
|
||||
describe('synthesis progress ("Linking dynamic dispatch" phase)', () => {
|
||||
it('SYNTH_PROGRESS_STEPS matches the synthesizer’s actual __mark() step count', () => {
|
||||
// The constant is cosmetic (progress denominator), but drift makes the bar
|
||||
// end early or jump to 100% — adding a pass must bump it. Every step site
|
||||
// calls __mark('<label>') with a string literal, so count those.
|
||||
const src = fs.readFileSync(
|
||||
path.join(__dirname, '../src/resolution/callback-synthesizer.ts'),
|
||||
'utf8'
|
||||
);
|
||||
const stepSites = (src.match(/__mark\('/g) ?? []).length;
|
||||
expect(SYNTH_PROGRESS_STEPS).toBe(stepSites);
|
||||
});
|
||||
|
||||
it('indexing emits a monotonic linking phase ending at the full step count', async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-synth-progress-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, 'a.ts'), 'export function helper() { return 1; }\n');
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'b.ts'),
|
||||
"import { helper } from './a';\nexport function main() { return helper(); }\n"
|
||||
);
|
||||
|
||||
const events: IndexProgress[] = [];
|
||||
const cg = await CodeGraph.init(dir, {
|
||||
index: true,
|
||||
onProgress: (p) => events.push(p),
|
||||
});
|
||||
await cg.close();
|
||||
|
||||
const linking = events.filter((e) => e.phase === 'linking');
|
||||
expect(linking.length).toBeGreaterThan(0);
|
||||
// Emitted up-front so the phase label flips as soon as synthesis starts…
|
||||
expect(linking[0]!.current).toBe(0);
|
||||
// …and every step reports against the same total, monotonically.
|
||||
expect(linking.every((e) => e.total === SYNTH_PROGRESS_STEPS)).toBe(true);
|
||||
for (let i = 1; i < linking.length; i++) {
|
||||
expect(linking[i]!.current).toBeGreaterThanOrEqual(linking[i - 1]!.current);
|
||||
}
|
||||
expect(linking[linking.length - 1]!.current).toBe(SYNTH_PROGRESS_STEPS);
|
||||
|
||||
// The linking phase comes after resolution has finished.
|
||||
const lastResolving = events.map((e) => e.phase).lastIndexOf('resolving');
|
||||
const firstLinking = events.map((e) => e.phase).indexOf('linking');
|
||||
expect(firstLinking).toBeGreaterThan(lastResolving);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,8 @@ 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';
|
||||
@@ -42,6 +44,36 @@ describe('WASM_RUNTIME_FLAGS', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -55,8 +87,9 @@ describe('processHasWasmRuntimeFlags', () => {
|
||||
});
|
||||
|
||||
describe('buildRelaunchArgv', () => {
|
||||
it('places the wasm flags first, then the script and its args', () => {
|
||||
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',
|
||||
@@ -66,8 +99,18 @@ describe('buildRelaunchArgv', () => {
|
||||
|
||||
it('preserves other existing node flags without duplicating ours', () => {
|
||||
expect(
|
||||
buildRelaunchArgv('/x/codegraph.js', ['status'], ['--liftoff-only', '--enable-source-maps'])
|
||||
).toEqual(['--liftoff-only', '--enable-source-maps', '/x/codegraph.js', 'status']);
|
||||
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', () => {
|
||||
|
||||
Reference in New Issue
Block a user