Skip to content

Commit 3c33d74

Browse files
committedMay 2, 2023
fix: cache sourcemap objects rather than converter instances

File tree

2 files changed

+43
-32
lines changed

2 files changed

+43
-32
lines changed
 

Diff for: ‎.changeset/quick-files-visit.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@web/test-runner-coverage-v8': minor
3+
---
4+
5+
Cache sourcemap resolution across v8-to-istanbul calls to avoid heavy FS reads

Diff for: ‎packages/test-runner-coverage-v8/src/index.ts

+38-32
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,36 @@ import { TestRunnerCoreConfig, fetchSourceMap } from '@web/test-runner-core';
55
import { Profiler } from 'inspector';
66
import picoMatch from 'picomatch';
77
import LruCache from 'lru-cache';
8+
import { readFile } from 'node:fs/promises';
89

910
import { toFilePath } from './utils';
1011

1112
type V8Coverage = Profiler.ScriptCoverage;
1213
type Matcher = (test: string) => boolean;
13-
type V8Converter = ReturnType<typeof v8toIstanbulLib>;
14+
type IstanbulSource = Required<Parameters<typeof v8toIstanbulLib>>[2];
1415

1516
const cachedMatchers = new Map<string, Matcher>();
1617

17-
// Cache the v8-to-istanbul converters between calls since they
18-
// result in loading files from disk repeatedly otherwise.
19-
const cachedConverters = new LruCache<string, V8Converter>({
20-
max: 200,
18+
// Cache the sourcemap/source objects to avoid repeatedly having to load
19+
// them from disk per call
20+
const cachedSources = new LruCache<string, IstanbulSource>({
21+
maxSize: 1024 * 1024 * 50,
2122
});
2223

2324
// coverage base dir must be separated with "/"
2425
const coverageBaseDir = process.cwd().split(sep).join('/');
2526

27+
function hasOriginalSource(source: IstanbulSource): boolean {
28+
return (
29+
'sourceMap' in source &&
30+
source.sourceMap !== undefined &&
31+
typeof source.sourceMap.sourcemap === 'object' &&
32+
source.sourceMap.sourcemap !== null &&
33+
Array.isArray(source.sourceMap.sourcemap.sourcesContent) &&
34+
source.sourceMap.sourcemap.sourcesContent.length > 0
35+
);
36+
}
37+
2638
function getMatcher(patterns?: string[]) {
2739
if (!patterns || patterns.length === 0) {
2840
return () => true;
@@ -71,35 +83,29 @@ export async function v8ToIstanbul(
7183
const filePath = join(config.rootDir, toFilePath(path));
7284

7385
if (!testFiles.includes(filePath) && included(filePath) && !excluded(filePath)) {
74-
const sources = await fetchSourceMap({
75-
protocol: config.protocol,
76-
host: config.hostname,
77-
port: config.port,
78-
browserUrl: `${url.pathname}${url.search}${url.hash}`,
79-
userAgent,
80-
});
81-
82-
const cachedConverter = cachedConverters.get(filePath);
83-
const converter = cachedConverter ?? v8toIstanbulLib(filePath, 0, sources as any);
84-
85-
if (!cachedConverter) {
86-
await converter.load();
87-
cachedConverters.set(filePath, converter);
88-
} else {
89-
// When we reuse a cached converter, we need to reset it before using its `applyCoverage` function.
90-
// If we don't, the coverage results will be poisoned with the results of the previous uses.
91-
//
92-
// This "workaround" is resetting some internal variables of the `V8ToIstanbul` class: `branches` & `functions`.
93-
// This can break when newer versions of v8-to-istanbul are released. (variable renaming, more variables are used, ...)
94-
//
95-
// TODO: use a (stable) clone technique instead when available (`structuredClone` is available in node 17)
96-
//
97-
// @ts-ignore
98-
converter.branches = {};
99-
// @ts-ignore
100-
converter.functions = {};
86+
const browserUrl = `${url.pathname}${url.search}${url.hash}`;
87+
const cachedSource = cachedSources.get(browserUrl);
88+
const sources =
89+
cachedSource ??
90+
((await fetchSourceMap({
91+
protocol: config.protocol,
92+
host: config.hostname,
93+
port: config.port,
94+
browserUrl,
95+
userAgent,
96+
})) as IstanbulSource);
97+
98+
if (!cachedSource) {
99+
if (!hasOriginalSource(sources)) {
100+
const contents = await readFile(filePath, 'utf8');
101+
(sources as IstanbulSource & { originalSource: string }).originalSource = contents;
102+
}
103+
cachedSources.set(browserUrl, sources);
101104
}
102105

106+
const converter = v8toIstanbulLib(filePath, 0, sources);
107+
await converter.load();
108+
103109
converter.applyCoverage(entry.functions);
104110
Object.assign(istanbulCoverage, converter.toIstanbul());
105111
}

0 commit comments

Comments
 (0)
Please sign in to comment.