diff --git a/benchmark/path/matchesGlob-filter.js b/benchmark/path/matchesGlob-filter.js new file mode 100644 index 000000000000..8439a24fe8a4 --- /dev/null +++ b/benchmark/path/matchesGlob-filter.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { posix, win32 } = require('node:path'); + +const bench = common.createBenchmark(main, { + n: [2e4], + platform: ['posix', 'win32'], + pattern: ['**/node_modules/**/test/**', '**/src/**'], +}); + +function main({ n, platform, pattern }) { + const implementation = platform === 'posix' ? posix : win32; + const filenames = Array.from({ length: 64 }, (_, index) => { + const directory = index % 2 === 0 ? 'node_modules/dependency/test' : 'src'; + return `workspace/packages/package${index}/${directory}/file${index}.js` + .replaceAll('/', implementation.sep); + }); + assert.strictEqual(filenames.filter((filename) => implementation.matchesGlob(filename, pattern)).length, 32); + let matches = 0; + bench.start(); + for (let iteration = 0; iteration < n; iteration++) { + for (const filename of filenames) matches += implementation.matchesGlob(filename, pattern); + } + bench.end(n * filenames.length); + assert.strictEqual(matches, n * 32); +} diff --git a/benchmark/path/matchesGlob-sections.js b/benchmark/path/matchesGlob-sections.js new file mode 100644 index 000000000000..55f5d4de6ad9 --- /dev/null +++ b/benchmark/path/matchesGlob-sections.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { posix, win32 } = require('node:path'); + +const bench = common.createBenchmark(main, { + n: [1e5], + platform: ['posix', 'win32'], + count: [1, 2, 4, 10, 16], + result: ['hit', 'miss'], +}); + +function main({ n, platform, count, result }) { + const implementation = platform === 'posix' ? posix : win32; + const names = Array.from({ length: count - 1 }, (_, index) => `part${index}`); + const pattern = ['**', ...names.flatMap((name) => [name, '**']), 'file.js'].join('/'); + const parts = ['root', ...names.flatMap((name) => [name, 'dir']), 'file.js']; + if (result === 'miss') { + if (names.length) parts[parts.length - 3] = 'absent'; + else parts[parts.length - 1] = 'file.txt'; + } + const filename = parts.join(implementation.sep); + const expected = result === 'hit'; + assert.strictEqual(implementation.matchesGlob(filename, pattern), expected); + let actual; + bench.start(); + for (let index = 0; index < n; index++) actual = implementation.matchesGlob(filename, pattern); + bench.end(n); + assert.strictEqual(actual, expected); +} diff --git a/src/glob/glob_matcher.cc b/src/glob/glob_matcher.cc index 37da737dbe40..e0cca5cf43e6 100644 --- a/src/glob/glob_matcher.cc +++ b/src/glob/glob_matcher.cc @@ -584,6 +584,7 @@ class PathMatcher { struct Section { size_t begin, end; // pattern part indices ptrdiff_t after; // last possible file index this section can start at + size_t non_gs; }; Tri MatchGlobstar(Parts file, @@ -660,13 +661,11 @@ class PathMatcher { // split body into globstar-delimited sections with their last possible // start positions std::vector
sections; - sections.push_back({body_begin, body_begin, 0}); - std::vector non_gs_sums{0}; + sections.push_back({body_begin, body_begin, 0, 0}); size_t non_gs = 0; for (size_t i = body_begin; i < body_end; i++) { if (pattern[i].kind == PartMatcher::Kind::kGlobstar) { - non_gs_sums.push_back(non_gs); - sections.push_back({i + 1, i + 1, 0}); + sections.push_back({i + 1, i + 1, 0, non_gs}); } else { sections.back().end = i + 1; non_gs++; @@ -677,7 +676,7 @@ class PathMatcher { { size_t i = sections.size() - 1; for (Section& s : sections) { - s.after = file_length - (static_cast(non_gs_sums[i--]) + + s.after = file_length - (static_cast(sections[i--].non_gs) + static_cast(s.end - s.begin)); } } diff --git a/test/parallel/test-path-glob-sections.js b/test/parallel/test-path-glob-sections.js new file mode 100644 index 000000000000..7bc26f9edc50 --- /dev/null +++ b/test/parallel/test-path-glob-sections.js @@ -0,0 +1,25 @@ +'use strict'; + +require('../common'); +const assert = require('node:assert'); +const { posix, win32 } = require('node:path'); + +for (const implementation of [posix, win32]) { + for (const count of [1, 2, 4, 8, 9, 10, 16]) { + for (const prefix of ['part', '\u00e9tage', '\u4e2d']) { + const names = Array.from({ length: count - 1 }, (_, index) => `${prefix}${index}`); + const pattern = ['**', ...names.flatMap((name) => [name, '**']), 'file.js'].join('/'); + const parts = ['root', ...names.flatMap((name) => [name, 'dir']), 'file.js']; + assert.strictEqual(implementation.matchesGlob(parts.join(implementation.sep), pattern), true); + if (names.length) { + parts[parts.length - 3] = 'absent'; + } else { + parts[parts.length - 1] = 'file.txt'; + } + assert.strictEqual(implementation.matchesGlob(parts.join(implementation.sep), pattern), false); + } + } + assert.strictEqual(implementation.matchesGlob('root/.hidden/src/lib/file.js', '**/src/**/file.js'), false); + assert.strictEqual(implementation.matchesGlob('root/src/./lib/file.js', '**/src/**/file.js'), true); + assert.strictEqual(implementation.matchesGlob('root/src/lib/file.js/', '**/src/**/file.js'), true); +}