Skip to content

Commit 7e7e35c

Browse files
authored
perf(coverage): reduce RPC data and unnecessary serializations (#10781)
1 parent c17677a commit 7e7e35c

18 files changed

Lines changed: 269 additions & 158 deletions

File tree

‎packages/coverage-istanbul/package.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
"types": "./dist/index.d.ts",
2929
"default": "./dist/index.js"
3030
},
31+
"./browser": {
32+
"types": "./dist/browser.d.ts",
33+
"default": "./dist/browser.js"
34+
},
3135
"./*": "./*"
3236
},
3337
"main": "./dist/index.js",

‎packages/coverage-istanbul/rollup.config.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const pkg = require('./package.json')
1212

1313
const entries = {
1414
index: 'src/index.ts',
15+
browser: 'src/browser.ts',
1516
provider: 'src/provider.ts',
1617
}
1718

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { CoverageMapData } from 'istanbul-lib-coverage'
2+
import type { IstanbulCoverageProvider } from './provider'
3+
import { COVERAGE_STORE_KEY } from './constants'
4+
5+
export const BaseCoverageProviderModule = {
6+
takeCoverage(): CoverageMapData | undefined {
7+
// @ts-expect-error -- untyped global
8+
return globalThis[COVERAGE_STORE_KEY]
9+
},
10+
11+
// Reset coverage map to prevent duplicate results if this is called twice in row
12+
startCoverage(): void {
13+
// @ts-expect-error -- untyped global
14+
const coverageMap = globalThis[COVERAGE_STORE_KEY] as CoverageMapData
15+
16+
// When isolated, there are no previous results
17+
if (!coverageMap) {
18+
return
19+
}
20+
21+
for (const filename in coverageMap) {
22+
const branches = coverageMap[filename].b
23+
24+
for (const key in branches) {
25+
branches[key] = branches[key].map(() => 0)
26+
}
27+
28+
for (const metric of ['f', 's'] as const) {
29+
const entry = coverageMap[filename][metric]
30+
31+
for (const key in entry) {
32+
entry[key] = 0
33+
}
34+
}
35+
}
36+
},
37+
38+
async getProvider(): Promise<IstanbulCoverageProvider> {
39+
// to not bundle the provider
40+
const providerPath = './provider.js'
41+
const { IstanbulCoverageProvider } = (await import(
42+
/* @vite-ignore */
43+
providerPath,
44+
)) as typeof import('./provider')
45+
46+
return new IstanbulCoverageProvider()
47+
},
48+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { CoverageProviderModule } from 'vitest/node'
2+
import { BaseCoverageProviderModule } from './base'
3+
4+
function triggerCommand(command: string, args: any[] = []): Promise<any> {
5+
return (globalThis as any).__vitest_browser_runner__?.commands?.triggerCommand?.(command, args)
6+
}
7+
8+
const mod: CoverageProviderModule = {
9+
takeCoverage() {
10+
const coverage = BaseCoverageProviderModule.takeCoverage()
11+
12+
if (!coverage) {
13+
return
14+
}
15+
16+
return triggerCommand('__vitest_writeCoverageFile', [coverage])
17+
},
18+
19+
startCoverage() {
20+
BaseCoverageProviderModule.startCoverage()
21+
},
22+
23+
getProvider() {
24+
return BaseCoverageProviderModule.getProvider()
25+
},
26+
}
27+
28+
export default mod
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { BrowserCommand } from 'vitest/node'
2+
import type { IstanbulCoverageProvider } from './provider'
3+
import { randomUUID } from 'node:crypto'
4+
import { existsSync } from 'node:fs'
5+
import { writeFile } from 'node:fs/promises'
6+
import { resolve } from 'pathe'
7+
8+
export const commands: Record<string, BrowserCommand<any[]>> = {
9+
writeCoverageFile(context, coverage: unknown) {
10+
const provider = context.project.vitest.coverageProvider as IstanbulCoverageProvider
11+
12+
return writeCoverageFile(provider.coverageFilesDirectory, coverage)
13+
},
14+
}
15+
16+
export async function writeCoverageFile(coverageFilesDirectory: string, coverage: unknown): Promise<string> {
17+
// Write results on file system directly and transfer only the filename over RPC
18+
const filename = resolve(
19+
coverageFilesDirectory,
20+
`coverage-${randomUUID()}.json`,
21+
)
22+
23+
try {
24+
await writeFile(filename, JSON.stringify(coverage), 'utf-8')
25+
}
26+
catch (error) {
27+
if (!existsSync(coverageFilesDirectory)) {
28+
throw new Error(
29+
`Something removed the coverage directory "${coverageFilesDirectory}" Vitest created earlier. Make sure you are not running multiple Vitests with the same "coverage.reportsDirectory" at the same time.`,
30+
{ cause: error },
31+
)
32+
}
33+
34+
throw error
35+
}
36+
37+
return filename
38+
}
Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,29 @@
1-
import type { CoverageMapData } from 'istanbul-lib-coverage'
21
import type { CoverageProviderModule } from 'vitest/node'
3-
import type { IstanbulCoverageProvider } from './provider'
4-
import { COVERAGE_STORE_KEY } from './constants'
2+
import assert from 'node:assert'
3+
import { BaseCoverageProviderModule } from './base'
4+
import { writeCoverageFile } from './commands'
55

66
const mod: CoverageProviderModule = {
7-
takeCoverage() {
8-
// @ts-expect-error -- untyped global
9-
return globalThis[COVERAGE_STORE_KEY]
10-
},
11-
12-
// Reset coverage map to prevent duplicate results if this is called twice in row
13-
startCoverage() {
14-
// @ts-expect-error -- untyped global
15-
const coverageMap = globalThis[COVERAGE_STORE_KEY] as CoverageMapData
7+
takeCoverage(options) {
8+
const coverage = BaseCoverageProviderModule.takeCoverage()
169

17-
// When isolated, there are no previous results
18-
if (!coverageMap) {
10+
if (!coverage) {
1911
return
2012
}
2113

22-
for (const filename in coverageMap) {
23-
const branches = coverageMap[filename].b
24-
25-
for (const key in branches) {
26-
branches[key] = branches[key].map(() => 0)
27-
}
28-
29-
for (const metric of ['f', 's'] as const) {
30-
const entry = coverageMap[filename][metric]
14+
const coverageFilesDirectory = options?.coverageFilesDirectory
15+
assert(coverageFilesDirectory, 'coverageFilesDirectory is required')
3116

32-
for (const key in entry) {
33-
entry[key] = 0
34-
}
35-
}
36-
}
17+
return writeCoverageFile(coverageFilesDirectory, coverage)
3718
},
3819

39-
async getProvider(): Promise<IstanbulCoverageProvider> {
40-
// to not bundle the provider
41-
const providerPath = './provider.js'
42-
const { IstanbulCoverageProvider } = (await import(
43-
/* @vite-ignore */
44-
providerPath,
45-
)) as typeof import('./provider')
20+
startCoverage() {
21+
BaseCoverageProviderModule.startCoverage()
22+
},
4623

47-
return new IstanbulCoverageProvider()
24+
getProvider() {
25+
return BaseCoverageProviderModule.getProvider()
4826
},
4927
}
28+
5029
export default mod

‎packages/coverage-istanbul/src/provider.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { createDebug } from 'obug'
1717
import c from 'tinyrainbow'
1818
import { BaseCoverageProvider, isCSSRequest } from 'vitest/node'
1919
import { version } from '../package.json' with { type: 'json' }
20+
import { commands } from './commands'
2021
import { COVERAGE_STORE_KEY } from './constants'
2122

2223
const debug = createDebug('vitest:coverage')
@@ -31,6 +32,14 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
3132
initialize(ctx: Vitest): void {
3233
this._initialize(ctx)
3334

35+
for (const project of ctx.projects) {
36+
if (project.isBrowserEnabled() && project.browser) {
37+
for (const [name, command] of Object.entries(commands)) {
38+
project.browser.registerCommand(`__vitest_${name}` as any, command)
39+
}
40+
}
41+
}
42+
3443
if (this.options.instrumenter) {
3544
this.instrumenter = this.options.instrumenter({
3645
coverageVariable: COVERAGE_STORE_KEY,
Lines changed: 2 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import type { Profiler } from 'node:inspector'
21
import type { CoverageProviderModule } from 'vitest/node'
32
import type { V8CoverageProvider } from './provider'
43
import { loadProvider } from './load-provider'
54

65
let enabled = false
76

8-
type ScriptCoverage = Profiler.TakePreciseCoverageReturnType
9-
107
function triggerCommand(command: string, args: any[] = []): Promise<any> {
118
return (globalThis as any).__vitest_browser_runner__.commands.triggerCommand(command, args)
129
}
@@ -22,21 +19,8 @@ const mod: CoverageProviderModule = {
2219
await triggerCommand('__vitest_startV8Coverage')
2320
},
2421

25-
async takeCoverage(): Promise<{ result: any[] }> {
26-
const coverage: ScriptCoverage = await triggerCommand('__vitest_takeV8Coverage')
27-
const result: typeof coverage.result = []
28-
29-
// Reduce amount of data sent over rpc by doing some early result filtering
30-
for (const entry of coverage.result) {
31-
if (filterResult(entry)) {
32-
result.push({
33-
...entry,
34-
url: decodeURIComponent(entry.url.replace(window.location.origin, '')),
35-
})
36-
}
37-
}
38-
39-
return { result }
22+
async takeCoverage(): Promise<unknown> {
23+
return triggerCommand('__vitest_takeV8Coverage', [window.location.href])
4024
},
4125

4226
stopCoverage() {
@@ -48,35 +32,3 @@ const mod: CoverageProviderModule = {
4832
},
4933
}
5034
export default mod
51-
52-
function filterResult(coverage: ScriptCoverage['result'][number]): boolean {
53-
if (!coverage.url.startsWith(window.location.origin)) {
54-
return false
55-
}
56-
57-
if (coverage.url.includes('/node_modules/')) {
58-
return false
59-
}
60-
61-
if (coverage.url.includes('__vitest_browser__')) {
62-
return false
63-
}
64-
65-
if (coverage.url.includes('__vitest__/assets')) {
66-
return false
67-
}
68-
69-
if (coverage.url === window.location.href) {
70-
return false
71-
}
72-
73-
if (coverage.url.includes('/@id/@vitest/')) {
74-
return false
75-
}
76-
77-
if (coverage.url.includes('/@vite/client')) {
78-
return false
79-
}
80-
81-
return true
82-
}

‎packages/coverage-v8/src/commands.ts‎

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import type { CDPSession } from '@vitest/browser-playwright'
22
import type { BrowserCommand, BrowserCommandContext } from 'vitest/node'
3+
import type { V8CoverageProvider } from './provider'
4+
import { randomUUID } from 'node:crypto'
5+
import { existsSync } from 'node:fs'
6+
import { writeFile } from 'node:fs/promises'
7+
import { resolve } from 'pathe'
38

4-
export const commands: Record<string, BrowserCommand> = {
9+
export const commands: Record<string, BrowserCommand<any[]>> = {
510
startV8Coverage,
611
takeV8Coverage,
712
}
@@ -15,7 +20,77 @@ async function startV8Coverage(context: BrowserCommandContext): Promise<void> {
1520
})
1621
}
1722

18-
async function takeV8Coverage(context: BrowserCommandContext): Promise<any> {
23+
async function takeV8Coverage(context: BrowserCommandContext, pageUrl: string): Promise<string> {
1924
const session: CDPSession = await context.__ensureCDPHandler()
20-
return session.send('Profiler.takePreciseCoverage')
25+
const coverage = await session.send('Profiler.takePreciseCoverage')
26+
27+
const origin = new URL(pageUrl).origin
28+
const result: typeof coverage.result = []
29+
30+
for (const entry of coverage.result) {
31+
if (filterResult(entry.url, origin, pageUrl)) {
32+
entry.url = decodeURIComponent(entry.url.replace(origin, ''))
33+
result.push(entry)
34+
}
35+
}
36+
37+
const provider = context.project.vitest.coverageProvider as V8CoverageProvider
38+
39+
return await writeCoverageFile(provider.coverageFilesDirectory, { result })
40+
}
41+
42+
export async function writeCoverageFile(coverageFilesDirectory: string, coverage: unknown): Promise<string> {
43+
// Write results on file system directly and transfer only the filename over RPC
44+
const filename = resolve(
45+
coverageFilesDirectory,
46+
`coverage-${randomUUID()}.json`,
47+
)
48+
49+
try {
50+
await writeFile(filename, JSON.stringify(coverage), 'utf-8')
51+
}
52+
catch (error) {
53+
if (!existsSync(coverageFilesDirectory)) {
54+
throw new Error(
55+
`Something removed the coverage directory "${coverageFilesDirectory}" Vitest created earlier. Make sure you are not running multiple Vitests with the same "coverage.reportsDirectory" at the same time.`,
56+
{ cause: error },
57+
)
58+
}
59+
60+
throw error
61+
}
62+
63+
return filename
64+
}
65+
66+
function filterResult(url: string, origin: string, pageUrl: string): boolean {
67+
if (!url.startsWith(origin)) {
68+
return false
69+
}
70+
71+
if (url.includes('/node_modules/')) {
72+
return false
73+
}
74+
75+
if (url.includes('__vitest_browser__')) {
76+
return false
77+
}
78+
79+
if (url.includes('__vitest__/assets')) {
80+
return false
81+
}
82+
83+
if (url === pageUrl) {
84+
return false
85+
}
86+
87+
if (url.includes('/@id/@vitest/')) {
88+
return false
89+
}
90+
91+
if (url.includes('/@vite/client')) {
92+
return false
93+
}
94+
95+
return true
2196
}

0 commit comments

Comments
 (0)