Skip to content

Commit

Permalink
Improve performance with mutiple patterns (#222)
Browse files Browse the repository at this point in the history
  • Loading branch information
fisker committed Jan 20, 2022
1 parent f816156 commit 04fbd5b
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 19 deletions.
2 changes: 1 addition & 1 deletion bench.js
Expand Up @@ -4,7 +4,7 @@ import path from 'node:path';
import {fileURLToPath} from 'node:url';
import Benchmark from 'benchmark';
import rimraf from 'rimraf';
import * as globbyMainBranch from 'globby';
import * as globbyMainBranch from '@globby/main-branch';
import gs from 'glob-stream';
import fastGlob from 'fast-glob';
import {globby, globbySync, globbyStream} from './index.js';
Expand Down
45 changes: 30 additions & 15 deletions index.js
Expand Up @@ -55,9 +55,11 @@ const normalizeArgumentsSync = fn => (patterns, options) => fn(toPatternsArray(p
const getFilter = async options => createFilterFunction(
options.gitignore && await isGitIgnored({cwd: options.cwd}),
);

const getFilterSync = options => createFilterFunction(
options.gitignore && isGitIgnoredSync({cwd: options.cwd}),
);

const createFilterFunction = isIgnored => {
const seen = new Set();

Expand All @@ -72,27 +74,40 @@ const createFilterFunction = isIgnored => {
const unionFastGlobResults = (results, filter) => results.flat().filter(fastGlobResult => filter(fastGlobResult));
const unionFastGlobStreams = (streams, filter) => merge2(streams).pipe(new FilterStream(fastGlobResult => filter(fastGlobResult)));

const convertNegativePatterns = (patterns, taskOptions) => {
const globTasks = [];
for (const [index, pattern] of patterns.entries()) {
if (isNegative(pattern)) {
continue;
const convertNegativePatterns = (patterns, options) => {
const tasks = [];

while (patterns.length > 0) {
const index = patterns.findIndex(pattern => isNegative(pattern));

if (index === -1) {
tasks.push({patterns, options});
break;
}

const ignore = patterns
.slice(index)
.filter(pattern => isNegative(pattern))
.map(pattern => pattern.slice(1));
const ignorePattern = patterns[index].slice(1);

const options = {
...taskOptions,
ignore: [...taskOptions.ignore, ...ignore],
};
for (const task of tasks) {
task.options.ignore.push(ignorePattern);
}

if (index !== 0) {
tasks.push({
patterns: patterns.slice(0, index),
options: {
...options,
ignore: [
...options.ignore,
ignorePattern,
],
},
});
}

globTasks.push({patterns: [pattern], options});
patterns = patterns.slice(index + 1);
}

return globTasks;
return tasks;
};

const getDirGlobOptions = (options, cwd) => ({
Expand Down
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -16,7 +16,7 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"scripts": {
"bench": "npm update globby glob-stream fast-glob && node bench.js",
"bench": "npm update @globby/main-branch glob-stream fast-glob && node bench.js",
"test": "xo && ava && tsd"
},
"files": [
Expand Down Expand Up @@ -66,12 +66,12 @@
"slash": "^4.0.0"
},
"devDependencies": {
"@globby/main-branch": "sindresorhus/globby#main",
"@types/node": "^16.11.11",
"ava": "^3.15.0",
"benchmark": "2.1.4",
"get-stream": "^6.0.1",
"glob-stream": "^7.0.0",
"globby": "sindresorhus/globby#main",
"rimraf": "^3.0.2",
"tsd": "^0.19.0",
"typescript": "^4.5.2",
Expand Down
121 changes: 121 additions & 0 deletions tests/generate-glob-tasks.js
Expand Up @@ -9,6 +9,7 @@ import {
import {
invalidPatterns,
getPathValues,
isUnique,
} from './utilities.js';

const runGenerateGlobTasks = async (t, patterns, options) => {
Expand All @@ -24,6 +25,11 @@ const runGenerateGlobTasks = async (t, patterns, options) => {
return promiseResult;
};

const getTasks = async (t, patterns, options) => {
const tasks = await runGenerateGlobTasks(t, patterns, options);
return tasks.map(({patterns, options: {ignore}}) => ({patterns, ignore}));
};

test('generateGlobTasks', async t => {
const tasks = await runGenerateGlobTasks(t, ['*.tmp', '!b.tmp'], {ignore: ['c.tmp']});

Expand Down Expand Up @@ -99,3 +105,118 @@ test('expandDirectories option', async t => {
t.deepEqual(tasks[0].options.ignore, ['**/b.tmp']);
}
});

test('combine tasks', async t => {
t.deepEqual(
await getTasks(t, ['a', 'b']),
[{patterns: ['a', 'b'], ignore: []}],
);

t.deepEqual(
await getTasks(t, ['!a', 'b']),
[{patterns: ['b'], ignore: []}],
);

t.deepEqual(
await getTasks(t, ['!a']),
[],
);

t.deepEqual(
await getTasks(t, ['a', 'b', '!c', '!d']),
[{patterns: ['a', 'b'], ignore: ['c', 'd']}],
);

t.deepEqual(
await getTasks(t, ['a', 'b', '!c', '!d', 'e']),
[
{patterns: ['a', 'b'], ignore: ['c', 'd']},
{patterns: ['e'], ignore: []},
],
);

t.deepEqual(
await getTasks(t, ['a', 'b', '!c', 'd', 'e', '!f', '!g', 'h']),
[
{patterns: ['a', 'b'], ignore: ['c', 'f', 'g']},
{patterns: ['d', 'e'], ignore: ['f', 'g']},
{patterns: ['h'], ignore: []},
],
);
});

test('random patterns', async t => {
for (let index = 0; index < 500; index++) {
const positivePatterns = [];
const negativePatterns = [];
const negativePatternsAtStart = [];

const patterns = Array.from({length: 1 + Math.floor(Math.random() * 20)}, (_, index) => {
const negative = Math.random() > 0.5;
let pattern = String(index + 1);
if (negative) {
negativePatterns.push(pattern);

if (positivePatterns.length === 0) {
negativePatternsAtStart.push(pattern);
}

pattern = `!${pattern}`;
} else {
positivePatterns.push(pattern);
}

return pattern;
});

// eslint-disable-next-line no-await-in-loop
const tasks = await getTasks(t, patterns);
const patternsToDebug = JSON.stringify(patterns);

t.true(
tasks.length <= negativePatterns.length - negativePatternsAtStart.length + 1,
`Unexpected tasks: ${patternsToDebug}`,
);

for (const [index, {patterns, ignore}] of tasks.entries()) {
t.not(
patterns.length,
0,
`Unexpected empty patterns: ${patternsToDebug}`,
);

t.true(
isUnique(patterns),
`patterns should be unique: ${patternsToDebug}`,
);

t.true(
isUnique(ignore),
`ignore should be unique: ${patternsToDebug}`,
);

if (index !== 0 && ignore.length > 0) {
t.deepEqual(
tasks[index - 1].ignore.slice(-ignore.length),
ignore,
`Unexpected ignore: ${patternsToDebug}`,
);
}
}

const allPatterns = tasks.flatMap(({patterns}) => patterns);
const allIgnore = tasks.flatMap(({ignore}) => ignore);

t.is(
new Set(allPatterns).size,
positivePatterns.length,
`positive patterns should be in patterns: ${patternsToDebug}`,
);

t.is(
new Set(allIgnore).size,
negativePatterns.length - negativePatternsAtStart.length,
`negative patterns should be in ignore: ${patternsToDebug}`,
);
}
});
2 changes: 1 addition & 1 deletion tests/globby.js
Expand Up @@ -14,6 +14,7 @@ import {
PROJECT_ROOT,
getPathValues,
invalidPatterns,
isUnique,
} from './utilities.js';

const cwd = process.cwd();
Expand Down Expand Up @@ -298,6 +299,5 @@ test('don\'t throw when specifying a non-existing cwd directory', async t => {

test('unique when using objectMode option', async t => {
const result = await runGlobby(t, ['a.tmp', '*.tmp'], {cwd, objectMode: true});
const isUnique = array => [...new Set(array)].length === array.length;
t.true(isUnique(result.map(({path}) => path)));
});
4 changes: 4 additions & 0 deletions tests/utilities.js
@@ -1,7 +1,9 @@
import {fileURLToPath, pathToFileURL} from 'node:url';

export const PROJECT_ROOT = fileURLToPath(new URL('../', import.meta.url));

export const getPathValues = path => [path, pathToFileURL(path)];

export const invalidPatterns = [
{},
[{}],
Expand All @@ -20,3 +22,5 @@ export const invalidPatterns = [
function () {},
[function () {}],
];

export const isUnique = array => new Set(array).size === array.length;

0 comments on commit 04fbd5b

Please sign in to comment.