From 1bd01ae0c6ab18105fd01d55bd981146dd7644bf Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Fri, 16 May 2025 15:32:35 +0200 Subject: [PATCH] path: improve path.resolve() performance when used as process.cwd() `path.resolve()` and `path.resolve('.')` is frequently called as alternative to process.cwd(). This minimized overhead for these specific cases. --- benchmark/path/join-posix.js | 4 ++-- benchmark/path/join-win32.js | 4 ++-- benchmark/path/normalize-posix.js | 2 +- benchmark/path/normalize-win32.js | 2 +- benchmark/path/resolve-posix.js | 11 +++++++---- benchmark/path/resolve-win32.js | 10 ++++++---- lib/path.js | 18 +++++++++++++++++- test/parallel/test-path-resolve.js | 4 ++++ 8 files changed, 40 insertions(+), 15 deletions(-) diff --git a/benchmark/path/join-posix.js b/benchmark/path/join-posix.js index 2e7836650af455..fe4ea4b4a4724a 100644 --- a/benchmark/path/join-posix.js +++ b/benchmark/path/join-posix.js @@ -16,8 +16,8 @@ function main({ n, paths }) { bench.start(); for (let i = 0; i < n; i++) { - if (i % 3 === 0) { - copy[1] = `${orig}${i}`; + if (i % 5 === 0) { + copy[1] = `${orig}/${i}`; posix.join(...copy); } else { posix.join(...args); diff --git a/benchmark/path/join-win32.js b/benchmark/path/join-win32.js index 3ad1c0c7ba2a61..41899dd9240842 100644 --- a/benchmark/path/join-win32.js +++ b/benchmark/path/join-win32.js @@ -16,8 +16,8 @@ function main({ n, paths }) { bench.start(); for (let i = 0; i < n; i++) { - if (i % 3 === 0) { - copy[1] = `${orig}${i}`; + if (i % 5 === 0) { + copy[1] = `${orig}\\${i}`; win32.join(...copy); } else { win32.join(...args); diff --git a/benchmark/path/normalize-posix.js b/benchmark/path/normalize-posix.js index 3e90bfc21aec46..4318ee0b147eda 100644 --- a/benchmark/path/normalize-posix.js +++ b/benchmark/path/normalize-posix.js @@ -17,7 +17,7 @@ const bench = common.createBenchmark(main, { function main({ n, path }) { bench.start(); for (let i = 0; i < n; i++) { - posix.normalize(i % 3 === 0 ? `${path}${i}` : path); + posix.normalize(i % 5 === 0 ? `${path}/${i}` : path); } bench.end(n); } diff --git a/benchmark/path/normalize-win32.js b/benchmark/path/normalize-win32.js index 33af7953ff547d..289b385d3f65a8 100644 --- a/benchmark/path/normalize-win32.js +++ b/benchmark/path/normalize-win32.js @@ -17,7 +17,7 @@ const bench = common.createBenchmark(main, { function main({ n, path }) { bench.start(); for (let i = 0; i < n; i++) { - win32.normalize(i % 3 === 0 ? `${path}${i}` : path); + win32.normalize(i % 5 === 0 ? `${path}\\${i}` : path); } bench.end(n); } diff --git a/benchmark/path/resolve-posix.js b/benchmark/path/resolve-posix.js index 4881947fe46b6d..edc3593bb7f4d9 100644 --- a/benchmark/path/resolve-posix.js +++ b/benchmark/path/resolve-posix.js @@ -4,23 +4,26 @@ const { posix } = require('path'); const bench = common.createBenchmark(main, { paths: [ + 'empty', '', + '.', ['', ''].join('|'), ['foo/bar', '/tmp/file/', '..', 'a/../subfile'].join('|'), ['a/b/c/', '../../..'].join('|'), + ['/a/b/c/', 'abc'].join('|'), ], n: [1e5], }); function main({ n, paths }) { - const args = paths.split('|'); + const args = paths === 'empty' ? [] : paths.split('|'); const copy = [...args]; - const orig = copy[0]; + const orig = copy[0] ?? ''; bench.start(); for (let i = 0; i < n; i++) { - if (i % 3 === 0) { - copy[0] = `${orig}${i}`; + if (i % 5 === 0) { + copy[0] = `${orig}/${i}`; posix.resolve(...copy); } else { posix.resolve(...args); diff --git a/benchmark/path/resolve-win32.js b/benchmark/path/resolve-win32.js index 822b98c2b86c52..a28f2f43e25314 100644 --- a/benchmark/path/resolve-win32.js +++ b/benchmark/path/resolve-win32.js @@ -4,7 +4,9 @@ const { win32 } = require('path'); const bench = common.createBenchmark(main, { paths: [ + 'empty', '', + '.', ['', ''].join('|'), ['c:/ignore', 'd:\\a/b\\c/d', '\\e.exe'].join('|'), ['c:/blah\\blah', 'd:/games', 'c:../a'].join('|'), @@ -13,14 +15,14 @@ const bench = common.createBenchmark(main, { }); function main({ n, paths }) { - const args = paths.split('|'); + const args = paths === 'empty' ? [] : paths.split('|'); const copy = [...args]; - const orig = copy[0]; + const orig = copy[0] ?? ''; bench.start(); for (let i = 0; i < n; i++) { - if (i % 3 === 0) { - copy[0] = `${orig}${i}`; + if (i % 5 === 0) { + copy[0] = `${orig}\\${i}`; win32.resolve(...copy); } else { win32.resolve(...args); diff --git a/lib/path.js b/lib/path.js index aa64a2032c6a71..26c020f5de06d9 100644 --- a/lib/path.js +++ b/lib/path.js @@ -95,7 +95,7 @@ function normalizeString(path, allowAboveRoot, separator, isPathSeparator) { StringPrototypeCharCodeAt(res, res.length - 1) !== CHAR_DOT || StringPrototypeCharCodeAt(res, res.length - 2) !== CHAR_DOT) { if (res.length > 2) { - const lastSlashIndex = StringPrototypeLastIndexOf(res, separator); + const lastSlashIndex = res.length - lastSegmentLength - 1; if (lastSlashIndex === -1) { res = ''; lastSegmentLength = 0; @@ -163,6 +163,8 @@ function _format(sep, pathObject) { return dir === pathObject.root ? `${dir}${base}` : `${dir}${sep}${base}`; } +const forwardSlashRegExp = /\//g; + const win32 = { /** * path.resolve([from ...], to) @@ -186,6 +188,14 @@ const win32 = { } } else if (resolvedDevice.length === 0) { path = process.cwd(); + // Fast path for current directory + if (args.length === 0 || ((args.length === 1 && (args[0] === '' || args[0] === '.')) && + isPathSeparator(StringPrototypeCharCodeAt(path, 0)))) { + if (!isWindows) { + path = StringPrototypeReplace(path, forwardSlashRegExp, '\\'); + } + return path; + } } else { // Windows has the concept of drive-specific current working // directories. If we've resolved a drive letter but not yet an @@ -1171,6 +1181,12 @@ const posix = { * @returns {string} */ resolve(...args) { + if (args.length === 0 || (args.length === 1 && (args[0] === '' || args[0] === '.'))) { + const cwd = posixCwd(); + if (StringPrototypeCharCodeAt(cwd, 0) === CHAR_FORWARD_SLASH) { + return cwd; + } + } let resolvedPath = ''; let resolvedAbsolute = false; diff --git a/test/parallel/test-path-resolve.js b/test/parallel/test-path-resolve.js index 6b1dfa7567d3d1..088ed4b3ff183f 100644 --- a/test/parallel/test-path-resolve.js +++ b/test/parallel/test-path-resolve.js @@ -24,6 +24,8 @@ const resolveTests = [ [['c:/ignore', 'd:\\a/b\\c/d', '\\e.exe'], 'd:\\e.exe'], [['c:/ignore', 'c:/some/file'], 'c:\\some\\file'], [['d:/ignore', 'd:some/dir//'], 'd:\\ignore\\some\\dir'], + [[], process.cwd()], + [[''], process.cwd()], [['.'], process.cwd()], [['//server/share', '..', 'relative\\'], '\\\\server\\share\\relative'], [['c:/', '//'], 'c:\\'], @@ -42,6 +44,8 @@ const resolveTests = [ [[['/var/lib', '../', 'file/'], '/var/file'], [['/var/lib', '/../', 'file/'], '/file'], [['a/b/c/', '../../..'], posixyCwd], + [[], posixyCwd], + [[''], posixyCwd], [['.'], posixyCwd], [['/some/dir', '.', '/absolute/'], '/absolute'], [['/foo/tmp.3/', '../tmp.3/cycles/root.js'], '/foo/tmp.3/cycles/root.js'],