diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 263268833ba1..e73d84cdd524 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -181,6 +181,32 @@ function createStartupDiagnosticsBanner(chunkName: string): string { ` } +function createCompileCacheBanner(): string { + // Why here and not in main-process-preflight: module.enableCompileCache() is not + // retroactive — by preflight time the entire static import graph of index.ts has + // already been evaluated, so nothing of the main process's own startup would be + // cached. Prepending to the bundle entry caches the real startup graph. + // No-arg call uses Node's default cache dir (per-user, outside userData), so the + // later dev/E2E userData redirect cannot misplace it; entries are keyed by chunk + // content hash, which is identical across profiles. + return ` +;(() => { + try { + const nodeModule = require('node:module') + if (typeof nodeModule.enableCompileCache === 'function') { + const result = nodeModule.enableCompileCache() + if (result && result.directory && typeof process !== 'undefined' && process.env) { + // Why: child processes (daemon, plugin-host, sidecars) inherit env and reuse the cache. + process.env.NODE_COMPILE_CACHE = result.directory + } + } + } catch { + // A cache-dir failure must never block startup. + } +})(); +` +} + function createMainBootstrapPlugin() { return { name: 'orca-main-bootstrap', @@ -194,6 +220,7 @@ function createMainBootstrapPlugin() { // prelude, too late to handle a missing bootstrap dependency. mainChunk.code = createBootstrapFatalExitBanner() + + createCompileCacheBanner() + createStartupDiagnosticsBanner(mainChunk.fileName) + mainChunk.code } diff --git a/src/main/startup/main-process-preflight.ts b/src/main/startup/main-process-preflight.ts index 891f6576a289..bd3c69d66db7 100644 --- a/src/main/startup/main-process-preflight.ts +++ b/src/main/startup/main-process-preflight.ts @@ -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() recordCrashBreadcrumb('app_started', { packaged: app.isPackaged, platform: process.platform, diff --git a/src/main/startup/native-code-cache.test.ts b/src/main/startup/native-code-cache.test.ts new file mode 100644 index 000000000000..22589cbd71e8 --- /dev/null +++ b/src/main/startup/native-code-cache.test.ts @@ -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() + }) +}) diff --git a/src/main/startup/native-code-cache.ts b/src/main/startup/native-code-cache.ts new file mode 100644 index 000000000000..330a1c48f0a7 --- /dev/null +++ b/src/main/startup/native-code-cache.ts @@ -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) + } + } +}