Skip to content

perf: eliminate symlink stat syscalls by reusing canonicalization#1184

Merged
graphite-app[bot] merged 1 commit into
mainfrom
perf/eliminate-symlink-stat
May 29, 2026
Merged

perf: eliminate symlink stat syscalls by reusing canonicalization#1184
graphite-app[bot] merged 1 commit into
mainfrom
perf/eliminate-symlink-stat

Conversation

@Boshen

@Boshen Boshen commented May 29, 2026

Copy link
Copy Markdown
Member

What

is_file/is_dir on a symlinked path issued an lstat (to detect the symlink) followed by a stat (to follow it and learn file-vs-dir). But canonicalization already read_links the same symlink for the final resolved path, so the stat duplicated work the resolver does anyway.

Cache::is_file/is_dir now take a symlinks flag. For a symlink, when it's set, they reuse canonicalize_impl and read the canonical target's already-cached lstat instead of issuing a standalone stat. ResolverGeneric injects its options.symlinks through thin is_file/is_dir wrappers; the cache-internal callers (find_package_json, the node_modules lookup helpers) thread it explicitly. When disabled — or if canonicalization fails — it falls back to a direct stat, the previous behavior.

Why it's correct

stat and canonicalization both report the final target's file/dir type, so the cached followed value is identical regardless of which path computes it. Resolution decisions (which only use the resulting bool) are unchanged — canonicalization is simply warmed slightly earlier and reused.

Impact

Measured over a fixed 16-request workload across the 8 package-manager layouts in fixtures/bench-pm:

layout before after
npm-flat 91 89
pnpm-isolated 122 114
pnpm-hoisted 122 114
yarn-flat 91 89
yarn-isolated 115 107
yarn-pnp 106 106
bun-flat 91 89
bun-isolated 122 114
total 860 822

All standalone symlink stat syscalls are eliminated. yarn-pnp is unchanged because it resolves through VPath and never follows symlinks.

@codecov

codecov Bot commented May 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.73684% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.78%. Comparing base (4e9ac5e) to head (be27c2f).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/dts_resolver.rs 80.00% 2 Missing ⚠️
src/lib.rs 95.23% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1184      +/-   ##
==========================================
+ Coverage   93.46%   93.78%   +0.32%     
==========================================
  Files          22       22              
  Lines        4208     4216       +8     
==========================================
+ Hits         3933     3954      +21     
+ Misses        275      262      -13     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Boshen Boshen force-pushed the perf/eliminate-symlink-stat branch from 15c54c3 to 08c49fa Compare May 29, 2026 16:40

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08c49fa980

ℹ️ 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".

Comment thread src/cache/cache_impl.rs Outdated
@Boshen Boshen force-pushed the perf/eliminate-symlink-stat branch from 08c49fa to 79a2692 Compare May 29, 2026 16:58
@codspeed-hq

codspeed-hq Bot commented May 29, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 5 regressed benchmarks
✅ 15 untouched benchmarks
⏩ 5 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
pm/yarn-isolated 1.1 ms 1 ms +6.49%
pm/bun-isolated 1.1 ms 1.1 ms -3.31%
pm/bun-flat 996.6 µs 1,041.9 µs -4.35%
resolver_memory[multi-thread] 385.4 µs 399.4 µs -3.5%
resolver_real[multi-thread] 393.3 µs 407.7 µs -3.52%
pm/npm-flat 942.1 µs 985.1 µs -4.37%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing perf/eliminate-symlink-stat (be27c2f) with main (4e9ac5e)2

Open in CodSpeed

Footnotes

  1. 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.

  2. No successful run was found on main (be27c2f) during the generation of this report, so 4e9ac5e was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Boshen Boshen added the merge label May 29, 2026

Boshen commented May 29, 2026

Copy link
Copy Markdown
Member Author

Merge activity

)

## What

`is_file`/`is_dir` on a symlinked path issued an `lstat` (to detect the symlink) followed by a `stat` (to follow it and learn file-vs-dir). But canonicalization already `read_link`s the same symlink for the final resolved path, so the `stat` duplicated work the resolver does anyway.

`Cache::is_file`/`is_dir` now take a `symlinks` flag. For a symlink, when it's set, they reuse `canonicalize_impl` and read the canonical target's already-cached `lstat` instead of issuing a standalone `stat`. `ResolverGeneric` injects its `options.symlinks` through thin `is_file`/`is_dir` wrappers; the cache-internal callers (`find_package_json`, the `node_modules` lookup helpers) thread it explicitly. When disabled — or if canonicalization fails — it falls back to a direct `stat`, the previous behavior.

## Why it's correct

`stat` and canonicalization both report the **final target's** file/dir type, so the cached `followed` value is identical regardless of which path computes it. Resolution decisions (which only use the resulting bool) are unchanged — canonicalization is simply warmed slightly earlier and reused.

## Impact

Measured over a fixed 16-request workload across the 8 package-manager layouts in `fixtures/bench-pm`:

| layout | before | after |
| --- | --- | --- |
| npm-flat | 91 | 89 |
| pnpm-isolated | 122 | 114 |
| pnpm-hoisted | 122 | 114 |
| yarn-flat | 91 | 89 |
| yarn-isolated | 115 | 107 |
| yarn-pnp | 106 | 106 |
| bun-flat | 91 | 89 |
| bun-isolated | 122 | 114 |
| **total** | **860** | **822** |

All standalone symlink `stat` syscalls are eliminated. yarn-pnp is unchanged because it resolves through `VPath` and never follows symlinks.
@graphite-app graphite-app Bot force-pushed the perf/eliminate-symlink-stat branch from 79a2692 to be27c2f Compare May 29, 2026 17:15
@graphite-app graphite-app Bot merged commit be27c2f into main May 29, 2026
18 checks passed
@graphite-app graphite-app Bot removed the merge label May 29, 2026
@graphite-app graphite-app Bot deleted the perf/eliminate-symlink-stat branch May 29, 2026 17:18
@oxc-guard oxc-guard Bot mentioned this pull request May 29, 2026
Boshen pushed a commit that referenced this pull request Jun 3, 2026
## 🤖 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant