Skip to content

perf(startup): enable V8 compile cache for the main process and children - #19650

Open
espetro wants to merge 3 commits into
stablyai:mainfrom
espetro:upstream/native-compile-cache
Open

espetro wants to merge 3 commits into
stablyai:mainfrom
espetro:upstream/native-compile-cache

Conversation

@espetro

@espetro espetro commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

Node re-parses the same JavaScript on every app launch. This PR turns on Node's on-disk compile cache so warm starts skip parsing/compiling for the main process and its child processes.

What

Enables module.enableCompileCache() for the main process and its child processes at startup.

Why (and why the original placement was wrong)

CodeRabbit correctly flagged that the first version called enableCompileCache() at the tail of preflight — after index.ts's entire static import graph had already evaluated. Since the API is not retroactive, the main process's own startup (the headline claim) was never cached. 4490ea0 re-architects:

  • Build-time banner: a new createCompileCacheBanner() is prepended to out/main/index.js by createMainBootstrapPlugin() — the same generateBundle mechanism already used for the bootstrap-fatal-exit and startup-diagnostics banners. It executes before Rollup's chunk requires, so the whole main graph is cached from the first launch. Verified in the built bundle: banner at byte ~3.3k, module graph begins at ~8.1k.
  • No-arg call: Node's default cache dir (per-user, outside userData) is used deliberately — preflight redirects userData for dev/E2E only after this banner runs; a no-arg enable is immune to that ordering, and chunk cache entries are keyed by content hash so they are safely shared across profiles.
  • Child processes: the banner pins process.env.NODE_COMPILE_CACHE to the resolved directory so forked children (daemon, plugin-host, sidecars) reuse the cache; the runtime enableMainProcessCompileCache() remains as an idempotent fallback for banner-less entry paths.
  • Dropped configureSessionCodeCache — session.setCodeCachePath to Electron's own default Code Cache location was a runtime no-op (h/t review).

Visual proof

N/A — no UI change; startup timing only.

Testing

  • pnpm vitest run src/main/startup/native-code-cache.test.ts — 3 passing (idempotency, env pinning, fallback contract)
  • pnpm tsc --noEmit -p config/tsconfig.node.json — clean
  • pnpm build:electron-vite — banner verified in built output before the module graph; banner IIFE executed standalone sets NODE_COMPILE_CACHE to the default cache dir

Platform coverage

  • macOS/Linux/Windows: no platform-specific code paths; cache dir resolution is Node's own
  • Child-process inheritance relies on env vars, consistent across platforms

Compatibility

  • No IPC, wire, Git, or process-spawn changes
  • enableCompileCache is a no-op guarded by a feature check on Node versions without it

AI disclosure

Authored with AI assistance (Claude); human-reviewed.

Review notes

  • Second review round flagged a misleading test that never exercised the fallback branch (process-global one-shot API) — tests were rewritten to test the actual wrapper contract (idempotency + result shape) rather than pretend to reset process-global state

Checklist

  • Cross-platform (macOS/Linux/Windows)
  • No new dependencies
  • Tests updated for the final architecture

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Enabling the compile cache at the tail of runMainProcessPreflight means the main process's own module graph is never cached — the main-process half of the stated goal isn't achieved by the current placement. See the inline note.

Reviewed changes

Clears up how this PR's two mechanisms actually behave at runtime:

  • Main-process compile cache — enableMainProcessCompileCache() calls module.enableCompileCache(dir), then re-exports the resolved directory via process.env.NODE_COMPILE_CACHE so child processes inherit it.
  • Session code-cache path — configureSessionCodeCache() pins session.defaultSession's V8 code cache location to <userData>/Code Cache.
  • Tests — cover the happy path, the env-var round-trip, and graceful failure, plus a setCodeCachePath mock assertion.

ℹ️ Nitpicks

  • app.getPath('userData') is a persistent, non-cleared directory, but Node's docs recommend a directory under os.tmpdir() for the compile cache to avoid filling the disk with stale entries. Since the cache is invalidated per Node version (not per app version), entries for modules that move or disappear across app updates will accumulate there. Consider whether a temp dir is a better default, or accept the growth and document it.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/startup/main-process-preflight.ts
Comment thread src/main/startup/native-code-cache.ts Outdated
Comment thread src/main/startup/native-code-cache.test.ts Outdated
@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7f56578c-5a22-4593-a1bd-6b720b1836c6
📥 Commits

Reviewing files that changed from the base of the PR and between 829da71 and 8e6efd4.

📒 Files selected for processing (2)
  • electron.vite.config.ts
  • src/main/startup/main-process-preflight.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The main bootstrap inserts a compile-cache banner before startup diagnostics. The banner calls Node’s compile-cache API when available and sets NODE_COMPILE_CACHE when the API returns a directory. Main-process preflight invokes enableMainProcessCompileCache after crash diagnostics setup. Vitest tests cover explicit and default initialization, environment propagation, repeated calls, and result fields.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to 8e6ef

The banner is configured to run before the main bundle, addressing the warm-start concern. The remaining risk is a bounded test gap: the fallback test may pass after earlier cache initialization without checking fallback selection, so a regression could go undetected.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes enabling the V8 compile cache for the main process and child processes.
Description check ✅ Passed The description explains the change, its rationale, implementation, testing, compatibility, and lack of UI changes. The linked issue field is blank, and several template checklist items are omitted or…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: ed09f6d8-f424-4f2f-ad2f-c6dddf29132e

📥 Commits

Reviewing files that changed from the base of the PR and between 12f2c6b and d7ee118.

📒 Files selected for processing (4)
  • src/main/startup/main-process-preflight.ts
  • src/main/startup/main-process-ready-foundation.ts
  • src/main/startup/native-code-cache.test.ts
  • src/main/startup/native-code-cache.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.

recordBreadcrumb: (data) => recordDurableCrashBreadcrumb('gpu_crash_hardware', data)
})
: null
enableMainProcessCompileCache()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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:

get_repo_knowledge stablyai/orca /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/learnings /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/conventions

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 -240

Repository: 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 -260

Repository: 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 -80

Repository: 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 -120

Repository: stablyai/orca

Length of output: 5647


🌐 Web query:

Node.js module.enableCompileCache modules compiled after enabled official documentation v24

💡 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.

src/main/index.ts imports runMainProcessPreflight, which imports enableMainProcessCompileCache. Line 305 enables the cache only after this graph has loaded, so warm launches cannot use it for the main startup modules. Initialize the cache in an entry shim before importing the bootstrap modules.

})

it('enables compile cache and sets process.env.NODE_COMPILE_CACHE', () => {
const result = enableMainProcessCompileCache('/tmp/mock-cache')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🤖 get_repo_knowledge executed:

get_repo_knowledge stablyai/orca /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/learnings /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/conventions

Length of output: 20402


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- test file ---'
cat -n src/main/startup/native-code-cache.test.ts
printf '%s\n' '--- implementation and direct references ---'
rg -n -C 8 'enableMainProcessCompileCache|configureSessionCodeCache|NODE_COMPILE_CACHE|ALREADY_ENABLED|compile-cache' src/main

Repository: stablyai/orca

Length of output: 17044


🌐 Web query:

Node.js v24 module.enableCompileCache ALREADY_ENABLED directory documentation

💡 Result:

In Node.js, ALREADY_ENABLED is a status value returned by the module.enableCompileCache function [1][2][3]. It indicates that the module compile cache has already been initialized in the current process [1][4][5]. When module.enableCompileCache returns a status of ALREADY_ENABLED, the returned object also includes a directory property [1][6][5]. This property specifies the absolute path to the directory currently being used to store the compile cache [1][7][5]. The compile cache is considered already enabled if it was previously activated by: 1. A prior call to module.enableCompileCache within the same process [1][7][4]. 2. The presence of the NODE_COMPILE_CACHE environment variable [1][2][7]. The compile cache feature is designed as a non-critical optimization [4][3][5]. Therefore, calling module.enableCompileCache when it is ALREADY_ENABLED does not throw an exception; instead, it provides the existing configuration details through the returned object to assist with debugging or verification [7][4][3].

Citations:


Isolate the compile-cache tests from process-global state.

The first test enables Node’s process-global compile cache at /tmp/mock-cache. The fallback test can then receive ALREADY_ENABLED and reuse that directory instead of testing the userData/compile-cache fallback. Mock node:module or run these tests in an isolated process, then assert the exact fallback directory.

Comment on lines +52 to +54
it('returns false gracefully if setCodeCachePath throws or is missing', () => {
expect(configureSessionCodeCache({} as Pick<Session, 'setCodeCachePath'>)).toBe(false)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise the throwing branch.

The test passes an object without setCodeCachePath. It exits through the guard. The catch for a throwing setCodeCachePath remains untested. Add a session mock whose method throws.

Proposed test addition
 it('returns false gracefully if setCodeCachePath throws or is missing', () => {
   expect(configureSessionCodeCache({} as Pick<Session, 'setCodeCachePath'>)).toBe(false)
+  expect(
+    configureSessionCodeCache({
+      setCodeCachePath: () => {
+        throw new Error('cache path unavailable')
+      }
+    })
+  ).toBe(false)
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('returns false gracefully if setCodeCachePath throws or is missing', () => {
expect(configureSessionCodeCache({} as Pick<Session, 'setCodeCachePath'>)).toBe(false)
})
it('returns false gracefully if setCodeCachePath throws or is missing', () => {
expect(configureSessionCodeCache({} as Pick<Session, 'setCodeCachePath'>)).toBe(false)
expect(
configureSessionCodeCache({
setCodeCachePath: () => {
throw new Error('cache path unavailable')
}
})
).toBe(false)
})

@espetro

espetro commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 4490ea0 addressing the placement blocker and the other two threads:

Placement (the blocker) — you're right that the preflight call was too late. The enable now lives in a build-time banner prepended to out/main/index.js via the existing createMainBootstrapPlugin generateBundle hook (same mechanism as the bootstrap-fatal-exit banner), so it executes before Rollup's chunk requires and the main process's own startup graph is actually cached. Verified in the built bundle: banner at ~3.3kB, graph requires start at ~8.1kB. The no-arg form intentionally uses Node's default cache dir: preflight's dev/E2E userData redirect happens later, and no-arg sidesteps that ordering entirely — cache entries are content-hash keyed and identical chunks are shared across profiles.

configureSessionCodeCache — agreed, dropped. It set Electron's own default path.

Test isolation — rewritten. The old fallback test could never fail; new tests cover the wrapper's actual contract (idempotent repeat-enable keeps enabled: true, env pinning, result shape) instead of pretending to reset a process-global API.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one minor cross-platform nitpick inline.

Reviewed changes

  • Added a build-time createCompileCacheBanner() that createMainBootstrapPlugin() prepends to out/main/index.js, calling module.enableCompileCache() before the main module graph evaluates and pinning NODE_COMPILE_CACHE for forked children — resolving the prior "enabled too late" finding.
  • Removed configureSessionCodeCache() and its main-process-ready-foundation.ts call site (a runtime no-op).
  • Rewrote native-code-cache.test.ts to assert the wrapper contract (idempotency + result shape) instead of pretending to exercise the process-global one-shot fallback.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/startup/native-code-cache.test.ts Outdated
@espetro

espetro commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Portable test paths pushed (os.tmpdir()-derived) — the hardcoded /tmp/mock-cache would indeed have hit the drive-root mkdir problem on Windows.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Test path portability — replaced the hardcoded /tmp/mock-cache cache dir with join(tmpdir(), 'orca-compile-cache-test') in the compile-cache tests, resolving the prior cross-platform nitpick (the Unix-only path broke on Windows where it resolved to <drive>:\tmp\mock-cache).

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

…uates

CodeRabbit blocker: module.enableCompileCache() is not retroactive — the
call at the tail of runMainProcessPreflight only saw modules compiled
after it, so the main process's own startup graph was never cached.

Move the enable to a build-time banner prepended to out/main/index.js
(same generateBundle mechanism as the bootstrap-fatal-exit and startup-
diagnostics banners). It runs before Rollup's chunk requires, so the
entire main graph is cached from the first launch. The no-arg call uses
Node's default cache dir outside userData, which keeps it correct across
the dev/E2E userData redirect that happens later in preflight; chunk
cache entries are keyed by content hash and shared across profiles.

The runtime enableMainProcessCompileCache() stays as an idempotent
fallback that pins NODE_COMPILE_CACHE for forked children (daemon,
plugin-host, sidecars); the banner sets the same env var on the normal
path. Drop configureSessionCodeCache — setCodeCachePath to the default
'Code Cache' location was a runtime no-op.
Hardcoded /tmp/mock-cache resolves to a drive root on Windows where
mkdirSync can fail; derive from os.tmpdir() instead.
@devin-ai-integration
devin-ai-integration Bot force-pushed the upstream/native-compile-cache branch from 829da71 to 8e6efd4 Compare October 7, 2026 10:40

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant