wasm-runtime-flags.test.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. /**
  2. * WASM runtime flags — the workaround for the V8 turboshaft WASM Zone OOM
  3. * (`Fatal process out of memory: Zone`) that crashed `codegraph index` on large
  4. * polyglot repos under Node >= 22. See issues #293 and #298.
  5. *
  6. * The crash was reproduced with the real indexer on the bundled Node 24 runtime;
  7. * empirically only `--liftoff-only` prevents it (`--no-wasm-tier-up` /
  8. * `--no-wasm-dynamic-tiering` do not), and the flag must be on node's command
  9. * line — `setFlagsFromString`, worker `execArgv`, and `NODE_OPTIONS` all fail.
  10. * These tests pin that contract so it can't silently regress.
  11. */
  12. import { describe, it, expect } from 'vitest';
  13. import { spawnSync } from 'child_process';
  14. import * as fs from 'fs';
  15. import * as os from 'os';
  16. import * as path from 'path';
  17. import {
  18. WASM_RUNTIME_FLAGS,
  19. NODE_RUNTIME_FLAGS,
  20. nodeRuntimeFlagsFor,
  21. processHasWasmRuntimeFlags,
  22. buildRelaunchArgv,
  23. } from '../src/extraction/wasm-runtime-flags';
  24. describe('WASM_RUNTIME_FLAGS', () => {
  25. it('pins --liftoff-only (the only flag shown to stop the turboshaft Zone OOM)', () => {
  26. // On Node 24, --no-wasm-tier-up and --no-wasm-dynamic-tiering both still
  27. // crash; only --liftoff-only forces grammars onto the Liftoff baseline and
  28. // off the optimizing tier. Pin it so it can't be swapped for an ineffective
  29. // flag.
  30. expect(WASM_RUNTIME_FLAGS).toContain('--liftoff-only');
  31. });
  32. it('every flag is a real, accepted flag on the running Node/V8 runtime', () => {
  33. // node rejects unknown CLI flags at startup, so a renamed/removed flag would
  34. // break the bundled launcher and make the relaunch guard a silent no-op.
  35. // Prove each flag actually launches node here.
  36. const res = spawnSync(
  37. process.execPath,
  38. [...WASM_RUNTIME_FLAGS, '-e', 'process.exit(0)'],
  39. { encoding: 'utf8' }
  40. );
  41. expect(res.status, `node rejected ${WASM_RUNTIME_FLAGS.join(' ')}:\n${res.stderr}`).toBe(0);
  42. });
  43. });
  44. describe('NODE_RUNTIME_FLAGS', () => {
  45. it('suppresses the node:sqlite ExperimentalWarning on this runtime', () => {
  46. // The warning is emitted once per THREAD (main + every parse worker), so
  47. // during indexing it repeatedly interleaves with the progress UI. Prove
  48. // the flag both launches node and actually silences the warning.
  49. expect(NODE_RUNTIME_FLAGS).toContain('--disable-warning=ExperimentalWarning');
  50. const res = spawnSync(
  51. process.execPath,
  52. [...NODE_RUNTIME_FLAGS, '-e', "require('node:sqlite'); process.exit(0)"],
  53. { encoding: 'utf8' }
  54. );
  55. expect(res.status, res.stderr).toBe(0);
  56. expect(res.stderr).not.toMatch(/ExperimentalWarning/);
  57. });
  58. it('is empty on nodes too old for --disable-warning (fatal "bad option" there)', () => {
  59. expect(nodeRuntimeFlagsFor('20.10.0')).toEqual([]);
  60. expect(nodeRuntimeFlagsFor('21.2.0')).toEqual([]);
  61. expect(nodeRuntimeFlagsFor('20.11.0')).toContain('--disable-warning=ExperimentalWarning');
  62. expect(nodeRuntimeFlagsFor('21.3.0')).toContain('--disable-warning=ExperimentalWarning');
  63. expect(nodeRuntimeFlagsFor('22.5.0')).toContain('--disable-warning=ExperimentalWarning');
  64. });
  65. it('is NOT required by the re-exec gate (old-launcher compat)', () => {
  66. // An installed bundle launcher that passes only the WASM flags must not
  67. // trigger a pointless re-exec over a cosmetic warning flag.
  68. expect(processHasWasmRuntimeFlags(['--liftoff-only'])).toBe(true);
  69. });
  70. });
  71. describe('processHasWasmRuntimeFlags', () => {
  72. it('is true only when every required flag is present', () => {
  73. expect(processHasWasmRuntimeFlags(['--liftoff-only'])).toBe(true);
  74. expect(processHasWasmRuntimeFlags(['--liftoff-only', '--enable-source-maps'])).toBe(true);
  75. });
  76. it('is false when the flags are absent', () => {
  77. expect(processHasWasmRuntimeFlags([])).toBe(false);
  78. expect(processHasWasmRuntimeFlags(['--max-old-space-size=4096'])).toBe(false);
  79. });
  80. });
  81. describe('buildRelaunchArgv', () => {
  82. it('places our flags first, then the script and its args', () => {
  83. expect(buildRelaunchArgv('/x/codegraph.js', ['index', '/repo'], [])).toEqual([
  84. ...NODE_RUNTIME_FLAGS,
  85. '--liftoff-only',
  86. '/x/codegraph.js',
  87. 'index',
  88. '/repo',
  89. ]);
  90. });
  91. it('preserves other existing node flags without duplicating ours', () => {
  92. expect(
  93. buildRelaunchArgv('/x/codegraph.js', ['status'], [
  94. '--liftoff-only',
  95. ...NODE_RUNTIME_FLAGS,
  96. '--enable-source-maps',
  97. ])
  98. ).toEqual([
  99. ...NODE_RUNTIME_FLAGS,
  100. '--liftoff-only',
  101. '--enable-source-maps',
  102. '/x/codegraph.js',
  103. 'status',
  104. ]);
  105. });
  106. it('produces an argv that actually launches node WITH the flag applied', () => {
  107. // End-to-end proof of the delivery mechanism without needing the crash:
  108. // run the constructed argv and confirm the child sees the flag in execArgv.
  109. const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-relaunch-'));
  110. try {
  111. const harness = path.join(dir, 'harness.cjs');
  112. fs.writeFileSync(harness, 'process.stdout.write(JSON.stringify(process.execArgv));');
  113. const res = spawnSync(process.execPath, buildRelaunchArgv(harness, []), { encoding: 'utf8' });
  114. expect(res.status, res.stderr).toBe(0);
  115. expect(JSON.parse(res.stdout)).toContain('--liftoff-only');
  116. } finally {
  117. fs.rmSync(dir, { recursive: true, force: true });
  118. }
  119. });
  120. });