diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js index c5bbdb9813c0d1..6cda2c9e6da073 100644 --- a/lib/internal/fs/glob.js +++ b/lib/internal/fs/glob.js @@ -947,5 +947,6 @@ function matchGlobPattern(path, pattern, windows = isWindows) { module.exports = { __proto__: null, Glob, + createMatcher, matchGlobPattern, }; diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index 37ff2473b68011..798e41f0ac4be6 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -37,7 +37,7 @@ const { ERR_SOURCE_MAP_MISSING_SOURCE, }, } = require('internal/errors'); -const { matchGlobPattern } = require('internal/fs/glob'); +const { createMatcher } = require('internal/fs/glob'); const { constants: { kMockSearchParam } } = require('internal/test_runner/mock/loader'); const kCoverageFileRegex = /^coverage-(\d+)-(\d{13})-(\d+)\.json$/; @@ -85,6 +85,9 @@ class TestCoverage { #sourceLines = new SafeMap(); #typeScriptLines = new SafeSet(); + #skipCache = new SafeMap(); + #excludeMatchers = null; + #includeMatchers = null; getLines(fileUrl, source) { // Split the file source into lines. Make sure the lines maintain their @@ -534,6 +537,14 @@ class TestCoverage { } shouldSkipFileCoverage(url) { + const cached = this.#skipCache.get(url); + if (cached !== undefined) return cached; + const result = this.#computeShouldSkipFileCoverage(url); + this.#skipCache.set(url, result); + return result; + } + + #computeShouldSkipFileCoverage(url) { // This check filters out core modules, which start with 'node:' in // coverage reports, as well as any invalid coverages which have been // observed on Windows. @@ -541,28 +552,27 @@ class TestCoverage { const absolutePath = fileURLToPath(url); const relativePath = relative(this.options.cwd, absolutePath); - const { - coverageExcludeGlobs: excludeGlobs, - coverageIncludeGlobs: includeGlobs, - } = this.options; + // The exclude/include globs are fixed for the lifetime of this + // TestCoverage instance, so compile each glob to a matcher once and reuse + // it for every file. Building a fresh Minimatch per call (the previous + // behavior) dominated the coverage report time, scaling with + // files * globs. + this.#excludeMatchers ??= ArrayPrototypeMap( + this.options.coverageExcludeGlobs ?? [], (pattern) => createMatcher(pattern)); + this.#includeMatchers ??= ArrayPrototypeMap( + this.options.coverageIncludeGlobs ?? [], (pattern) => createMatcher(pattern)); // This check filters out files that match the exclude globs. - if (excludeGlobs?.length > 0) { - for (let i = 0; i < excludeGlobs.length; ++i) { - if ( - matchGlobPattern(relativePath, excludeGlobs[i]) || - matchGlobPattern(absolutePath, excludeGlobs[i]) - ) return true; - } + for (let i = 0; i < this.#excludeMatchers.length; ++i) { + const matcher = this.#excludeMatchers[i]; + if (matcher.match(relativePath) || matcher.match(absolutePath)) return true; } // This check filters out files that do not match the include globs. - if (includeGlobs?.length > 0) { - for (let i = 0; i < includeGlobs.length; ++i) { - if ( - matchGlobPattern(relativePath, includeGlobs[i]) || - matchGlobPattern(absolutePath, includeGlobs[i]) - ) return false; + if (this.#includeMatchers.length > 0) { + for (let i = 0; i < this.#includeMatchers.length; ++i) { + const matcher = this.#includeMatchers[i]; + if (matcher.match(relativePath) || matcher.match(absolutePath)) return false; } return true; }