Skip to content

Commit 032d318

Browse files
hi-ogawaOpenCode
andauthored
fix(browser): trigger playwright/chromium gc on lower disk availability (#10912)
Co-authored-by: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Co-authored-by: OpenCode <noreply@opencode.ai>
1 parent 34e4c00 commit 032d318

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
@@ -665,6 +665,8 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
665665
on: cdp.on.bind(cdp),
666666
off: cdp.off.bind(cdp),
667667
once: cdp.once.bind(cdp),
668+
// For now this isn't typed as CDPSession but exposed only for `maybeCollectChromiumGarbage`
669+
detach: cdp.detach.bind(cdp),
668670
} as any // overloaded CDPSession type is too tricky in monorepo
669671
}
670672

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'
@@ -408,8 +409,9 @@ class BrowserPool {
408409
},
409410
)
410411
testersPromise
411-
.then(() => {
412+
.then(async () => {
412413
debug?.('[%s] test %s finished running', sessionId, file)
414+
await maybeCollectChromiumGarbage(this.project, sessionId)
413415
this.runNextTest(method, sessionId)
414416
})
415417
.catch((error) => {
@@ -466,3 +468,99 @@ function shouldIgnoreDebugger(provider: string, browser: string) {
466468
}
467469
return browser !== 'chromium'
468470
}
471+
472+
// Best-effort workaround for chromium/playwright bug
473+
// https://issues.chromium.org/issues/530892387
474+
475+
// Trigger gc on lower disk (default to 4GB)
476+
const chromiumGCDiskThreshold = process.env.VITEST_CHROMIUM_GC_DISK_THRESHOLD_GB
477+
? Number(process.env.VITEST_CHROMIUM_GC_DISK_THRESHOLD_GB) * 1024 ** 3
478+
: 4 * 1024 ** 3
479+
const forceChromiumGC = !!process.env.VITEST_CHROMIUM_GC_FORCE
480+
const debugGC = createDebugger('vitest:browser:gc')
481+
482+
async function maybeCollectChromiumGarbage(project: TestProject, sessionId: string): Promise<void> {
483+
// trigger only on linux/chromium/playwright
484+
const provider = project.browser!.provider
485+
if (
486+
(!forceChromiumGC && process.platform !== 'linux')
487+
|| provider.name !== 'playwright'
488+
|| project.config.browser.name !== 'chromium'
489+
|| !project.config.isolate
490+
|| !provider.getCDPSession
491+
) {
492+
return
493+
}
494+
495+
const start = performance.now()
496+
const diagnostics: Record<string, any> = {
497+
statfsBeforeMs: undefined,
498+
statfsAfterMs: undefined,
499+
cdpSessionMs: undefined,
500+
cdpSendMs: undefined,
501+
cdpDetachMs: undefined,
502+
forced: forceChromiumGC,
503+
}
504+
try {
505+
// Playwright enables --disable-dev-shm-usage by default, which makes
506+
// Chromium use TMPDIR or /tmp for shared memory files.
507+
// https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/chromium/chromiumSwitches.ts
508+
// https://source.chromium.org/chromium/chromium/src/+/main:base/files/file_util_posix.cc
509+
const tempDirectory = process.env.TMPDIR || '/tmp'
510+
let operationStart = performance.now()
511+
const fsStats = statfsSync(tempDirectory)
512+
diagnostics.statfsBeforeMs = performance.now() - operationStart
513+
514+
const available = fsStats.bavail * fsStats.bsize
515+
diagnostics.availableBytesBefore = available.toString()
516+
diagnostics.thresholdBytes = chromiumGCDiskThreshold.toString()
517+
diagnostics.tempDirectory = tempDirectory
518+
diagnostics.triggered = available < chromiumGCDiskThreshold
519+
if (available >= chromiumGCDiskThreshold) {
520+
return
521+
}
522+
523+
operationStart = performance.now()
524+
// `detach` is available only internally and not on CDPSession type
525+
const cdp = await provider.getCDPSession(sessionId) as CDPSession & { detach: () => Promise<void> }
526+
diagnostics.cdpSessionMs = performance.now() - operationStart
527+
528+
try {
529+
operationStart = performance.now()
530+
await cdp.send('HeapProfiler.collectGarbage')
531+
diagnostics.cdpSendMs = performance.now() - operationStart
532+
}
533+
finally {
534+
operationStart = performance.now()
535+
await cdp.detach().catch((error) => {
536+
debugGC?.('[%s] failed to detach Chromium CDP session: %s', sessionId, error)
537+
})
538+
diagnostics.cdpDetachMs = performance.now() - operationStart
539+
}
540+
541+
if (debugGC?.enabled) {
542+
operationStart = performance.now()
543+
const fsStatsAfter = statfsSync(tempDirectory)
544+
diagnostics.statfsAfterMs = performance.now() - operationStart
545+
diagnostics.availableBytesAfter = (fsStatsAfter.bavail * fsStatsAfter.bsize).toString()
546+
}
547+
548+
const availableGiB = available / 1024 ** 3
549+
const thresholdGiB = chromiumGCDiskThreshold / 1024 ** 3
550+
debugGC?.(
551+
'[%s] Low disk space detected in %s (%s GiB available, %s GiB threshold). Vitest triggered Chromium garbage collection to prevent browser crashes.',
552+
sessionId,
553+
tempDirectory,
554+
availableGiB.toFixed(1),
555+
thresholdGiB.toFixed(1),
556+
)
557+
}
558+
catch (error) {
559+
// don't surface if fs or cdp fails
560+
debugGC?.('[%s] failed to collect Chromium garbage: %s', sessionId, error)
561+
}
562+
finally {
563+
diagnostics.totalMs = performance.now() - start
564+
debugGC?.('[%s] Chromium garbage collection check: %O', sessionId, diagnostics)
565+
}
566+
}

0 commit comments

Comments
 (0)
Sponsor
SponsoredKunjungi sekarang
Promo