perf(alias): short-circuit load_alias with a 1+2 byte prefix index#225
Conversation
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`.
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
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
AliasFirstBytesaccelerator that indexes 1-byte and 2-byte prefixes of alias keys (including$-exact-match keys by stripped form). - Store precomputed prefix indexes on
ResolverGenericfor bothaliasandfallback, built during construction/clone. - Gate
load_aliaswith a fastmay_matchcheck before iterating all alias entries.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…-index # Conflicts: # src/lib.rs
Merging this PR will improve performance by 6.23%
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing |
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).
Why
load_aliasruns astrip_prefixagainst every alias key for every resolve, even when the specifier's first character can already rule out every key —./fooagainst bare-name aliases likereact,@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
AliasFirstBytesaccelerator built once perResolver:[bool; 256]for first-byte hits of length-1 alias keys (e.g."@")FxHashSet<u16>for the first 2 bytes of longer alias keysload_aliascallsfirst_bytes.may_match(specifier)upfront and returnsOk(None)immediately when nothing can match.ResolverGenericgains two fields (alias_first_bytes,fallback_first_bytes), populated innew/new_with_file_system/clone_with_options.Edge cases
$-suffixed exact-match keys are indexed by their stripped form, so alias"b$"records first byteb, and specifier"b"still enters the matching loop.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:The official
benches/resolver.rsdoesclear_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 unrelatedcargo clippy --all-features -- -D warnings(via pre-commit hook)load_aliassemantics — the existingstrip_prefixloop is unchanged behind the gate