Commit bb3f9176ca9 for nodejs
commit bb3f9176ca9ae9cc304bd460969046340c14d4a0
Author: Roberto Bianchi <rbianchidev@gmail.com>
Date: Mon Oct 19 16:53:43 2026 +0200
fs: reduce glob section allocations
Store prefix counts in section records to avoid a separate vector
and its allocations during multi-globstar matching.
Signed-off-by: Roberto Bianchi <roberto.bianchi@spendesk.com>
PR-URL: https://github.com/nodejs/node/pull/66716
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
diff --git a/benchmark/path/matchesGlob-filter.js b/benchmark/path/matchesGlob-filter.js
new file mode 100644
index 00000000000..8439a24fe8a
--- /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 00000000000..55f5d4de6ad
--- /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 37da737dbe4..e0cca5cf43e 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<Section> sections;
- sections.push_back({body_begin, body_begin, 0});
- std::vector<size_t> 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<ptrdiff_t>(non_gs_sums[i--]) +
+ s.after = file_length - (static_cast<ptrdiff_t>(sections[i--].non_gs) +
static_cast<ptrdiff_t>(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 00000000000..7bc26f9edc5
--- /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);
+}