Skip to content

Commit

Permalink
Cache micromatch in SearchSource globsToMatcher
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lencioni committed Jun 7, 2020
1 parent 068ec04 commit 65a0405
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -22,6 +22,8 @@

### Performance

- `[jest-core]` Cache micromatch in SearchSource globsToMatcher ([#10131](https://github.com/facebook/jest/pull/10131))

## 26.0.1

### Fixes
Expand Down
47 changes: 45 additions & 2 deletions packages/jest-core/src/SearchSource.ts
Expand Up @@ -37,8 +37,51 @@ export type TestSelectionConfig = {
watch?: boolean;
};

const globsToMatcher = (globs: Array<Config.Glob>) => (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<Config.Glob>) => {
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,
Expand Down
28 changes: 28 additions & 0 deletions packages/jest-core/src/__tests__/SearchSource.test.ts
Expand Up @@ -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(
{
Expand Down

0 comments on commit 65a0405

Please sign in to comment.