Skip to content

perf(alias): short-circuit load_alias with a 1+2 byte prefix index#225

Merged
stormslowly merged 8 commits into
mainfrom
perf/alias-first-byte-index
May 20, 2026
Merged

perf(alias): short-circuit load_alias with a 1+2 byte prefix index#225
stormslowly merged 8 commits into
mainfrom
perf/alias-first-byte-index

Conversation

@stormslowly
Copy link
Copy Markdown
Collaborator

Why

load_alias runs a strip_prefix against every alias key for every resolve, even when the specifier's first character can already rule out every key — ./foo against bare-name aliases like react, @scope/x, etc. For configs with a few dozen aliases this adds up; the test suite uses 23 aliases and the loop becomes a measurable per-resolve tax.

A tiny precomputed prefix index turns the common "no possible match" case into a single byte-table lookup.

What

  • New AliasFirstBytes accelerator built once per Resolver:
    • [bool; 256] for first-byte hits of length-1 alias keys (e.g. "@")
    • FxHashSet<u16> for the first 2 bytes of longer alias keys
  • load_alias calls first_bytes.may_match(specifier) upfront and returns Ok(None) immediately when nothing can match.
  • ResolverGeneric gains two fields (alias_first_bytes, fallback_first_bytes), populated in new / new_with_file_system / clone_with_options.

Edge cases

  • $-suffixed exact-match keys are indexed by their stripped form, so alias "b$" records first byte b, and specifier "b" still enters the matching loop.
  • Single-byte alias keys are tracked separately so any specifier starting with that byte enters the loop regardless of its second byte.

Before / after (local, warm-cache)

Measured on Apple M4 Pro with a long-lived Resolver (LazyLock-shared so the cache survives across criterion samples). Criterion baseline diff:

Scenario main PR change
single-thread (40 cases) 83.7 µs 64.0 µs −26.3%
multi-thread (40 cases) 92.8 µs 82.2 µs −12.7%
symlinks (10000 resolves) 13.4 ms 9.3 ms −30.4%

The official benches/resolver.rs does clear_cache() per iter (cold cache); the alias-loop short-circuit still applies there but is overshadowed by FS I/O. CodSpeed CI should still surface the per-resolve instruction-count drop.

Test plan

  • cargo test --lib — 125 pass, 6 pre-existing pnp fixture failures unrelated
  • cargo clippy --all-features -- -D warnings (via pre-commit hook)
  • Aligns with existing load_alias semantics — the existing strip_prefix loop is unchanged behind the gate

load_alias() iterates every alias entry and runs strip_prefix per call. For
typical configs (a few dozen aliases plus the test suite that uses 23), this
is significant per-resolve overhead even when no alias key can possibly match
the specifier (e.g. relative requests like ./foo against bare-name aliases).

Precompute, at Resolver construction, a bitmap of which 1-byte and 2-byte
specifier prefixes could match any alias key. The check at the top of
load_alias short-circuits the entire loop when the specifier rules out every
entry by its first 1-2 bytes.

Edge cases handled:
- `$`-suffixed exact-match keys index by the stripped key (so alias "b$"
  records 'b', not 'b$', and specifier "b" still triggers a match attempt).
- Single-byte alias keys like "@" are tracked in a [bool; 256] so any
  specifier starting with '@' enters the loop.
- Longer keys are tracked by their first 2 bytes in an FxHashSet<u16> so an
  absolute path like "/Users/..." doesn't get matched against an unrelated
  "/absolute/path" alias.

ResolverGeneric grows two AliasFirstBytes fields (one for `alias`, one for
`fallback`). They're rebuilt on every construction path: `new`,
`new_with_file_system`, and `clone_with_options`.
Copilot AI review requested due to automatic review settings May 19, 2026 04:20
Copy link
Copy Markdown

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

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: b81ca5d274

ℹ️ 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/lib.rs Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a small precomputed prefix index to speed up alias resolution by quickly rejecting specifiers whose first 1–2 bytes cannot match any configured alias key, reducing per-resolve overhead in common “no match” cases.

Changes:

  • Add an AliasFirstBytes accelerator that indexes 1-byte and 2-byte prefixes of alias keys (including $-exact-match keys by stripped form).
  • Store precomputed prefix indexes on ResolverGeneric for both alias and fallback, built during construction/clone.
  • Gate load_alias with a fast may_match check before iterating all alias entries.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib.rs
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 19, 2026

Merging this PR will improve performance by 6.23%

⚡ 4 improved benchmarks
❌ 1 regressed benchmark
✅ 7 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory resolver[multi-thread] 10.7 MB 11.1 MB -3.84%
Simulation resolver[multi-thread] 62.9 ms 59.7 ms +5.38%
Simulation resolver[single-thread] 55.5 ms 53.3 ms +4.04%
Simulation resolver[resolve from symlinks multi thread] 109.5 ms 93.9 ms +16.65%
Simulation resolver[resolve from symlinks] 180.7 ms 164.3 ms +9.98%

Tip

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


Comparing perf/alias-first-byte-index (f782f1a) with main (45a4472)

Open in CodSpeed

@stormslowly stormslowly requested a review from hardfist May 19, 2026 07:08
The prefix accelerator built a 1+2 byte allowlist over `options.alias` keys
and rejected anything not in the set. An alias key of `""` (a supported
enhanced-resolve config that matches any absolute specifier — `strip_prefix("")`
succeeds and the tail starts with `/`) was silently dropped because the empty
slice fell through both arms of the build match.

Record a `wildcard` flag whenever an empty effective key (including a bare
`"$"` exact-match) is seen and bypass the fast path in that case so the
existing `strip_prefix` loop still runs.

Adds a regression test `empty_alias_key_matches_absolute_specifier` covering
the case Codex's review flagged.
`options.alias` uses `strip_prefix` semantics — `*` is just a one-byte
literal prefix, unlike tsconfig paths / exports field which support glob
wildcards. Lock that contract in so future changes to the alias prefix-byte
accelerator don't silently turn `*` into a wildcard or vice versa.

The test exercises both halves:
- `*/anything` against `("*", Ignore)` → must be ignored
- `./a.js` against the same alias → must NOT be ignored
`"$"` strips down to `""` in the alias loop, making it an exact-match
alias keyed by the empty string — it only matches when the specifier is
also `""`. The prefix-byte accelerator records its effective key as a
wildcard so the fall-through stays open, but the wildcard flag MUST NOT
short-circuit the loop's strict `alias_key != specifier` check into a
blanket ignore.

Guards against a future regression where the wildcard flag is wired to
return Ignored eagerly instead of just bypassing the may_match gate.
- empty_alias_key_matches_absolute_specifier: drop the cfg(not(windows))
  gate so the regression test runs everywhere.
- Remove star_alias_key_to_ignored_is_literal_prefix and
  dollar_alias_key_to_ignored_is_exact_match_only; both passed on the
  current code without exercising new behavior, the empty-key test already
  covers the AliasFirstBytes wildcard branch.
The previous version passed `f.join("a.js")` (an absolute fixture path) as
the specifier. On Windows that becomes a drive-letter path like
`D:\...\a.js`, which fails `strip_package_name`'s `SLASH_START` filter on
`main` — so the empty alias key never matched even before the prefix index,
and the test panicked in CI.

Switch to the synthesised specifier `/foo`. The leading `/` satisfies the
filter on every platform and is enough to exercise the wildcard branch of
`AliasFirstBytes::may_match` (the actual file doesn't need to exist —
`AliasValue::Ignore` short-circuits before resolution).
@stormslowly stormslowly merged commit 3e0aa8f into main May 20, 2026
20 of 21 checks passed
@stormslowly stormslowly deleted the perf/alias-first-byte-index branch May 20, 2026 13:30
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.

3 participants