Repository navigation
perf(startup): enable V8 compile cache for the main process and children #19650
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ import { | |
| import { getDevInstanceIdentity, shouldApplyPreReadyAppName } from './dev-instance-identity' | ||
| import { enableRendererHeapHeadroom } from './renderer-heap-headroom' | ||
| import { configureLinuxDevShmUsage } from './linux-dev-shm-policy' | ||
| import { enableMainProcessCompileCache } from './native-code-cache' | ||
| import { isStartupDiagnosticsEnabled, logStartupDiagnostic } from './startup-diagnostics' | ||
| import { startEventLoopStallProbe } from './event-loop-stall-probe' | ||
| import { | ||
|
|
@@ -365,6 +366,9 @@ function initializeMainProcessPreflight(options: MainProcessPreflightOptions): b | |
| startCrashpadCapture() | ||
| state.crashReports = CrashReportStore.fromUserData() | ||
| state.gpuCrashDiagnostics = createGpuCrashDiagnosticsRecorder() | ||
| // Why: the main graph is already cached by the build-time banner; this only pins | ||
| // NODE_COMPILE_CACHE for forked children and covers banner-less entry paths. | ||
| enableMainProcessCompileCache() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🤖 get_repo_knowledge executed:
Length of output: 37238 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- changed file context ---'
sed -n '1,80p;270,325p' src/main/startup/main-process-preflight.ts
printf '%s\n' '--- helper definition and usages ---'
rg -n -C 8 'enableMainProcessCompileCache' .
printf '%s\n' '--- main entry candidates and imports ---'
rg -n -C 4 'main-process-preflight|bootstrap|startup' src/main package.json 2>/dev/null | head -240Repository: stablyai/orca Length of output: 38689 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- compile-cache implementation ---'
cat -n src/main/startup/native-code-cache.ts
printf '%s\n' '--- preflight declaration and call sites ---'
rg -n -C 12 'export (async )?function|function .*preflight|runMainProcessPreflight|main-process-preflight' src/main --glob '*.ts' --glob '!*.test.ts'
printf '%s\n' '--- package entry configuration ---'
rg -n -C 8 '"main"|electron|src/main' package.json electron-builder.yml* forge.config.* config src --glob '*.json' --glob '*.yml' --glob '*.yaml' --glob '*.ts' --glob '*.mjs' 2>/dev/null | head -260Repository: stablyai/orca Length of output: 50370 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- compile-cache implementation ---'
cat -n src/main/startup/native-code-cache.ts
printf '%s\n' '--- exact preflight references ---'
rg -n --glob '*.ts' --glob '*.tsx' --glob '*.mjs' --glob '*.js' 'main-process-preflight' . | head -80
printf '%s\n' '--- package main fields ---'
rg -n -C 3 '"main"|"build"|"files"' package.json
printf '%s\n' '--- likely main entry files ---'
git ls-files 'src/main/*' | head -80Repository: stablyai/orca Length of output: 10888 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- main entry ---'
cat -n src/main/index.ts | sed -n '1,90p'
printf '%s\n' '--- preflight declaration and relevant beginning ---'
rg -n -C 10 'runMainProcessPreflight|export const|export function|export async function' src/main/startup/main-process-preflight.ts | head -120Repository: stablyai/orca Length of output: 5647 🌐 Web query:
💡 Result: In Node.js v24, the module compile cache is a feature that allows Node.js to persist V8 code cache on disk for CommonJS, ECMAScript, and TypeScript modules to speed up subsequent compilations [1][2]. Once enabled, it improves the performance of module loading by reducing repeated compilation work, though it may introduce a slight overhead during the initial load of a module graph [1][3]. To enable this feature, you can use the module.enableCompileCache method or the NODE_COMPILE_CACHE environment variable [1][4]. Method Usage: module.enableCompileCache([options]) The method accepts an optional object: - directory (string, optional): The location to store the compile cache. If not provided, Node.js defaults to the path specified by the NODE_COMPILE_CACHE environment variable, or path.join(os.tmpdir, 'node-compile-cache') if that variable is unset [1][3]. - portable (boolean, optional): If set to true, this enables portable compile cache, allowing the cache to be reused even if the project directory is moved (a best-effort feature) [1][2]. Recommendations and Behavior: - For general use, it is recommended to call module.enableCompileCache without arguments to allow the directory to be overridden by the NODE_COMPILE_CACHE environment variable when necessary [1][4]. - Enabling the cache only affects the current Node.js instance. To extend it to child worker threads, you must either call the method within those threads or set the NODE_COMPILE_CACHE environment variable in the parent process, which can be inherited by the children [1][4][2]. - The API is designed not to throw exceptions; it is intended to be a quiet optimization. The method returns an object containing information such as the status (one of module.constants.compileCacheStatus) and the directory being used [1][5]. - To locate the compile cache directory used by a running instance, you can use the module.getCompileCacheDir method [4][2]. - You can clear the cache at any time by deleting the directory specified for storage; it will be recreated automatically the next time it is needed [1][6]. Citations:
Enable Node’s compile cache before importing the main startup graph.
|
||
| recordCrashBreadcrumb('app_started', { | ||
| packaged: app.isPackaged, | ||
| platform: process.platform, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { tmpdir } from 'node:os' | ||
| import { join } from 'node:path' | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' | ||
| import { enableMainProcessCompileCache } from './native-code-cache' | ||
|
|
||
| describe('enableMainProcessCompileCache', () => { | ||
| const originalEnv = process.env.NODE_COMPILE_CACHE | ||
|
|
||
| beforeEach(() => { | ||
| delete process.env.NODE_COMPILE_CACHE | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| if (originalEnv !== undefined) { | ||
| process.env.NODE_COMPILE_CACHE = originalEnv | ||
| } else { | ||
| delete process.env.NODE_COMPILE_CACHE | ||
| } | ||
| vi.restoreAllMocks() | ||
| }) | ||
|
|
||
| it('enables compile cache and sets process.env.NODE_COMPILE_CACHE', () => { | ||
| const result = enableMainProcessCompileCache(join(tmpdir(), 'orca-compile-cache-test')) | ||
| expect(result.enabled).toBe(true) | ||
| expect(result.directory).toBeTruthy() | ||
| expect(process.env.NODE_COMPILE_CACHE).toBe(result.directory) | ||
| }) | ||
|
|
||
| it('is idempotent: repeat calls keep reporting the enabled state without throwing', () => { | ||
| const cacheDir = join(tmpdir(), 'orca-compile-cache-test') | ||
| const first = enableMainProcessCompileCache(cacheDir) | ||
| const second = enableMainProcessCompileCache(cacheDir) | ||
| expect(first.enabled).toBe(true) | ||
| // Node returns ALREADY_ENABLED with the original directory; the wrapper must | ||
| // still report enabled so callers cannot mistake the state for a failure. | ||
| expect(second.enabled).toBe(true) | ||
| expect(second.directory).toBeTruthy() | ||
| }) | ||
|
|
||
| it('exposes the runtime enable result shape for the build-banner fallback path', () => { | ||
| // The build-time banner (electron.vite.config.ts) enables the cache before the | ||
| // main graph evaluates; this module is the in-process fallback. Both must agree | ||
| // on the contract: enabled + directory, never throw on a missing cache dir. | ||
| const result = enableMainProcessCompileCache() | ||
| expect(result.enabled).toBe(true) | ||
| expect(typeof result.directory).toBe('string') | ||
| expect(result.error).toBeUndefined() | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { join } from 'node:path' | ||
| import { app } from 'electron' | ||
|
|
||
| export type NativeCodeCacheResult = { | ||
| enabled: boolean | ||
| directory: string | null | ||
| error?: string | ||
| } | ||
|
|
||
| // Why: the main-process graph itself is cached by the build-time compile-cache banner | ||
| // prepended to out/main/index.js (see electron.vite.config.ts) — this runtime call is | ||
| // only the idempotent fallback for entry paths that skip the banner. It pins the | ||
| // resolved cache dir into NODE_COMPILE_CACHE so forked children reuse the same cache. | ||
| export function enableMainProcessCompileCache(customCacheDir?: string): NativeCodeCacheResult { | ||
| try { | ||
| const nodeModule = require('node:module') as { | ||
| enableCompileCache?: (dir?: string) => { status: number; directory: string } | ||
| } | ||
|
|
||
| if (typeof nodeModule.enableCompileCache !== 'function') { | ||
| return { enabled: false, directory: null } | ||
| } | ||
|
|
||
| const cacheDir = | ||
| customCacheDir ?? | ||
| process.env.NODE_COMPILE_CACHE ?? | ||
| (typeof app?.getPath === 'function' | ||
| ? join(app.getPath('userData'), 'compile-cache') | ||
| : undefined) | ||
|
|
||
| const result = nodeModule.enableCompileCache(cacheDir) | ||
| if (result && result.directory) { | ||
| // Why: child processes (daemon, plugin-host, watcher) inherit env and automatically use compile cache. | ||
| process.env.NODE_COMPILE_CACHE = result.directory | ||
| return { enabled: true, directory: result.directory } | ||
| } | ||
|
|
||
| return { enabled: false, directory: null } | ||
| } catch (error) { | ||
| return { | ||
| enabled: false, | ||
| directory: null, | ||
| error: error instanceof Error ? error.message : String(error) | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.