mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 16:02:11 +00:00
fix(linux): move Chromium shared memory off a tiny /dev/shm (#23751)
* fix(linux): move Chromium shared memory off a tiny /dev/shm Containers such as GitHub Codespaces mount a 64 MB /dev/shm by default. When it fills, Chromium aborts the renderer (IMMEDIATE_CRASH, SIGILL/SIGTRAP) on every reload, trapping Orca in a renderer crash loop. On Linux, stat /dev/shm before app ready and append --disable-dev-shm-usage when it is under 512 MB or unreadable (ORCA_DEV_SHM=off|force overrides). Records a dev_shm_policy crash breadcrumb so future container crashes are diagnosable. * docs(linux): correct dev-shm hasSwitch rationale --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { appMock, statfsSyncMock, breadcrumbMock } = vi.hoisted(() => ({
|
||||
appMock: {
|
||||
commandLine: {
|
||||
appendSwitch: vi.fn(),
|
||||
hasSwitch: vi.fn(() => false)
|
||||
}
|
||||
},
|
||||
statfsSyncMock: vi.fn(),
|
||||
breadcrumbMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({ app: appMock }))
|
||||
vi.mock('node:fs', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
statfsSync: statfsSyncMock
|
||||
}))
|
||||
vi.mock('../crash-reporting/crash-breadcrumb-store', () => ({
|
||||
recordCrashBreadcrumb: breadcrumbMock
|
||||
}))
|
||||
|
||||
const MIB = 1024 * 1024
|
||||
const originalPlatform = process.platform
|
||||
const originalOverride = process.env.ORCA_DEV_SHM
|
||||
|
||||
function setPlatform(platform: NodeJS.Platform): void {
|
||||
Object.defineProperty(process, 'platform', { value: platform, configurable: true })
|
||||
}
|
||||
|
||||
function mockDevShm(totalMib: number, freeMib: number): void {
|
||||
// statfs reports sizes in blocks; use 4 KiB blocks like tmpfs.
|
||||
const bsize = 4096
|
||||
statfsSyncMock.mockReturnValue({
|
||||
bsize,
|
||||
blocks: (totalMib * MIB) / bsize,
|
||||
bavail: (freeMib * MIB) / bsize
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
appMock.commandLine.appendSwitch.mockReset()
|
||||
appMock.commandLine.hasSwitch.mockReset()
|
||||
appMock.commandLine.hasSwitch.mockReturnValue(false)
|
||||
statfsSyncMock.mockReset()
|
||||
breadcrumbMock.mockReset()
|
||||
delete process.env.ORCA_DEV_SHM
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
setPlatform(originalPlatform)
|
||||
if (originalOverride === undefined) {
|
||||
delete process.env.ORCA_DEV_SHM
|
||||
} else {
|
||||
process.env.ORCA_DEV_SHM = originalOverride
|
||||
}
|
||||
})
|
||||
|
||||
describe('configureLinuxDevShmUsage', () => {
|
||||
it('moves Chromium shared memory off a Docker-default 64 MB /dev/shm', async () => {
|
||||
setPlatform('linux')
|
||||
mockDevShm(64, 60)
|
||||
const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy')
|
||||
|
||||
configureLinuxDevShmUsage()
|
||||
|
||||
expect(statfsSyncMock).toHaveBeenCalledWith('/dev/shm')
|
||||
expect(appMock.commandLine.appendSwitch).toHaveBeenCalledWith('disable-dev-shm-usage')
|
||||
expect(breadcrumbMock).toHaveBeenCalledWith('dev_shm_policy', {
|
||||
devShmTotalMB: 64,
|
||||
devShmFreeMB: 60,
|
||||
devShmUsageDisabled: true,
|
||||
reason: 'small'
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves a normally sized /dev/shm on the fast shared-memory path', async () => {
|
||||
setPlatform('linux')
|
||||
mockDevShm(8192, 8000)
|
||||
const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy')
|
||||
|
||||
configureLinuxDevShmUsage()
|
||||
|
||||
expect(appMock.commandLine.appendSwitch).not.toHaveBeenCalled()
|
||||
expect(breadcrumbMock).toHaveBeenCalledWith('dev_shm_policy', {
|
||||
devShmTotalMB: 8192,
|
||||
devShmFreeMB: 8000,
|
||||
devShmUsageDisabled: false,
|
||||
reason: 'ok'
|
||||
})
|
||||
})
|
||||
|
||||
it('disables /dev/shm usage when the mount is missing', async () => {
|
||||
setPlatform('linux')
|
||||
statfsSyncMock.mockImplementation(() => {
|
||||
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
|
||||
})
|
||||
const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy')
|
||||
|
||||
configureLinuxDevShmUsage()
|
||||
|
||||
expect(appMock.commandLine.appendSwitch).toHaveBeenCalledWith('disable-dev-shm-usage')
|
||||
expect(breadcrumbMock).toHaveBeenCalledWith('dev_shm_policy', {
|
||||
devShmUsageDisabled: true,
|
||||
reason: 'unreadable'
|
||||
})
|
||||
})
|
||||
|
||||
it('honors ORCA_DEV_SHM=off on a small mount and =force on a large one', async () => {
|
||||
setPlatform('linux')
|
||||
mockDevShm(64, 60)
|
||||
process.env.ORCA_DEV_SHM = 'off'
|
||||
const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy')
|
||||
configureLinuxDevShmUsage()
|
||||
expect(appMock.commandLine.appendSwitch).not.toHaveBeenCalled()
|
||||
|
||||
mockDevShm(8192, 8000)
|
||||
process.env.ORCA_DEV_SHM = 'force'
|
||||
configureLinuxDevShmUsage()
|
||||
expect(appMock.commandLine.appendSwitch).toHaveBeenCalledWith('disable-dev-shm-usage')
|
||||
})
|
||||
|
||||
it('does not append the switch twice when it is already on the command line', async () => {
|
||||
setPlatform('linux')
|
||||
mockDevShm(64, 60)
|
||||
appMock.commandLine.hasSwitch.mockReturnValue(true)
|
||||
const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy')
|
||||
|
||||
configureLinuxDevShmUsage()
|
||||
|
||||
expect(appMock.commandLine.appendSwitch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('is a no-op off Linux', async () => {
|
||||
setPlatform('darwin')
|
||||
const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy')
|
||||
|
||||
configureLinuxDevShmUsage()
|
||||
|
||||
expect(statfsSyncMock).not.toHaveBeenCalled()
|
||||
expect(appMock.commandLine.appendSwitch).not.toHaveBeenCalled()
|
||||
expect(breadcrumbMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('desktop startup wiring', () => {
|
||||
it('runs the /dev/shm policy for every launch before app ready, GPU fallback included', () => {
|
||||
const source = readFileSync(
|
||||
join(process.cwd(), 'src/main/startup/main-process-preflight.ts'),
|
||||
'utf8'
|
||||
).replace(/\r\n/g, '\n')
|
||||
const call = source.indexOf('\n configureLinuxDevShmUsage()\n')
|
||||
expect(call).toBeGreaterThan(-1)
|
||||
expect(call).toBeLessThan(source.indexOf('\n maybeApplyGpuFallbackForThisLaunch()\n'))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import { statfsSync } from 'node:fs'
|
||||
import { app } from 'electron'
|
||||
import { recordCrashBreadcrumb } from '../crash-reporting/crash-breadcrumb-store'
|
||||
|
||||
const DEV_SHM_PATH = '/dev/shm'
|
||||
const DEV_SHM_OVERRIDE_ENV_VAR = 'ORCA_DEV_SHM'
|
||||
// Why 512: Docker/Codespaces default to 64 MB, where Chromium's ring-buffer and
|
||||
// texture allocations fail and IMMEDIATE_CRASH the renderer on every reload.
|
||||
const MIN_DEV_SHM_TOTAL_MIB = 512
|
||||
const MIB = 1024 * 1024
|
||||
|
||||
type DevShmReading = { totalMib: number; freeMib: number } | null
|
||||
|
||||
function readDevShm(): DevShmReading {
|
||||
try {
|
||||
const stats = statfsSync(DEV_SHM_PATH)
|
||||
return {
|
||||
totalMib: Math.floor((stats.blocks * stats.bsize) / MIB),
|
||||
freeMib: Math.floor((stats.bavail * stats.bsize) / MIB)
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On Linux hosts with a tiny or missing /dev/shm (containers, Codespaces),
|
||||
* back Chromium shared memory with /tmp files instead of letting renderers abort.
|
||||
*/
|
||||
export function configureLinuxDevShmUsage(): void {
|
||||
if (process.platform !== 'linux') {
|
||||
return
|
||||
}
|
||||
const override = (process.env[DEV_SHM_OVERRIDE_ENV_VAR] ?? '').trim().toLowerCase()
|
||||
const reading = readDevShm()
|
||||
const reason =
|
||||
override === 'off' || override === 'force'
|
||||
? override
|
||||
: reading === null
|
||||
? 'unreadable'
|
||||
: reading.totalMib < MIN_DEV_SHM_TOTAL_MIB
|
||||
? 'small'
|
||||
: 'ok'
|
||||
const disable = reason === 'force' || reason === 'small' || reason === 'unreadable'
|
||||
// Why hasSwitch: user argv may already carry it (headless serve appends its own later).
|
||||
if (disable && !app.commandLine.hasSwitch('disable-dev-shm-usage')) {
|
||||
app.commandLine.appendSwitch('disable-dev-shm-usage')
|
||||
}
|
||||
recordCrashBreadcrumb('dev_shm_policy', {
|
||||
...(reading ? { devShmTotalMB: reading.totalMib, devShmFreeMB: reading.freeMib } : {}),
|
||||
devShmUsageDisabled: disable,
|
||||
reason
|
||||
})
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from '../updater'
|
||||
import { getDevInstanceIdentity, shouldApplyPreReadyAppName } from './dev-instance-identity'
|
||||
import { enableRendererHeapHeadroom } from './renderer-heap-headroom'
|
||||
import { configureLinuxDevShmUsage } from './linux-dev-shm-policy'
|
||||
import { isStartupDiagnosticsEnabled, logStartupDiagnostic } from './startup-diagnostics'
|
||||
import { startEventLoopStallProbe } from './event-loop-stall-probe'
|
||||
import {
|
||||
@@ -364,6 +365,7 @@ function initializeMainProcessPreflight(options: MainProcessPreflightOptions): b
|
||||
optOutOfHiddenPageWakeUpThrottling()
|
||||
configureElectronNetworkCompatibility()
|
||||
enableRendererHeapHeadroom()
|
||||
configureLinuxDevShmUsage()
|
||||
maybeApplyGpuFallbackForThisLaunch()
|
||||
if (!state.gpuFallbackActiveThisLaunch) {
|
||||
enableMainProcessGpuFeatures()
|
||||
|
||||
Reference in New Issue
Block a user