Files
orca/electron.vite.config.ts
T
NeilandOrca cc078a5021 perf(main): move hang watchdog into a worker thread (#11488)
* perf(main): add watchdog boundary memory benchmark

Add a repeatable Electron 43 RSS harness that measures the production-built watchdog entry across the child-process and worker-thread boundaries. Record per-trial samples, the median, revision, runtime, and settling procedure for reproducible PR evidence.

* perf(main): move hang watchdog into a worker thread

Keep main-thread hang detection independent of the blocked Electron event loop without paying for a second ELECTRON_RUN_AS_NODE process. Preserve the marker and telemetry contract while moving timing configuration and heartbeats onto a bundled worker entry.

* test(main): smoke packaged hang watchdog worker

* fix(main): make packaged watchdog smoke able to fail

The smoke reported failure only through process.exitCode, but its finally
block quit Electron gracefully, and Electron takes its status from the
browser exit code. Every failure mode — entry missing from app.asar, worker
error, marker timeout, non-zero worker exit — exited 0 with the diagnostic
discarded on stderr, so the required PR check could never go red.

Propagate a real status via app.exit, assert the success line in stdout, and
surface stderr. Verified against a packaged tree with the entry removed:
exit 0 before, exit 1 after.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-30 19:33:05 -07:00

336 lines
13 KiB
TypeScript

import { isBuiltin } from 'node:module'
import { resolve } from 'node:path'
import { defineConfig, type UserConfig } from 'electron-vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { createPlainNodeEntryGuardPlugin } from './build-plugins/plain-node-entry-guard'
import packageJson from './package.json' with { type: 'json' }
const BUNDLED_MAIN_DEPENDENCIES = new Set([
'@xterm/headless',
'@xterm/addon-serialize',
// Why: Windows NSIS deploys app.asar before external resources; bootstrap must
// not race the later resources/node_modules copy.
'zod'
])
const EXTERNAL_MAIN_DEPENDENCIES = Object.keys(packageJson.dependencies).filter(
(dependency) => !BUNDLED_MAIN_DEPENDENCIES.has(dependency)
)
const BOOTSTRAP_FATAL_EXIT_GUARD_KEY = '__ORCA_BOOTSTRAP_FATAL_EXIT_GUARD__'
function isExternalMainModule(source: string): boolean {
if (isBuiltin(source) || source === 'electron' || source.startsWith('electron/')) {
return true
}
return EXTERNAL_MAIN_DEPENDENCIES.some(
(dependency) => source === dependency || source.startsWith(`${dependency}/`)
)
}
// Why: the telemetry transport is gated by two compile-time constants that
// only the official CI release workflow sets. Contributor / `pnpm dev` /
// third-party rebuilds must substitute literal `null` at these sites so
// `IS_OFFICIAL_BUILD` in `src/main/telemetry/client.ts` evaluates `false`
// at module load and the track() wrapper short-circuits to console-mirror.
// The substitution happens at compile time — there is no runtime env-var
// fallback — so a curious contributor cannot spoof transmission with a
// shell export.
//
// CI injects real values via GitHub Actions secrets
// (ORCA_BUILD_IDENTITY='stable' | 'rc', ORCA_POSTHOG_WRITE_KEY=phc_...);
// every other build path resolves these env vars to undefined, which the
// JSON.stringify below folds to the literal `null`. Ambient declarations
// for the two constants live in `src/types/build-constants.d.ts`.
const orcaBuildIdentity = process.env.ORCA_BUILD_IDENTITY
const ORCA_BUILD_IDENTITY_LITERAL =
orcaBuildIdentity === 'stable' || orcaBuildIdentity === 'rc'
? JSON.stringify(orcaBuildIdentity)
: 'null'
const orcaPostHogWriteKey = process.env.ORCA_POSTHOG_WRITE_KEY
const ORCA_POSTHOG_WRITE_KEY_LITERAL =
typeof orcaPostHogWriteKey === 'string' && orcaPostHogWriteKey.length > 0
? JSON.stringify(orcaPostHogWriteKey)
: 'null'
const orcaDiagnosticsTokenUrl = process.env.ORCA_DIAGNOSTICS_TOKEN_URL
const ORCA_DIAGNOSTICS_TOKEN_URL_LITERAL =
typeof orcaDiagnosticsTokenUrl === 'string' && orcaDiagnosticsTokenUrl.length > 0
? JSON.stringify(orcaDiagnosticsTokenUrl)
: 'null'
function createStartupDiagnosticsBanner(chunkName: string): string {
return `
;(() => {
const env = typeof process !== 'undefined' ? process.env : undefined
const mode = env?.ORCA_STARTUP_DIAGNOSTICS
if (mode !== '1' && mode !== 'trace') {
return
}
const safeJson = (value) => {
try {
return JSON.stringify(value)
} catch {
return '"<unserializable>"'
}
}
let closeSync
let diagnosticFileDescriptor
let openSync
let writeSync
try {
const fs = require('node:fs')
closeSync = fs.closeSync
openSync = fs.openSync
writeSync = fs.writeSync
} catch {
closeSync = undefined
openSync = undefined
writeSync = undefined
}
const diagnosticFile = env?.ORCA_STARTUP_DIAGNOSTICS_FILE
if (typeof diagnosticFile === 'string' && diagnosticFile.length > 0 && typeof openSync === 'function') {
try {
diagnosticFileDescriptor = openSync(diagnosticFile, 'a', 0o600)
} catch {
diagnosticFileDescriptor = undefined
}
}
const writeLine = (message) => {
try {
const line = message.endsWith('\\n') ? message : message + '\\n'
if (typeof writeSync === 'function') {
writeSync(2, line)
if (typeof diagnosticFileDescriptor === 'number') {
writeSync(diagnosticFileDescriptor, line)
}
}
} catch {
// Diagnostics must never affect startup.
}
}
const chunkName = ${JSON.stringify(chunkName)}
writeLine('[bootstrap] bundle-enter chunk=' + safeJson(chunkName) + ' pid=' + process.pid + ' ppid=' + process.ppid + ' execPath=' + safeJson(process.execPath) + ' argv=' + safeJson(process.argv) + ' electronRunAsNode=' + safeJson(env?.ELECTRON_RUN_AS_NODE ?? null))
if (!globalThis.__ORCA_BOOTSTRAP_EXIT_LOG_INSTALLED__) {
globalThis.__ORCA_BOOTSTRAP_EXIT_LOG_INSTALLED__ = true
process.once('exit', (code) => {
writeLine('[bootstrap] process-exit code=' + code)
if (typeof closeSync === 'function' && typeof diagnosticFileDescriptor === 'number') {
try {
closeSync(diagnosticFileDescriptor)
} catch {
// Diagnostics must never affect shutdown.
}
}
})
process.on('uncaughtExceptionMonitor', (error, origin) => {
const message = error && typeof error === 'object' && 'stack' in error ? error.stack : error
writeLine('[bootstrap] uncaught-exception origin=' + safeJson(origin) + ' error=' + safeJson(String(message)))
})
process.on('unhandledRejection', (reason) => {
const message = reason && typeof reason === 'object' && 'stack' in reason ? reason.stack : reason
writeLine('[bootstrap] unhandled-rejection error=' + safeJson(String(message)))
})
}
if (mode === 'trace' && !globalThis.__ORCA_BOOTSTRAP_REQUIRE_TRACE_INSTALLED__) {
globalThis.__ORCA_BOOTSTRAP_REQUIRE_TRACE_INSTALLED__ = true
try {
const Module = require('node:module')
const originalLoad = Module._load
const parsedTraceLimit = Number(env?.ORCA_STARTUP_DIAGNOSTICS_TRACE_LIMIT ?? 20000)
const traceLimit = Number.isFinite(parsedTraceLimit) && parsedTraceLimit > 0 ? parsedTraceLimit : 20000
let traceLineCount = 0
let traceLimitReported = false
const writeTraceLine = (message) => {
if (traceLineCount >= traceLimit) {
if (!traceLimitReported) {
traceLimitReported = true
writeLine('[bootstrap] require-trace-limit-reached limit=' + safeJson(traceLimit))
}
return
}
traceLineCount += 1
writeLine(message)
}
Module._load = function (request, parent, isMain) {
const parentName = parent && parent.filename ? parent.filename : null
writeTraceLine('[bootstrap] require-start request=' + safeJson(request) + ' parent=' + safeJson(parentName) + ' isMain=' + safeJson(Boolean(isMain)))
try {
const result = Reflect.apply(originalLoad, this, arguments)
writeTraceLine('[bootstrap] require-ok request=' + safeJson(request))
return result
} catch (error) {
const message = error && typeof error === 'object' && 'stack' in error ? error.stack : error
writeTraceLine('[bootstrap] require-error request=' + safeJson(request) + ' error=' + safeJson(String(message)))
throw error
}
}
} catch (error) {
writeLine('[bootstrap] require-trace-install-error error=' + safeJson(String(error)))
}
}
})();
`
}
export function createBootstrapFatalExitBanner(): string {
// Why: Electron's pre-import error dialog can leave main resident and block NSIS replacement.
return `
;(() => {
const guardKey = ${JSON.stringify(BOOTSTRAP_FATAL_EXIT_GUARD_KEY)}
if (typeof globalThis[guardKey] === 'function') {
return
}
let exitScheduled = false
const exitAfterBootstrapFailure = () => {
if (exitScheduled) {
return
}
exitScheduled = true
process.exitCode = 1
setImmediate(() => process.exit(1))
}
globalThis[guardKey] = () => {
process.off('uncaughtException', exitAfterBootstrapFailure)
delete globalThis[guardKey]
}
process.once('uncaughtException', exitAfterBootstrapFailure)
})();
`
}
function createMainBootstrapPlugin() {
return {
name: 'orca-main-bootstrap',
generateBundle(_options, bundle) {
const mainChunk = bundle['index.js']
if (!mainChunk || mainChunk.type !== 'chunk') {
return
}
// Why: source guards and diagnostics run after Rollup's generated require
// prelude, too late to handle a missing bootstrap dependency.
mainChunk.code =
createBootstrapFatalExitBanner() +
createStartupDiagnosticsBanner(mainChunk.fileName) +
mainChunk.code
}
}
}
export const electronViteConfig: UserConfig = {
main: {
build: {
// Why: daemon-entry.js is asar-unpacked so child_process.fork() can
// execute it from disk. Node's module resolution from the unpacked
// directory cannot reach into app.asar; startup-critical pure JS must
// also survive a partially copied Windows resources tree.
externalizeDeps: {
exclude: [...BUNDLED_MAIN_DEPENDENCIES]
},
rollupOptions: {
// Why: native dependencies must resolve from packaged node_modules,
// while the unpacked daemon needs its pure-JS xterm graph bundled.
external: isExternalMainModule,
input: {
index: resolve('src/main/index.ts'),
'daemon-entry': resolve('src/main/daemon/daemon-entry.ts'),
'plugin-host-entry': resolve('src/main/plugins/plugin-host-entry.ts'),
'computer-sidecar': resolve('src/main/computer/sidecar-entry.ts'),
'stt-worker': resolve('src/main/speech/stt-worker.ts'),
'warp-theme-parser-worker': resolve('src/main/warp-themes/warp-theme-parser-worker.ts'),
'session-scanner-opencode-sqlite-worker-entry': resolve(
'src/main/ai-vault/session-scanner-opencode-sqlite-worker-entry.ts'
),
// Why: forked with ELECTRON_RUN_AS_NODE so @parcel/watcher faults
// can't take down the main process (issue #7547).
'parcel-watcher-process-entry': resolve('src/main/ipc/parcel-watcher-process-entry.ts'),
// Why: a worker thread survives the macOS 26 AppKit main-thread deadlock
// without paying for another Electron process.
'main-thread-hang-watchdog-entry': resolve(
'src/main/hang-watchdog/main-thread-hang-watchdog-entry.ts'
),
// Why: run under ELECTRON_RUN_AS_NODE while the caller blocks on
// spawnSync — codex app-server trust grants need a live event loop
// but must finish before a Codex pane launch proceeds.
'codex/codex-app-server-grant-entry': resolve(
'src/main/codex/codex-app-server-grant-entry.ts'
),
// Why: electron-vite cleans out/main in dev. The dev CLI imports
// this path for `orca agent hooks ...`, so it must survive rebuilds.
'agent-hooks/managed-agent-hook-controls': resolve(
'src/main/agent-hooks/managed-agent-hook-controls.ts'
),
// Why: account import mutates the user's macOS Keychain from the CLI.
'claude-accounts/keychain': resolve('src/main/claude-accounts/keychain.ts')
},
// Why: Rolldown's SSR default is ESM, but Electron and sidecar launchers
// consume these stable CommonJS paths.
output: {
format: 'cjs',
entryFileNames: '[name].js',
chunkFileNames: 'chunks/[name]-[hash].js'
},
plugins: [createMainBootstrapPlugin(), createPlainNodeEntryGuardPlugin()]
}
},
// Why: compile-time substitution for the telemetry gate. See the block
// above for the full rationale.
define: {
ORCA_BUILD_IDENTITY: ORCA_BUILD_IDENTITY_LITERAL,
ORCA_POSTHOG_WRITE_KEY: ORCA_POSTHOG_WRITE_KEY_LITERAL,
ORCA_DIAGNOSTICS_TOKEN_URL: ORCA_DIAGNOSTICS_TOKEN_URL_LITERAL
},
// Why: @xterm/headless declares "exports": null in package.json, which
// prevents Vite's default resolver from finding the CJS entry. Point
// directly at the published main file so the bundler can inline it.
resolve: {
alias: {
'@xterm/headless': resolve('node_modules/@xterm/headless/lib-headless/xterm-headless.js'),
'@xterm/addon-serialize': resolve(
'node_modules/@xterm/addon-serialize/lib/addon-serialize.js'
)
}
}
},
preload: {
build: {
externalizeDeps: {
exclude: ['@electron-toolkit/preload']
}
}
},
renderer: {
resolve: {
alias: {
'@renderer': resolve('src/renderer/src'),
'@': resolve('src/renderer/src')
}
},
plugins: [react(), tailwindcss()],
worker: {
format: 'es'
},
build: {
manifest: true,
modulePreload: { polyfill: true },
target: 'es2020',
// Why: the pop-out dashboard is a second top-level window with its own
// React root. It gets its own HTML entry so it can boot independently of
// the main window while reusing the same preload/window.api. `index` must
// stay listed — overriding input otherwise drops electron-vite's default
// renderer entry.
rollupOptions: {
// Why: shared chunks must never import an HTML entry whose module mounts
// a different React root.
preserveEntrySignatures: 'strict',
input: {
index: resolve('src/renderer/index.html'),
popout: resolve('src/renderer/popout.html'),
web: resolve('src/renderer/web-index.html')
}
}
}
}
}
export default defineConfig(electronViteConfig)