Skip to content

Commit 9851dbc

Browse files
hi-ogawaOpenCode
andauthored
fix(browser): trigger playwright/chromium gc on lower disk availability [backport to v4] (#10951)
Co-authored-by: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Co-authored-by: OpenCode <noreply@opencode.ai>
1 parent 10b2cd2 commit 9851dbc

2 files changed

Lines changed: 102 additions & 2 deletions

File tree

packages/browser-playwright/src/playwright.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,8 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
541541
on: cdp.on.bind(cdp),
542542
off: cdp.off.bind(cdp),
543543
once: cdp.once.bind(cdp),
544+
// For now this isn't typed as CDPSession but exposed only for `maybeCollectChromiumGarbage`
545+
detach: cdp.detach.bind(cdp),
544546
} as any // overloaded CDPSession type is too tricky in monorepo
545547
}
546548

packages/vitest/src/node/pools/browser.ts

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import type { Vitest } from '../core'
66
import type { ProcessPool } from '../pool'
77
import type { TestProject } from '../project'
88
import type { TestSpecification } from '../test-specification'
9-
import type { BrowserProvider } from '../types/browser'
9+
import type { BrowserProvider, CDPSession } from '../types/browser'
1010
import crypto from 'node:crypto'
11+
import { statfsSync } from 'node:fs'
1112
import { readFile } from 'node:fs/promises'
1213
import * as nodeos from 'node:os'
1314
import { createDefer } from '@vitest/utils/helpers'
@@ -372,8 +373,9 @@ class BrowserPool {
372373
},
373374
)
374375
testersPromise
375-
.then(() => {
376+
.then(async () => {
376377
debug?.('[%s] test %s finished running', sessionId, file)
378+
await maybeCollectChromiumGarbage(this.project, sessionId)
377379
this.runNextTest(method, sessionId)
378380
})
379381
.catch((error) => {
@@ -430,3 +432,99 @@ function shouldIgnoreDebugger(provider: string, browser: string) {
430432
}
431433
return browser !== 'chromium'
432434
}
435+
436+
// Best-effort workaround for chromium/playwright bug
437+
// https://issues.chromium.org/issues/530892387
438+
439+
// Trigger gc on lower disk (default to 4GB)
440+
const chromiumGCDiskThreshold = process.env.VITEST_CHROMIUM_GC_DISK_THRESHOLD_GB
441+
? Number(process.env.VITEST_CHROMIUM_GC_DISK_THRESHOLD_GB) * 1024 ** 3
442+
: 4 * 1024 ** 3
443+
const forceChromiumGC = !!process.env.VITEST_CHROMIUM_GC_FORCE
444+
const debugGC = createDebugger('vitest:browser:gc')
445+
446+
async function maybeCollectChromiumGarbage(project: TestProject, sessionId: string): Promise<void> {
447+
// trigger only on linux/chromium/playwright
448+
const provider = project.browser!.provider
449+
if (
450+
(!forceChromiumGC && process.platform !== 'linux')
451+
|| provider.name !== 'playwright'
452+
|| project.config.browser.name !== 'chromium'
453+
|| !project.config.browser.isolate
454+
|| !provider.getCDPSession
455+
) {
456+
return
457+
}
458+
459+
const start = performance.now()
460+
const diagnostics: Record<string, any> = {
461+
statfsBeforeMs: undefined,
462+
statfsAfterMs: undefined,
463+
cdpSessionMs: undefined,
464+
cdpSendMs: undefined,
465+
cdpDetachMs: undefined,
466+
forced: forceChromiumGC,
467+
}
468+
try {
469+
// Playwright enables --disable-dev-shm-usage by default, which makes
470+
// Chromium use TMPDIR or /tmp for shared memory files.
471+
// https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/chromium/chromiumSwitches.ts
472+
// https://source.chromium.org/chromium/chromium/src/+/main:base/files/file_util_posix.cc
473+
const tempDirectory = process.env.TMPDIR || '/tmp'
474+
let operationStart = performance.now()
475+
const fsStats = statfsSync(tempDirectory)
476+
diagnostics.statfsBeforeMs = performance.now() - operationStart
477+
478+
const available = fsStats.bavail * fsStats.bsize
479+
diagnostics.availableBytesBefore = available.toString()
480+
diagnostics.thresholdBytes = chromiumGCDiskThreshold.toString()
481+
diagnostics.tempDirectory = tempDirectory
482+
diagnostics.triggered = available < chromiumGCDiskThreshold
483+
if (available >= chromiumGCDiskThreshold) {
484+
return
485+
}
486+
487+
operationStart = performance.now()
488+
// `detach` is available only internally and not on CDPSession type
489+
const cdp = await provider.getCDPSession(sessionId) as CDPSession & { detach: () => Promise<void> }
490+
diagnostics.cdpSessionMs = performance.now() - operationStart
491+
492+
try {
493+
operationStart = performance.now()
494+
await cdp.send('HeapProfiler.collectGarbage')
495+
diagnostics.cdpSendMs = performance.now() - operationStart
496+
}
497+
finally {
498+
operationStart = performance.now()
499+
await cdp.detach().catch((error) => {
500+
debugGC?.('[%s] failed to detach Chromium CDP session: %s', sessionId, error)
501+
})
502+
diagnostics.cdpDetachMs = performance.now() - operationStart
503+
}
504+
505+
if (debugGC?.enabled) {
506+
operationStart = performance.now()
507+
const fsStatsAfter = statfsSync(tempDirectory)
508+
diagnostics.statfsAfterMs = performance.now() - operationStart
509+
diagnostics.availableBytesAfter = (fsStatsAfter.bavail * fsStatsAfter.bsize).toString()
510+
}
511+
512+
const availableGiB = available / 1024 ** 3
513+
const thresholdGiB = chromiumGCDiskThreshold / 1024 ** 3
514+
debugGC?.(
515+
'[%s] Low disk space detected in %s (%s GiB available, %s GiB threshold). Vitest triggered Chromium garbage collection to prevent browser crashes.',
516+
sessionId,
517+
tempDirectory,
518+
availableGiB.toFixed(1),
519+
thresholdGiB.toFixed(1),
520+
)
521+
}
522+
catch (error) {
523+
// don't surface if fs or cdp fails
524+
debugGC?.('[%s] failed to collect Chromium garbage: %s', sessionId, error)
525+
}
526+
finally {
527+
diagnostics.totalMs = performance.now() - start
528+
debugGC?.('[%s] Chromium garbage collection check: %O', sessionId, diagnostics)
529+
}
530+
}

0 commit comments

Comments
 (0)