From 566957484c9d6c68b07c822001719f589e349619 Mon Sep 17 00:00:00 2001 From: Joe Lencioni Date: Mon, 8 Jun 2020 10:41:57 -0500 Subject: [PATCH] Optimize micromatch and RegExps in shouldInstrument I've been profiling running Jest with code coverage at Airbnb, and noticed that shouldInstrument is called often and is fairly expensive. It also seems to call micromatch and `new RegExp` repeatedly, both of which can be optimized by caching the work to convert globs and strings into matchers and regexes. I profiled this change by running a set of 27 fairly simple tests. Before this change, about 6-7 seconds was spent in shouldInstrument. After this change, only 400-500 ms is spent there. I would expect this delta to increase along with the number of tests and size of their dependency graphs. --- .../jest-transform/src/shouldInstrument.ts | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/jest-transform/src/shouldInstrument.ts b/packages/jest-transform/src/shouldInstrument.ts index d9fcdcd268c3..bd71314076de 100644 --- a/packages/jest-transform/src/shouldInstrument.ts +++ b/packages/jest-transform/src/shouldInstrument.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import type {Config} from '@jest/types'; import {escapePathForRegex} from 'jest-regex-util'; -import {replacePathSepForGlob} from 'jest-util'; +import {globsToMatcher, replacePathSepForGlob} from 'jest-util'; import micromatch = require('micromatch'); import type {ShouldInstrumentOptions} from './types'; @@ -16,6 +16,20 @@ const MOCKS_PATTERN = new RegExp( escapePathForRegex(path.sep + '__mocks__' + path.sep), ); +const cachedRegexes = new Map(); +const getRegex = (regexStr: string) => { + if (!cachedRegexes.has(regexStr)) { + cachedRegexes.set(regexStr, new RegExp(regexStr)); + } + + const regex = cachedRegexes.get(regexStr); + + // prevent stateful regexes from breaking, just in case + regex.lastIndex = 0; + + return regex; +}; + export default function shouldInstrument( filename: Config.Path, options: ShouldInstrumentOptions, @@ -33,15 +47,15 @@ export default function shouldInstrument( } if ( - !config.testPathIgnorePatterns.some(pattern => !!filename.match(pattern)) + !config.testPathIgnorePatterns.some(pattern => + getRegex(pattern).test(filename), + ) ) { if (config.testRegex.some(regex => new RegExp(regex).test(filename))) { return false; } - if ( - micromatch([replacePathSepForGlob(filename)], config.testMatch).length - ) { + if (globsToMatcher(config.testMatch)(replacePathSepForGlob(filename))) { return false; } } @@ -59,10 +73,9 @@ export default function shouldInstrument( // still cover if `only` is specified !options.collectCoverageOnlyFrom && options.collectCoverageFrom.length && - micromatch( - [replacePathSepForGlob(path.relative(config.rootDir, filename))], - options.collectCoverageFrom, - ).length === 0 + !globsToMatcher(options.collectCoverageFrom)( + replacePathSepForGlob(path.relative(config.rootDir, filename)), + ) ) { return false; }