perf: reduce resolution syscalls by unifying stat and lstat#1182
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1182 +/- ##
==========================================
+ Coverage 93.33% 93.46% +0.13%
==========================================
Files 22 22
Lines 4184 4208 +24
==========================================
+ Hits 3905 3933 +28
+ Misses 279 275 -4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Merging this PR will degrade performance by 5.07%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | pm/bun-flat |
925.2 µs | 996.8 µs | -7.19% |
| ❌ | pm/npm-flat |
936.8 µs | 1,027.9 µs | -8.87% |
| ❌ | pm/yarn-flat |
889.3 µs | 978.8 µs | -9.14% |
| ⚡ | resolver_memory[multi-thread] |
432.9 µs | 413.4 µs | +4.72% |
| ❌ | resolver_real[multi-thread] |
401 µs | 418.7 µs | -4.21% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing perf/lstat-first-metadata (4e9ac5e) with main (a539c40)2
Footnotes
-
5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
-
No successful run was found on
main(4e9ac5e) during the generation of this report, so a539c40 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩
44d7fcf to
e86ec37
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e86ec378f3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
## Summary
`FileSystemOs::metadata` already translates Yarn PnP **virtual** and **zip** paths through `VPath` before hitting the filesystem, but `symlink_metadata` did not — it `lstat`'d the raw path directly:
```rust
fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
Self::symlink_metadata(path) // no VPath translation
}
```
For a virtual/zip path that has no physical existence, that `lstat` fails, so canonicalization (`canonicalize_with_visited`) sees the error, treats the path as a non-symlink, and cannot resolve symlinks correctly under PnP.
This makes `symlink_metadata` mirror `metadata`'s `VPath` handling. Zip entries are never symlinks, so they reuse the same `file_type` lookup as `metadata`; virtual/native paths fall through to the real `lstat` on the translated physical path.
This is also a prerequisite for #1182, which routes `is_file`/`is_dir` through the cached `lstat` and so requires `symlink_metadata` to be VPath-aware.
## Verification
- `cargo test --all-features` (includes the Yarn PnP suite) — green
- `fixtures/pnp` JS resolver test — green
🤖 Generated with [Claude Code](https://claude.com/claude-code)
c3748ff to
2284d67
Compare
Merge activity
|
## Summary Module resolution looked at each path twice: a `stat` (`metadata`, follows symlinks) for `is_file`/`is_dir` during resolution, then an `lstat` (`symlink_metadata`) on the same path again during canonicalization. Most paths aren't symlinks, so the two passes were redundant — for `npm-flat`, 49 lstats found only **2** actual symlinks, and ~37 of those paths had already been `stat`'d. This unifies them so `is_file`/`is_dir` are **lstat-first**. ## What changed - `CachedMeta` now holds two 1-byte slots — `link` (the cached `lstat` view) and `followed` (the cached `stat` view). - `is_file`/`is_dir` read the cached `lstat`; for the common non-symlink case that *is* the answer (no `stat`), and only a genuine symlink triggers a follow-up `stat`. - Canonicalization reuses the same cached `lstat` instead of issuing its own. - `FileSystemOs::symlink_metadata` is now VPath-aware for Yarn PnP. Previously only `metadata()` translated virtual/zip paths, so lstat-backed `is_file`/`is_dir` would have failed under PnP. Net per path: `savings = |stat∩lstat| − symlinks-followed`. ## Results — 18–28% fewer syscalls Measured on the 16-request package-manager workload (same one the `package_managers` bench uses): | layout | before | after | |---|---|---| | npm-flat / yarn-flat / bun-flat | 126 | **91** | | pnpm-isolated / hoisted / bun-isolated | 149 | **122** | | yarn-isolated | 142 | **115** | | yarn-pnp | 144 | **106** | `stat` calls collapse from ~40–51 down to 0–8 (only the genuine symlink-follows). Behavior of `is_file`/`is_dir` is unchanged — they still return symlink-followed results. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2284d67 to
4e9ac5e
Compare
## 🤖 New release * `oxc_resolver`: 11.20.0 -> 11.21.0 * `oxc_resolver_napi`: 11.20.0 -> 11.21.0 <details><summary><i><b>Changelog</b></i></summary><p> ## `oxc_resolver` <blockquote> ## [11.21.0](v11.20.0...v11.21.0) - 2026-06-03 ### <!-- 0 -->🚀 Features - *(tsconfig)* support package.json imports field in extends ([#1199](#1199)) (by @Boshen) ### <!-- 1 -->🐛 Bug Fixes - *(tsconfig)* apply each referenced project's own `allowJs` ([#1198](#1198)) (by @shulaoda) - make symlink_metadata VPath-aware for Yarn PnP ([#1183](#1183)) (by @Boshen) ### <!-- 4 -->⚡ Performance - borrow relative main field instead of allocating a "./" prefix ([#1187](#1187)) (by @Boshen) - *(cache)* move package.json path into parse instead of cloning ([#1186](#1186)) (by @Boshen) - eliminate symlink stat syscalls by reusing canonicalization ([#1184](#1184)) (by @Boshen) - reduce resolution syscalls by unifying stat and lstat ([#1182](#1182)) (by @Boshen) ### <!-- 9 -->💼 Other - add baselines for each package manager x node_modules layout ([#1176](#1176)) (by @Boshen) ### Contributors * @shulaoda * @Boshen * @renovate[bot] </blockquote> </p></details> --- This PR was generated with [release-plz](https://github.com/release-plz/release-plz/). Co-authored-by: oxc-guard[bot] <276638029+oxc-guard[bot]@users.noreply.github.com>
Summary
Module resolution looked at each path twice: a
stat(metadata, follows symlinks) foris_file/is_dirduring resolution, then anlstat(symlink_metadata) on the same path again during canonicalization. Most paths aren't symlinks, so the two passes were redundant — fornpm-flat, 49 lstats found only 2 actual symlinks, and ~37 of those paths had already beenstat'd.This unifies them so
is_file/is_dirare lstat-first.What changed
CachedMetanow holds two 1-byte slots —link(the cachedlstatview) andfollowed(the cachedstatview).is_file/is_dirread the cachedlstat; for the common non-symlink case that is the answer (nostat), and only a genuine symlink triggers a follow-upstat.lstatinstead of issuing its own.FileSystemOs::symlink_metadatais now VPath-aware for Yarn PnP. Previously onlymetadata()translated virtual/zip paths, so lstat-backedis_file/is_dirwould have failed under PnP.Net per path:
savings = |stat∩lstat| − symlinks-followed.Results — 18–28% fewer syscalls
Measured on the 16-request package-manager workload (same one the
package_managersbench uses):statcalls collapse from ~40–51 down to 0–8 (only the genuine symlink-follows). Behavior ofis_file/is_diris unchanged — they still return symlink-followed results.🤖 Generated with Claude Code