Skip to content

Commit 1c28073

Browse files
committed
perf(windows): add caching for binary path resolution
Add comprehensive caching for expensive Windows PATH operations: - bin.ts: Cache binPath, binPathAll, and Volta binary lookups - spawn.ts: Cache binary path resolution in spawn operations - git.ts: Cache git binary path, realpath calls, and git root lookups - dlx/detect.ts: Cache package.json path and content lookups - dlx/package.ts: Cache binary path resolution with extension - constants/agents.ts: Share npm path resolution between exports - process-lock.ts: Use single statSync instead of existsSync + statSync All caches validate entries with existsSync and remove stale entries. Also adjusts vitest config to reduce parallelism in CI (maxForks 4, disable concurrent) to prevent worker timeout issues. Includes unit tests for cache validation and invalidation behavior.
1 parent 2544e9f commit 1c28073

9 files changed

Lines changed: 456 additions & 107 deletions

File tree

.config/vitest.config.mts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,10 @@ const vitestConfig = defineConfig({
9797
},
9898
forks: {
9999
// CI: Use forks for stability (no worker timeout issues)
100+
// Limit forks in CI to prevent file system contention on Windows
100101
singleFork: isCoverageEnabled,
101-
maxForks: isCoverageEnabled ? 1 : 16,
102-
minForks: isCoverageEnabled ? 1 : 4,
102+
maxForks: isCoverageEnabled ? 1 : process.env.CI ? 4 : 16,
103+
minForks: isCoverageEnabled ? 1 : process.env.CI ? 2 : 4,
103104
isolate: true,
104105
},
105106
},
@@ -110,8 +111,9 @@ const vitestConfig = defineConfig({
110111
hookTimeout: 10_000,
111112
// Speed optimizations
112113
sequence: {
113-
// Run tests concurrently within suites
114-
concurrent: true,
114+
// Run tests concurrently within suites locally, but sequentially in CI
115+
// to prevent worker timeouts from parallel binary path resolutions
116+
concurrent: !process.env.CI,
115117
},
116118
// Bail early on first failure in CI
117119
bail: process.env.CI ? 1 : 0,

src/bin.ts

Lines changed: 140 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ import { readJsonSync } from './fs'
1313
import { isPath, normalizePath } from './paths/normalize'
1414
import { spawn } from './spawn'
1515

16+
// Cache for binary path resolutions to avoid repeated PATH searches.
17+
// Cache is validated with existsSync() which is much cheaper than PATH search.
18+
const binPathCache = new Map<string, string>()
19+
// Separate cache for 'all: true' results (array of paths).
20+
const binPathAllCache = new Map<string, string[]>()
21+
// Cache for Volta binary path resolutions (keyed by volta path + binary name).
22+
const voltaBinCache = new Map<string, string>()
23+
1624
let _fs: typeof import('node:fs') | undefined
1725
/**
1826
* Lazily load the fs module to avoid Webpack errors.
@@ -75,10 +83,30 @@ export async function execBin(
7583
args?: string[],
7684
options?: import('./spawn').SpawnOptions,
7785
) {
78-
// Resolve the binary path.
79-
const resolvedPath = isPath(binPath)
80-
? resolveRealBinSync(binPath)
81-
: await whichReal(binPath)
86+
// Resolve the binary path, using cache for binary names (not paths).
87+
let resolvedPath: string | string[] | undefined
88+
if (isPath(binPath)) {
89+
resolvedPath = resolveRealBinSync(binPath)
90+
} else {
91+
// Check cache first for binary names.
92+
// Validate with existsSync() - cheaper than full PATH search.
93+
const cached = binPathCache.get(binPath)
94+
if (cached) {
95+
if (getFs().existsSync(cached)) {
96+
resolvedPath = cached
97+
} else {
98+
// Cached path no longer exists, remove stale entry.
99+
binPathCache.delete(binPath)
100+
}
101+
}
102+
if (!resolvedPath) {
103+
resolvedPath = await whichReal(binPath)
104+
// Cache the result if found.
105+
if (typeof resolvedPath === 'string') {
106+
binPathCache.set(binPath, resolvedPath)
107+
}
108+
}
109+
}
82110

83111
if (!resolvedPath) {
84112
const error = new Error(
@@ -127,34 +155,30 @@ export function findRealBin(
127155
}
128156

129157
// Fall back to whichModule.sync if no direct path found.
158+
// Use all: true to get all paths in a single call (avoids double PATH search on Windows).
130159
/* c8 ignore next - External which call */
131-
const binPath = whichModule.sync(binName, { nothrow: true })
132-
if (binPath) {
133-
const binDir = path.dirname(binPath)
160+
const allPaths = whichModule.sync(binName, { all: true, nothrow: true }) || []
161+
// Ensure allPaths is an array.
162+
const pathsArray = Array.isArray(allPaths)
163+
? allPaths
164+
: typeof allPaths === 'string'
165+
? [allPaths]
166+
: []
167+
168+
if (pathsArray.length === 0) {
169+
return undefined
170+
}
134171

135-
if (isShadowBinPath(binDir)) {
136-
// This is likely a shadowed binary, try to find the real one.
137-
/* c8 ignore next 2 - External which call */
138-
const allPaths =
139-
whichModule.sync(binName, { all: true, nothrow: true }) || []
140-
// Ensure allPaths is an array.
141-
const pathsArray = Array.isArray(allPaths)
142-
? allPaths
143-
: typeof allPaths === 'string'
144-
? [allPaths]
145-
: []
146-
147-
for (const altPath of pathsArray) {
148-
const altDir = path.dirname(altPath)
149-
if (!isShadowBinPath(altDir)) {
150-
return altPath
151-
}
152-
}
172+
// First, try to find a non-shadow bin path.
173+
for (const binPath of pathsArray) {
174+
const binDir = path.dirname(binPath)
175+
if (!isShadowBinPath(binDir)) {
176+
return binPath
153177
}
154-
return binPath
155178
}
156-
// If all else fails, return undefined to indicate binary not found.
157-
return undefined
179+
180+
// If all paths are shadow bins, return the first one.
181+
return pathsArray[0]
158182
}
159183

160184
/**
@@ -289,6 +313,17 @@ export function resolveRealBinSync(binPath: string): string {
289313
basename === 'node' ? -1 : (/(?<=\/)\.volta\//i.exec(binPath)?.index ?? -1)
290314
if (voltaIndex !== -1) {
291315
const voltaPath = binPath.slice(0, voltaIndex)
316+
// Check Volta cache first - keyed by volta path + binary name.
317+
const voltaCacheKey = `${voltaPath}:${basename}`
318+
const cachedVolta = voltaBinCache.get(voltaCacheKey)
319+
if (cachedVolta) {
320+
if (fs.existsSync(cachedVolta)) {
321+
return cachedVolta
322+
}
323+
// Cached Volta path no longer exists, remove stale entry.
324+
voltaBinCache.delete(voltaCacheKey)
325+
}
326+
292327
const voltaToolsPath = path.join(voltaPath, 'tools')
293328
const voltaImagePath = path.join(voltaToolsPath, 'image')
294329
const voltaUserPath = path.join(voltaToolsPath, 'user')
@@ -337,10 +372,13 @@ export function resolveRealBinSync(binPath: string): string {
337372
}
338373
}
339374
if (voltaBinPath) {
375+
let resolvedVoltaPath = voltaBinPath
340376
try {
341-
return normalizePath(fs.realpathSync.native(voltaBinPath))
377+
resolvedVoltaPath = normalizePath(fs.realpathSync.native(voltaBinPath))
342378
} catch {}
343-
return voltaBinPath
379+
// Cache the resolved Volta path.
380+
voltaBinCache.set(voltaCacheKey, resolvedVoltaPath)
381+
return resolvedVoltaPath
344382
}
345383
}
346384
if (WIN32) {
@@ -689,9 +727,33 @@ export async function whichReal(
689727
binName: string,
690728
options?: WhichOptions,
691729
): Promise<string | string[] | undefined> {
692-
// whichModule is imported at the top
730+
const fs = getFs()
693731
// Default to nothrow: true if not specified to return undefined instead of throwing
694732
const opts = { nothrow: true, ...options }
733+
734+
// Use cache - validate with existsSync() which is cheaper than full PATH search.
735+
if (opts.all) {
736+
// Check array cache for 'all: true' lookups.
737+
// Only validate first path for performance - if primary binary exists, assume others do too.
738+
const cachedAll = binPathAllCache.get(binName)
739+
if (cachedAll && cachedAll.length > 0) {
740+
if (fs.existsSync(cachedAll[0]!)) {
741+
return cachedAll
742+
}
743+
// Primary cached path no longer exists, remove stale entry.
744+
binPathAllCache.delete(binName)
745+
}
746+
} else {
747+
const cached = binPathCache.get(binName)
748+
if (cached) {
749+
if (fs.existsSync(cached)) {
750+
return cached
751+
}
752+
// Cached path no longer exists, remove stale entry.
753+
binPathCache.delete(binName)
754+
}
755+
}
756+
695757
// Depending on options `whichModule` may throw if `binName` is not found.
696758
// With nothrow: true, it returns null when `binName` is not found.
697759
/* c8 ignore next - External which call */
@@ -705,15 +767,24 @@ export async function whichReal(
705767
? [result]
706768
: undefined
707769
// If all is true and we have paths, resolve each one.
708-
return paths?.length ? paths.map(p => resolveRealBinSync(p)) : paths
770+
if (paths?.length) {
771+
const resolved = paths.map(p => resolveRealBinSync(p))
772+
// Cache the resolved paths.
773+
binPathAllCache.set(binName, resolved)
774+
return resolved
775+
}
776+
return paths
709777
}
710778

711779
// If result is undefined (binary not found), return undefined
712780
if (!result) {
713781
return undefined
714782
}
715783

716-
return resolveRealBinSync(result)
784+
const resolved = resolveRealBinSync(result)
785+
// Cache the resolved path.
786+
binPathCache.set(binName, resolved)
787+
return resolved
717788
}
718789

719790
/**
@@ -725,8 +796,33 @@ export function whichRealSync(
725796
binName: string,
726797
options?: WhichOptions,
727798
): string | string[] | undefined {
799+
const fs = getFs()
728800
// Default to nothrow: true if not specified to return undefined instead of throwing
729801
const opts = { nothrow: true, ...options }
802+
803+
// Use cache - validate with existsSync() which is cheaper than full PATH search.
804+
if (opts.all) {
805+
// Check array cache for 'all: true' lookups.
806+
// Only validate first path for performance - if primary binary exists, assume others do too.
807+
const cachedAll = binPathAllCache.get(binName)
808+
if (cachedAll && cachedAll.length > 0) {
809+
if (fs.existsSync(cachedAll[0]!)) {
810+
return cachedAll
811+
}
812+
// Primary cached path no longer exists, remove stale entry.
813+
binPathAllCache.delete(binName)
814+
}
815+
} else {
816+
const cached = binPathCache.get(binName)
817+
if (cached) {
818+
if (fs.existsSync(cached)) {
819+
return cached
820+
}
821+
// Cached path no longer exists, remove stale entry.
822+
binPathCache.delete(binName)
823+
}
824+
}
825+
730826
// Depending on options `which` may throw if `binName` is not found.
731827
// With nothrow: true, it returns null when `binName` is not found.
732828
const result = whichSync(binName, opts)
@@ -739,15 +835,24 @@ export function whichRealSync(
739835
? [result]
740836
: undefined
741837
// If all is true and we have paths, resolve each one.
742-
return paths?.length ? paths.map(p => resolveRealBinSync(p)) : paths
838+
if (paths?.length) {
839+
const resolved = paths.map(p => resolveRealBinSync(p))
840+
// Cache the resolved paths.
841+
binPathAllCache.set(binName, resolved)
842+
return resolved
843+
}
844+
return paths
743845
}
744846

745847
// If result is undefined (binary not found), return undefined
746848
if (!result) {
747849
return undefined
748850
}
749851

750-
return resolveRealBinSync(result as string)
852+
const resolved = resolveRealBinSync(result as string)
853+
// Cache the resolved path.
854+
binPathCache.set(binName, resolved)
855+
return resolved
751856
}
752857

753858
/**

src/constants/agents.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,33 @@ export const BUN = 'bun'
1313
export const VLT = 'vlt'
1414
export const NPX = 'npx'
1515

16-
// NPM binary path - resolved at runtime using which.
17-
export const NPM_BIN_PATH = /*@__PURE__*/ (() => {
16+
// NPM binary path - resolved once at runtime using which.
17+
// Shared between NPM_BIN_PATH and NPM_REAL_EXEC_PATH to avoid duplicate which.sync calls.
18+
const _npmBinPath = /*@__PURE__*/ (() => {
1819
try {
19-
// module is imported at the top
20-
return which.sync('npm', { nothrow: true }) || 'npm'
20+
return which.sync('npm', { nothrow: true }) || null
2121
} catch {
22-
return 'npm'
22+
return null
2323
}
2424
})()
2525

26+
export const NPM_BIN_PATH = _npmBinPath || 'npm'
27+
2628
// NPM CLI entry point - resolved at runtime from npm bin location.
2729
// NOTE: This is kept for backward compatibility but NPM_BIN_PATH should be used instead
2830
// because cli.js exports a function that must be invoked, not executed directly.
2931
export const NPM_REAL_EXEC_PATH = /*@__PURE__*/ (() => {
3032
try {
31-
const { existsSync } = /*@__PURE__*/ require('fs')
32-
const path = /*@__PURE__*/ require('path')
33-
// module is imported at the top
34-
// Find npm binary using which.
35-
const npmBin = which.sync('npm', { nothrow: true })
36-
if (!npmBin) {
33+
// Reuse cached npm bin path to avoid duplicate which.sync call.
34+
if (!_npmBinPath) {
3735
return undefined
3836
}
37+
const { existsSync } = /*@__PURE__*/ require('fs')
38+
const path = /*@__PURE__*/ require('path')
3939
// npm bin is typically at: /path/to/node/bin/npm
4040
// cli.js is at: /path/to/node/lib/node_modules/npm/lib/cli.js
4141
// /path/to/node/bin
42-
const npmDir = path.dirname(npmBin)
42+
const npmDir = path.dirname(_npmBinPath)
4343
const nodeModulesPath = path.join(
4444
npmDir,
4545
'..',

0 commit comments

Comments
 (0)