From 2297d93a854968d2ffee6025700c69099e6b7f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Tue, 6 Oct 2026 08:15:30 +0300 Subject: [PATCH] fix(coverage): v8 browser memory leak between test files (#11466) --- packages/coverage-v8/src/browser.ts | 8 +++-- packages/coverage-v8/src/commands.ts | 11 +++++++ .../test/memory-leak-1-fixture.test.ts | 6 ++++ .../test/memory-leak-2-fixture.test.ts | 6 ++++ .../test/memory-leak-3-fixture.test.ts | 6 ++++ .../test/memory-leak-4-fixture.test.ts | 6 ++++ test/coverage-test/test/isolation.test.ts | 32 +++++++++++++++++++ 7 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 test/coverage-test/fixtures/test/memory-leak-1-fixture.test.ts create mode 100644 test/coverage-test/fixtures/test/memory-leak-2-fixture.test.ts create mode 100644 test/coverage-test/fixtures/test/memory-leak-3-fixture.test.ts create mode 100644 test/coverage-test/fixtures/test/memory-leak-4-fixture.test.ts diff --git a/packages/coverage-v8/src/browser.ts b/packages/coverage-v8/src/browser.ts index e98489c48..4e88708b4 100644 --- a/packages/coverage-v8/src/browser.ts +++ b/packages/coverage-v8/src/browser.ts @@ -23,8 +23,12 @@ const mod: CoverageProviderModule = { return triggerCommand('__vitest_takeV8Coverage', [window.location.href]) }, - stopCoverage() { - // Browser mode should not stop coverage as same V8 instance is shared between tests + async stopCoverage({ isolate }) { + if (isolate === false) { + return + } + + await triggerCommand('__vitest_stopV8Coverage') }, async getProvider(): Promise { diff --git a/packages/coverage-v8/src/commands.ts b/packages/coverage-v8/src/commands.ts index adbf0e28a..a4976f554 100644 --- a/packages/coverage-v8/src/commands.ts +++ b/packages/coverage-v8/src/commands.ts @@ -9,10 +9,15 @@ import { resolve } from 'pathe' export const commands: Record> = { startV8Coverage, takeV8Coverage, + stopV8Coverage, } async function startV8Coverage(context: BrowserCommandContext): Promise { const session: CDPSession = await context.__ensureCDPHandler() + + // Previous test file's modules must be collected, otherwise V8 reuses their compiled code without block coverage + await session.send('HeapProfiler.collectGarbage') + await session.send('Profiler.enable') await session.send('Profiler.startPreciseCoverage', { callCount: true, @@ -20,6 +25,12 @@ async function startV8Coverage(context: BrowserCommandContext): Promise { }) } +async function stopV8Coverage(context: BrowserCommandContext): Promise { + const session: CDPSession = await context.__ensureCDPHandler() + await session.send('Profiler.stopPreciseCoverage') + await session.send('Profiler.disable') +} + async function takeV8Coverage(context: BrowserCommandContext, pageUrl: string): Promise { const session: CDPSession = await context.__ensureCDPHandler() const coverage = await session.send('Profiler.takePreciseCoverage') diff --git a/test/coverage-test/fixtures/test/memory-leak-1-fixture.test.ts b/test/coverage-test/fixtures/test/memory-leak-1-fixture.test.ts new file mode 100644 index 000000000..8ccc2800e --- /dev/null +++ b/test/coverage-test/fixtures/test/memory-leak-1-fixture.test.ts @@ -0,0 +1,6 @@ +import { test } from "vitest"; +import { commands } from "vitest/browser"; + +test("report live documents", async () => { + await (commands as any).countLiveDocuments(); +}); diff --git a/test/coverage-test/fixtures/test/memory-leak-2-fixture.test.ts b/test/coverage-test/fixtures/test/memory-leak-2-fixture.test.ts new file mode 100644 index 000000000..8ccc2800e --- /dev/null +++ b/test/coverage-test/fixtures/test/memory-leak-2-fixture.test.ts @@ -0,0 +1,6 @@ +import { test } from "vitest"; +import { commands } from "vitest/browser"; + +test("report live documents", async () => { + await (commands as any).countLiveDocuments(); +}); diff --git a/test/coverage-test/fixtures/test/memory-leak-3-fixture.test.ts b/test/coverage-test/fixtures/test/memory-leak-3-fixture.test.ts new file mode 100644 index 000000000..8ccc2800e --- /dev/null +++ b/test/coverage-test/fixtures/test/memory-leak-3-fixture.test.ts @@ -0,0 +1,6 @@ +import { test } from "vitest"; +import { commands } from "vitest/browser"; + +test("report live documents", async () => { + await (commands as any).countLiveDocuments(); +}); diff --git a/test/coverage-test/fixtures/test/memory-leak-4-fixture.test.ts b/test/coverage-test/fixtures/test/memory-leak-4-fixture.test.ts new file mode 100644 index 000000000..8ccc2800e --- /dev/null +++ b/test/coverage-test/fixtures/test/memory-leak-4-fixture.test.ts @@ -0,0 +1,6 @@ +import { test } from "vitest"; +import { commands } from "vitest/browser"; + +test("report live documents", async () => { + await (commands as any).countLiveDocuments(); +}); diff --git a/test/coverage-test/test/isolation.test.ts b/test/coverage-test/test/isolation.test.ts index e1dd8cd5c..50754926e 100644 --- a/test/coverage-test/test/isolation.test.ts +++ b/test/coverage-test/test/isolation.test.ts @@ -56,6 +56,38 @@ for (const isolate of [true, false]) { } } +/* + * This is mostly relevant for V8 provider which enables Profiler, + * but we'll test Istanbul too. + */ +test('browser mode tab isolation (#11447)', async ({ skip }) => { + skip(process.env.COVERAGE_BROWSER !== 'true', 'only relevant for browser mode') + + const liveDocuments: number[] = [] + + await runVitest({ + include: ['fixtures/test/memory-leak-*-fixture.test.ts'], + fileParallelism: false, + coverage: { reporter: 'json' }, + browser: { + commands: { + async countLiveDocuments(context) { + const session = await context.page.context().newCDPSession(context.page) + await session.send('HeapProfiler.collectGarbage') + + const { documents } = await session.send('Memory.getDOMCounters') + liveDocuments.push(documents) + + await session.detach() + }, + }, + }, + }) + + expect(liveDocuments).toHaveLength(4) + expect(new Set(liveDocuments).size).toBe(1) +}) + class Sorter { sort(files: TestSpecification[]) { return files.sort((a) => { -- 2.51.2