@@ -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'
@@ -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