From 069905ae0af26dabd2246e1d716f084744520831 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. --- CHANGELOG.md | 2 +- .../jest-transform/src/shouldInstrument.ts | 31 +++++++++++++------ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e96c04a5d4a..099ba311f489 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ ### Performance -- `[jest-core]` Cache micromatch in SearchSource globsToMatcher and avoid recreating RegExp instances in regexToMatcher ([#10131](https://github.com/facebook/jest/pull/10131)) +- `[jest-core, jest-transform]` Improve Jest startup time and test runtime, particularly when running with coverage, by caching micromatch and avoiding recreating RegExp instances ([#10131](https://github.com/facebook/jest/pull/10131)) ## 26.0.1 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; }