@@ -6,8 +6,9 @@ import type { Vitest } from '../core'
66import type { ProcessPool } from '../pool'
77import type { TestProject } from '../project'
88import type { TestSpecification } from '../test-specification'
9- import type { BrowserProvider } from '../types/browser'
9+ import type { BrowserProvider , CDPSession } from '../types/browser'
1010import crypto from 'node:crypto'
11+ import { statfsSync } from 'node:fs'
1112import { readFile } from 'node:fs/promises'
1213import * as nodeos from 'node:os'
1314import { 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