From 65a04052e646f7e9485bc5d056b5989bca8e3a2b Mon Sep 17 00:00:00 2001 From: Joe Lencioni Date: Fri, 5 Jun 2020 08:26:58 -0500 Subject: [PATCH] Cache micromatch in SearchSource globsToMatcher I was profiling some Jest runs at Airbnb and noticed that on my MacBook Pro, we can spend over 2 seconds at Jest startup time in SearchSource getTestPaths. I believe that this will grow as the size of the codebase increases. Looking at the call stacks, it appears to be calling micromatch repeatedly, which calls picomatch, which builds a regex out of the globs. It seems that the parsing and regex building also triggers the garbage collector frequently. Upon testing, I noticed that the globs don't actually change between these calls, so we can save a bunch of work by making a micromatch matcher and reusing that function for all of the paths. micromatch has some logic internally to handle lists of globs that may include negated globs. A naive approach of just checking if it matched any of the globs won't capture that, so I copied and simplified the logic from within micromatch. https://github.com/micromatch/micromatch/blob/fe4858b0/index.js#L27-L77 In my profiling of this change locally, this brings down the time of startRun from about 2000ms to about 200ms. --- CHANGELOG.md | 2 + packages/jest-core/src/SearchSource.ts | 47 ++++++++++++++++++- .../src/__tests__/SearchSource.test.ts | 28 +++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85c66a5d976f..c87ad645b840 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ ### Performance +- `[jest-core]` Cache micromatch in SearchSource globsToMatcher ([#10131](https://github.com/facebook/jest/pull/10131)) + ## 26.0.1 ### Fixes diff --git a/packages/jest-core/src/SearchSource.ts b/packages/jest-core/src/SearchSource.ts index 48c630f2adb4..a384b4791d14 100644 --- a/packages/jest-core/src/SearchSource.ts +++ b/packages/jest-core/src/SearchSource.ts @@ -37,8 +37,51 @@ export type TestSelectionConfig = { watch?: boolean; }; -const globsToMatcher = (globs: Array) => (path: Config.Path) => - micromatch([replacePathSepForGlob(path)], globs, {dot: true}).length > 0; +const globsMatchers = new Map< + string, + { + isMatch: (str: string) => boolean; + negated: boolean; + } +>(); + +const globsToMatcher = (globs: Array) => { + const matchers = globs.map(glob => { + if (!globsMatchers.has(glob)) { + const state = micromatch.scan(glob, {dot: true}); + const matcher = { + isMatch: micromatch.matcher(glob, {dot: true}), + negated: state.negated, + }; + globsMatchers.set(glob, matcher); + } + return globsMatchers.get(glob)!; + }); + + return (path: Config.Path) => { + const replacedPath = replacePathSepForGlob(path); + let kept = false; + let omitted = false; + let negatives = 0; + + for (let i = 0; i < matchers.length; i++) { + const {isMatch, negated} = matchers[i]; + + if (negated) negatives++; + + const matched = isMatch(replacedPath); + + if (!matched && negated) { + kept = false; + omitted = true; + } else if (matched && !negated) { + kept = true; + } + } + + return negatives === matchers.length ? !omitted : kept && !omitted; + }; +}; const regexToMatcher = (testRegex: Config.ProjectConfig['testRegex']) => ( path: Config.Path, diff --git a/packages/jest-core/src/__tests__/SearchSource.test.ts b/packages/jest-core/src/__tests__/SearchSource.test.ts index fbc4513ef1f6..5bb1deedc263 100644 --- a/packages/jest-core/src/__tests__/SearchSource.test.ts +++ b/packages/jest-core/src/__tests__/SearchSource.test.ts @@ -206,6 +206,34 @@ describe('SearchSource', () => { }); }); + it('finds tests matching a JS with overriding glob patterns', () => { + const {options: config} = normalize( + { + moduleFileExtensions: ['js', 'jsx'], + name, + rootDir, + testMatch: [ + '**/*.js?(x)', + '!**/test.js?(x)', + '**/test.js', + '!**/test.js', + ], + testRegex: '', + }, + {} as Config.Argv, + ); + + return findMatchingTests(config).then(data => { + const relPaths = toPaths(data.tests).map(absPath => + path.relative(rootDir, absPath), + ); + expect(relPaths.sort()).toEqual([ + path.normalize('module.jsx'), + path.normalize('no_tests.js'), + ]); + }); + }); + it('finds tests with default file extensions using testRegex', () => { const {options: config} = normalize( {