perf: improve resolver cache hit rate#481
Conversation
| /** @typedef {import("./Resolver").ResolveContextYield} ResolveContextYield */ | ||
| /** @typedef {{ [k: string]: undefined | ResolveRequest | ResolveRequest[] }} Cache */ | ||
|
|
||
| const RELATIVE_REQUEST_REGEXP = /^\.\.?(?:\/|$)/; |
There was a problem hiding this comment.
enhanced-resolve/lib/getInnerRequest.js
Line 28 in c1093c9
ported it from here
we can refactor these later if needed
|
|
||
| // described-resolve | ||
| plugins.push(new NextPlugin("described-resolve", "raw-resolve")); | ||
| if (unsafeCache) { |
There was a problem hiding this comment.
Moved UnsafeCachePlugin to described-resolve to access descriptionFileRoot and relativePath for cache key normalization
| function getCachePath(request) { | ||
| if (request.descriptionFileRoot && !request.module) { | ||
| return request.descriptionFileRoot; | ||
| } |
There was a problem hiding this comment.
Normalization to descriptionFileRoot is limited to !request.module because bare specifiers are resolved from the caller's directory, not the package root
ex:
reactfromsrc/b-><pkg>/node_modules/reactreactfromsrc/components-><pkg>/src/components/node_modules/react
Even within the same package, resolution results vary based on local node_modules. Collapsing these into a single package-root-based cache key would cause incorrect reuse of previously cached results in different directories
but this is a niche case that it's not worth considering. What do you think?
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #481 +/- ##
==========================================
- Coverage 93.43% 93.40% -0.04%
==========================================
Files 50 50
Lines 2483 2502 +19
Branches 751 759 +8
==========================================
+ Hits 2320 2337 +17
- Misses 134 136 +2
Partials 29 29
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@3ru Looks good for me, can you measure before and now? I think we need to add |
|
@alexander-akait I ran some experiments Ran each package from
The wins are concentrated in packages with deep directory trees like |
|
@3ru Thanks ⭐ |
Extends the cache-hit work from PR #481 with additional hot-path wins that don't change public behavior or semantics: - UnsafeCachePlugin.getCacheId: build the cache id by direct string concat with a `\0` separator instead of `JSON.stringify`ing a 6-key object per lookup. Paths, requests, queries and fragments produced by parseIdentifier never contain raw `\0`; context is still JSON-stringified when included, which escapes any `\0` there. - util/path.isRelativeRequest: char-code-based check that replaces the `/^\.\.?(?:\/|$)/` regex used in UnsafeCachePlugin and getInnerRequest. - DescriptionFileUtils.cdUp: single reverse char-code scan instead of two `lastIndexOf` calls (POSIX strings don't need to be fully re-scanned for a backslash that isn't there). - DescriptionFilePlugin: skip the global-regex backslash replace when the relative slice doesn't contain any backslashes. Also adds `benchmark/cases/unsafe-cache-miss-heavy/` which exercises the cache-id path directly (a miss pass followed by a hit pass over 42 unique requests, so `getCacheId` is the dominant cost) and unit tests for `isRelativeRequest`. Local results (Node 22, warm fs cache): unsafe-cache (ON, 3x repeat): 11.5k → 14.5k ops/s (+25%) unsafe-cache-miss-heavy: 2.2k → 2.8k ops/s (+27%) realistic-midsize (warm cache): 4.2k → 4.6k ops/s (+9%) realistic-midsize (cold cache): 42.5 → 46.6 ops/s (+10%) One internal change: UnsafeCachePlugin cache keys are no longer valid JSON, so test/yield.test.js was updated to split on the `\0` separator instead of `JSON.parse`ing a key. The cache-key format has never been part of the public API. https://claude.ai/code/session_014HMqKsoba7AvAVojZWECzz
Summary
This optimization aligns unsafe cache keys by package rather than by importer path, reducing redundant request re-resolution within the same package.
It is most effective when multiple importers in one package repeatedly resolve the same package-internal imports or package-local modules through different relative or absolute request forms.
This work is based on #449
What kind of change does this PR introduce?
perf
Did you add tests for your changes?
Yes
node_modulesshadows the package rootDoes this PR introduce a breaking change?
no
If relevant, what needs to be documented once your changes are merged or what have you already documented?
no
Use of AI
for making test cases