Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .agents/docs/performance-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ The 20 pinned Rolldown `normalize` call sites also show why the next owner matte

If a real Rolldown run is instrumented, keep aggregate-only counters outside timed runs: `borrowed_clean`, `dirty_early`, `dirty_late`, `invalid_encoding`, total input bytes, and bytes examined before the decision. Separate hot module-ID normalization, the relative pipeline, and other call sites; never record raw paths, hashes, or reversible samples.

The current safe normalization classifier reads `OsStr` encoded bytes directly for ASCII dot and native-separator checks on conventional Unix targets and Windows; Cygwin retains its existing Unicode-validation gate. This lets clean non-UTF-8 native encoding borrow without rebuilding on the encoded-byte targets, while exhaustive short-path tests compare classifier dispatch with unconditional full normalization for arbitrary Unix bytes and Windows wide units. The counterweight is non-UTF-8 encoding combined with dirty spelling: the former Unicode gate returned immediately, whereas the encoded-byte classifier must continue to the dirty marker. In the stacked Linux CodSpeed comparison, `non_utf8_dirty_before` and `non_utf8_before_dirty_late` were each 16.27% slower, while `non_utf8_lexically_clean` was 2.4 times as efficient and `non_utf8_absolute_target` was 17.33% more efficient with peak memory reduced from 149 to 64 bytes. Keep these rows separate rather than averaging a rare non-UTF-8-and-dirty cost into the observed clean valid Rolldown workload.

The explicitly renamed `borrowed_receiver/non_utf8_strict` row reports an additional 11.68% slowdown. Its Unix implementation remains the same `Path::to_str().map(Cow::Borrowed)` path between the stacked base and head, so this result reflects whole-crate compiled layout rather than added slash-conversion work. The vouched UTF-8 performance target keeps all three non-UTF-8 regressions visible without treating them as acceptance gates.

Generated-code inspection supports outlining `needs_normalization` while leaving the leading-parent helper and `normalize_inner` eligible for inlining. Linux CodSpeed showed broad classifier gains with the classifier outlined, but forcing `normalize_inner` out of line regressed the existing parent-cancellation benchmark by 12.82%; dirty normalization remains a supported general path rather than cold code.

## Recorded Rolldown runtime shape trace

Aggregate-only instrumentation used SugarPath commit `60b74dc` and Rolldown commit [`b9823050b`](https://github.com/rolldown/rolldown/commit/b9823050bc658ef65105148ea0504d4fbda7fa4c). Ten full ThreeJS generate/write/sourcemap builds produced 3,696 normalization calls, and one Rome TypeScript build produced 1,205. All 4,901 were ordinary clean borrowed inputs; their average encoded lengths were about 113.9 and 121.2 bytes respectively. No leading-parent, dirty, current-directory, or invalid-encoding normalization input appeared. A disposable external-module fixture forced the path-like external branch and produced one canonical leading-parent hit among seven calls, but that branch represented about 0.02% of the combined sample and the conditional count was only one; it is evidence that the shape exists, not a stable frequency estimate.
Expand Down Expand Up @@ -52,6 +58,8 @@ The final descendant strict-`String` variants were effectively equal in the full

The final Windows implementation adds a native-separator-aware root and common-prefix scan over borrowed canonical inputs, while dirty and mixed-separator inputs retain the normalization fallback. Native Windows-MSVC GitHub Actions generation run [`29181673809`](https://github.com/hyf0/sugar_path/actions/runs/29181673809) produced the final-API Windows evidence; ordinary PR CI checks the committed MSVC snapshot via `cargo allocs`: canonical native descendant `relative -> Cow<Path>` is zero allocations, `Cow::into_owned` is one, descendant and upward ordinary final-`String` composition are one, clean `PathBuf::into_slash` is zero, and clean owned cwd reuse has no fresh allocation plus one growth reallocation. Ordinary and verbatim same-share and different-share UNC rows each construct only their one required owned result. The scenario matrix inside the allocation runner still covers drive, UNC, verbatim, device, mixed-separator, invalid-wide, and root-relative cancellation controls; exact tests cover the additional components that cannot be represented as standalone relative paths. This goal did not authorize or run Docker, Wine, or containers; non-container GNU/MSVC cross-checks and native Actions supply the final build and allocation evidence.

Internally produced slash-form relative outcomes are valid UTF-8. On Windows, their owned `String` can therefore replace only ASCII `/` bytes with `\` in its existing allocation before becoming a `PathBuf`; a pointer-and-capacity test locks both exact Unicode output and buffer reuse without unsafe code. Borrowed slash conversion retains its single-pass replacement path because a `memchr` precheck followed by `str::replace` regressed the existing owned-result benchmark by 12.68% in Linux CodSpeed. Invalid native encoding still follows the strict failure or explicit lossy paths before this helper. GNU/MSVC cross-compilation proves target coverage only, so native Windows correctness, allocation snapshots, and timing remain the acceptance gate for the owned-buffer reuse.

## Benchmark audit findings

The pre-baseline suite aggregated many paths into unnamed batch costs, did not declare throughput, black-boxed outputs but not inputs, and used Unix-shaped absolute fixtures on Windows. `relative_with_dots` normalized both inputs before calling `relative`, so it never exercised the named dot slow path. `as_path_vs_path_new` measured both operations in one closure and could not compare them. These benchmarks could detect very large changes but could not attribute smaller allocation or branch regressions.
Expand Down Expand Up @@ -194,7 +202,7 @@ The pinned [`rust-performance-improvement-plan` at `fe59e3d`](https://github.com
| Reserve capacity, reuse collections, and avoid `clone`/`to_owned` | Adopted where ownership proves reuse. The final `into_normalized` and strict `into_slash` operations reuse clean owned buffers, and an owned cwd moves into `absolutize_with`; dirty normalization still builds the exact transformed representation rather than forcing mutation into the old buffer. |
| Use `Cow`, `SmallVec`, and compact storage for short values | Retained where justified. `Cow<Path>` exposes allocation-free clean paths; normalization uses eight inline references, while lexical relative calculation uses 16 because the pinned Rolldown distribution is 10 components at p99 and 13 at maximum. Dedicated dirty and 24-level tests execute both spill paths. |
| Avoid repeated `Path::to_string_lossy` | Adopted for valid-Unicode owned input through strict `into_slash` and for the ordinary relative-to-final-`String` composition. The earlier public fused method was removed; invalid native encoding still requires explicit lossy re-encoding, with byte- and wide-unit tests proving the fallback semantics. |
| Add `#[inline]`, `#[cold]`, unchecked indexing, or compiler assertions | Use only with generated-code or profile evidence. Tiny classifiers are inline; no current profile justifies unsafe indexing or a permanent cold-path layout, and path lengths are too small to assume bounds checks dominate. |
| Add `#[inline]`, `#[cold]`, unchecked indexing, or compiler assertions | Use only with generated-code or profile evidence. The normalization classifier is outlined because generated code and CodSpeed favor that separation, while the rebuilding core and leading-parent helper remain eligible for inlining. No current profile justifies unsafe indexing or marking the general rebuilding path cold. |
| De-monomorphize generic functions | `relative(base: impl AsRef<Path>)` is a candidate for compile-time and binary-size measurement, not an assumed runtime optimization. Keep the generic shim until `cargo llvm-lines` or binary-size evidence shows meaningful duplication. |
| Faster hashers, sorting changes, regex caching, and parallelism | Not applicable to the current algorithms: SugarPath has no hash table, sorting, regex, or independent work large enough to repay coordination overhead. |
| Build flags such as one codegen unit, LTO, mimalloc, and `target-cpu=native` | The benchmark profile uses one codegen unit, fat LTO, and Rolldown's pinned mimalloc. Shared baselines deliberately reject `target-cpu=native`; target-specific SIMD experiments require a separate benchmark identity. |
Expand Down
28 changes: 14 additions & 14 deletions benchmarks/allocations/x86_64-pc-windows-msvc.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Each row measures one operation after allocation-capable setup and an untracked
| normalize / collapsing input -> normalized path | 1 | 0 | 0 |
| normalize / noncanonical leading parents -> normalized path | 1 | 0 | 0 |
| normalize / canonical leading parents -> normalized path | 0 | 0 | 0 |
| normalize / clean invalid encoding -> normalized path | 1 | 0 | 0 |
| normalize / clean invalid encoding -> normalized path | 0 | 0 | 0 |
| normalize / owned clean input via borrowed receiver -> PathBuf | 1 | 0 | 0 |
| normalize / owned clean input via owned receiver -> PathBuf | 0 | 0 | 0 |
| normalize / owned dirty input via borrowed receiver -> PathBuf | 1 | 0 | 0 |
Expand All @@ -26,7 +26,7 @@ Each row measures one operation after allocation-capable setup and an untracked
| normalize / owned collapsing input via borrowed receiver -> PathBuf | 2 | 0 | 0 |
| normalize / owned collapsing input via owned receiver -> PathBuf | 1 | 0 | 0 |
| normalize / owned invalid input via borrowed receiver -> PathBuf | 1 | 0 | 0 |
| normalize / owned invalid input via owned receiver -> PathBuf | 1 | 0 | 0 |
| normalize / owned invalid input via owned receiver -> PathBuf | 0 | 0 | 0 |
| pipeline / dirty join via borrowed receiver -> PathBuf | 2 | 0 | 2 |
| pipeline / dirty join via owned receiver -> PathBuf | 2 | 0 | 2 |
| pipeline / dirty join via borrowed receiver -> String | 3 | 0 | 2 |
Expand All @@ -52,13 +52,13 @@ Each row measures one operation after allocation-capable setup and an untracked
| relative / p99-depth equal leading-parent inputs -> relative path | 1 | 0 | 0 |
| relative / p99-depth unequal leading-parent inputs -> relative path | 1 | 0 | 0 |
| relative / current-directory spellings -> relative path | 0 | 0 | 0 |
| relative / dotted slow path -> relative path | 4 | 0 | 0 |
| relative / invalid-encoding absolute target -> relative path (setup excluded) | 3 | 0 | 0 |
| relative / dotted slow path -> relative path | 3 | 0 | 0 |
| relative / invalid-encoding absolute target -> relative path (setup excluded) | 1 | 0 | 0 |
| relative / invalid-encoding relative target -> relative path (setup excluded) | 1 | 0 | 0 |
| Windows / absolute forward-slash inputs -> relative path | 2 | 0 | 0 |
| Windows / absolute forward-slash inputs -> relative path | 1 | 0 | 0 |
| Windows / same drive-relative prefix -> relative path | 1 | 0 | 0 |
| Windows / different drive-relative prefix -> relative path | 4 | 0 | 0 |
| Windows / root-relative inputs -> relative path | 3 | 0 | 0 |
| Windows / root-relative inputs -> relative path | 1 | 0 | 0 |
| Windows / ordinary UNC same share -> relative path | 1 | 0 | 0 |
| Windows / ordinary UNC different share -> relative path | 1 | 0 | 0 |
| Windows / verbatim UNC same share -> relative path | 1 | 0 | 0 |
Expand Down Expand Up @@ -101,7 +101,7 @@ Each row measures one operation after allocation-capable setup and an untracked
| normalize / collapsing input -> normalized path | 6 | 0 |
| normalize / noncanonical leading parents -> normalized path | 51 | 0 |
| normalize / canonical leading parents -> normalized path | 0 | 0 |
| normalize / clean invalid encoding -> normalized path | 70 | 0 |
| normalize / clean invalid encoding -> normalized path | 0 | 0 |
| normalize / owned clean input via borrowed receiver -> PathBuf | 58 | 0 |
| normalize / owned clean input via owned receiver -> PathBuf | 0 | 0 |
| normalize / owned dirty input via borrowed receiver -> PathBuf | 58 | 0 |
Expand All @@ -111,7 +111,7 @@ Each row measures one operation after allocation-capable setup and an untracked
| normalize / owned collapsing input via borrowed receiver -> PathBuf | 7 | 0 |
| normalize / owned collapsing input via owned receiver -> PathBuf | 6 | 0 |
| normalize / owned invalid input via borrowed receiver -> PathBuf | 70 | 0 |
| normalize / owned invalid input via owned receiver -> PathBuf | 70 | 0 |
| normalize / owned invalid input via owned receiver -> PathBuf | 0 | 0 |
| pipeline / dirty join via borrowed receiver -> PathBuf | 100 | 126 |
| pipeline / dirty join via owned receiver -> PathBuf | 100 | 126 |
| pipeline / dirty join via borrowed receiver -> String | 170 | 126 |
Expand All @@ -133,17 +133,17 @@ Each row measures one operation after allocation-capable setup and an untracked
| relative / relative inputs -> relative path | 31 | 0 |
| relative / dotted relative inputs -> relative path | 23 | 0 |
| relative / equal leading-parent inputs -> relative path | 23 | 0 |
| relative / unequal leading-parent inputs -> relative path | 383 | 276 |
| relative / unequal leading-parent inputs -> relative path | 60 | 0 |
| relative / p99-depth equal leading-parent inputs -> relative path | 18 | 0 |
| relative / p99-depth unequal leading-parent inputs -> relative path | 427 | 276 |
| relative / p99-depth unequal leading-parent inputs -> relative path | 87 | 0 |
| relative / current-directory spellings -> relative path | 0 | 0 |
| relative / dotted slow path -> relative path | 195 | 0 |
| relative / invalid-encoding absolute target -> relative path (setup excluded) | 139 | 0 |
| relative / dotted slow path -> relative path | 164 | 0 |
| relative / invalid-encoding absolute target -> relative path (setup excluded) | 48 | 0 |
| relative / invalid-encoding relative target -> relative path (setup excluded) | 24 | 0 |
| Windows / absolute forward-slash inputs -> relative path | 56 | 0 |
| Windows / absolute forward-slash inputs -> relative path | 28 | 0 |
| Windows / same drive-relative prefix -> relative path | 18 | 0 |
| Windows / different drive-relative prefix -> relative path | 178 | 0 |
| Windows / root-relative inputs -> relative path | 51 | 0 |
| Windows / root-relative inputs -> relative path | 18 | 0 |
| Windows / ordinary UNC same share -> relative path | 27 | 0 |
| Windows / ordinary UNC different share -> relative path | 36 | 0 |
| Windows / verbatim UNC same share -> relative path | 27 | 0 |
Expand Down
16 changes: 8 additions & 8 deletions benchmarks/allocations/x86_64-unknown-linux-gnu.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Each row measures one operation after allocation-capable setup and an untracked
| normalize / collapsing input -> normalized path | 1 | 0 | 0 |
| normalize / noncanonical leading parents -> normalized path | 1 | 0 | 0 |
| normalize / canonical leading parents -> normalized path | 0 | 0 | 0 |
| normalize / clean invalid encoding -> normalized path | 1 | 0 | 0 |
| normalize / clean invalid encoding -> normalized path | 0 | 0 | 0 |
| normalize / owned clean input via borrowed receiver -> PathBuf | 1 | 0 | 0 |
| normalize / owned clean input via owned receiver -> PathBuf | 0 | 0 | 0 |
| normalize / owned dirty input via borrowed receiver -> PathBuf | 1 | 0 | 0 |
Expand All @@ -26,7 +26,7 @@ Each row measures one operation after allocation-capable setup and an untracked
| normalize / owned collapsing input via borrowed receiver -> PathBuf | 2 | 0 | 0 |
| normalize / owned collapsing input via owned receiver -> PathBuf | 1 | 0 | 0 |
| normalize / owned invalid input via borrowed receiver -> PathBuf | 1 | 0 | 0 |
| normalize / owned invalid input via owned receiver -> PathBuf | 1 | 0 | 0 |
| normalize / owned invalid input via owned receiver -> PathBuf | 0 | 0 | 0 |
| pipeline / dirty join via borrowed receiver -> PathBuf | 2 | 0 | 2 |
| pipeline / dirty join via owned receiver -> PathBuf | 2 | 0 | 2 |
| pipeline / dirty join via borrowed receiver -> String | 3 | 0 | 2 |
Expand All @@ -50,7 +50,7 @@ Each row measures one operation after allocation-capable setup and an untracked
| relative / p99-depth unequal leading-parent inputs -> relative path | 1 | 0 | 0 |
| relative / current-directory spellings -> relative path | 0 | 0 | 0 |
| relative / dotted slow path -> relative path | 1 | 0 | 0 |
| relative / invalid-encoding absolute target -> relative path (setup excluded) | 3 | 0 | 3 |
| relative / invalid-encoding absolute target -> relative path (setup excluded) | 1 | 0 | 3 |
| relative / invalid-encoding relative target -> relative path (setup excluded) | 1 | 0 | 0 |
| to_slash / native path -> slash text | 0 | 0 | 0 |
| to_slash_lossy / invalid encoding -> lossy slash text (setup excluded) | 1 | 0 | 1 |
Expand Down Expand Up @@ -88,7 +88,7 @@ Each row measures one operation after allocation-capable setup and an untracked
| normalize / collapsing input -> normalized path | 6 | 0 |
| normalize / noncanonical leading parents -> normalized path | 51 | 0 |
| normalize / canonical leading parents -> normalized path | 0 | 0 |
| normalize / clean invalid encoding -> normalized path | 66 | 0 |
| normalize / clean invalid encoding -> normalized path | 0 | 0 |
| normalize / owned clean input via borrowed receiver -> PathBuf | 56 | 0 |
| normalize / owned clean input via owned receiver -> PathBuf | 0 | 0 |
| normalize / owned dirty input via borrowed receiver -> PathBuf | 58 | 0 |
Expand All @@ -98,7 +98,7 @@ Each row measures one operation after allocation-capable setup and an untracked
| normalize / owned collapsing input via borrowed receiver -> PathBuf | 7 | 0 |
| normalize / owned collapsing input via owned receiver -> PathBuf | 6 | 0 |
| normalize / owned invalid input via borrowed receiver -> PathBuf | 66 | 0 |
| normalize / owned invalid input via owned receiver -> PathBuf | 66 | 0 |
| normalize / owned invalid input via owned receiver -> PathBuf | 0 | 0 |
| pipeline / dirty join via borrowed receiver -> PathBuf | 96 | 115 |
| pipeline / dirty join via owned receiver -> PathBuf | 96 | 115 |
| pipeline / dirty join via borrowed receiver -> String | 164 | 115 |
Expand All @@ -117,12 +117,12 @@ Each row measures one operation after allocation-capable setup and an untracked
| relative / relative inputs -> relative path | 31 | 0 |
| relative / dotted relative inputs -> relative path | 23 | 0 |
| relative / equal leading-parent inputs -> relative path | 23 | 0 |
| relative / unequal leading-parent inputs -> relative path | 203 | 250 |
| relative / unequal leading-parent inputs -> relative path | 60 | 0 |
| relative / p99-depth equal leading-parent inputs -> relative path | 18 | 0 |
| relative / p99-depth unequal leading-parent inputs -> relative path | 220 | 388 |
| relative / p99-depth unequal leading-parent inputs -> relative path | 87 | 0 |
| relative / current-directory spellings -> relative path | 0 | 0 |
| relative / dotted slow path -> relative path | 31 | 0 |
| relative / invalid-encoding absolute target -> relative path (setup excluded) | 93 | 112 |
| relative / invalid-encoding absolute target -> relative path (setup excluded) | 8 | 112 |
| relative / invalid-encoding relative target -> relative path (setup excluded) | 22 | 0 |
| to_slash / native path -> slash text | 0 | 0 |
| to_slash_lossy / invalid encoding -> lossy slash text (setup excluded) | 66 | 132 |
Expand Down
Loading
Loading