Skip to content

perf: fast path for normalized absolute special URLs#1

Open
crowlbot wants to merge 7 commits into
mainfrom
perf/fast-path-absolute-url
Open

perf: fast path for normalized absolute special URLs#1
crowlbot wants to merge 7 commits into
mainfrom
perf/fast-path-absolute-url

Conversation

@crowlbot

@crowlbot crowlbot commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a single-pass fast path for parsing already-normalized absolute URLs, modeled on ada-url's try_parse_simple_absolute.

Today every Url::parse call runs the full WHATWG state machine (char-by-char iteration with tab/newline filtering, percent-encode set lookups, IDNA, etc.). The overwhelming majority of real-world URLs are already normalized. This PR adds two sibling byte-scan fast paths that construct the Url directly for them, deferring anything that needs normalization/encoding to the existing parser so observable behavior is unchanged:

  • try_fast_parse — special schemes (http/https/ws/wss/ftp): clean ASCII host (mixed-case allowed, lower-cased like IDNA would), optional canonical port, verbatim path/query/fragment (percent-escapes in the path are kept verbatim; only dot-segments defer). Because it only accepts complete absolute URLs (scheme://…), which the WHATWG algorithm resolves without the base, it also serves Url::join with an absolute argument.
  • try_fast_parse_nonspecial — non-special schemes with an authority (redis://, postgres://, web+demo://, …), which serialize verbatim (opaque host, no lower-casing/IDNA/IPv4, no default port, empty path stays empty).

Correctness

  • In debug builds, every fast-path hit is cross-checked field-by-field against the general parser using the same base URL (debug_check_fast_parse). The test suite runs in debug, so the full WPT urltestdata corpus (thousands of URLs, many with base URLs, mixed-case hosts, and non-special schemes) validates byte-for-byte equivalence, including base-independence. This guard caught several subtle cases during development (xn-- punycode labels, IPv4-looking hosts, default/leading-zero ports), all now deferred.
  • New test_fast_path_parse covers accepted (special, ports, mixed-case, non-special), deferred, and join-vs-parse inputs explicitly (also runs check_invariants).
  • All existing url tests pass (unit + wpt).

Benchmarks

cargo bench -p url --bench parse_url (median of runs; unicode_*/punycode_* don't hit the fast path and are unchanged):

bench before after change
short 126 ns 70 ns −44%
long 149 ns 80 ns −46%
plain 112 ns 71 ns −37%
port 149 ns 58 ns −61%
hyphen 153 ns 73 ns −52%
leading_digit 161 ns 72 ns −55%
fragment 160 ns 84 ns −48%
uppercase_host 194 ns 82 ns −58%
nonspecial_scheme 125 ns 51 ns −59%
encoded_path 214 ns 103 ns −52%
ipv4_host 286 ns 81 ns −72%
join_absolute 141 ns 84 ns −40%

crowlbot added 2 commits July 13, 2026 09:58
Parsing an already-normalized absolute `http`/`https`/`ws`/`wss`/`ftp`
URL previously always went through the full WHATWG state machine. This
adds a single byte-scan fast path (modeled on ada-url's
`try_parse_simple_absolute`) that handles the overwhelmingly common case
of a clean URL — lower-case ASCII host, no credentials/port/IPv4/IDNA,
and path/query/fragment that need no percent-encoding or dot-segment
normalization — building the `Url` directly.

Anything that would require normalization, encoding, punycode, or
C0/tab/newline stripping returns `None` and defers to the general
parser, so behavior is unchanged. In debug builds every fast-path hit is
cross-checked field-by-field against the general parser, so the extensive
WPT `urltestdata` suite validates equivalence.

Benchmarks (`cargo bench -p url --bench parse_url`):

    short          126 -> 70 ns  (-44%)
    long           149 -> 80 ns  (-46%)
    plain          112 -> 71 ns  (-37%)
    hyphen         153 -> 73 ns  (-52%)
    leading_digit  161 -> 72 ns  (-55%)
    fragment       160 -> 84 ns  (-48%)

URLs not eligible for the fast path (port, IDNA, punycode) are unchanged.
Extend the normalized-URL fast path to also handle a port, as long as it
is already in the form the general parser would serialize verbatim: all
digits, in u16 range, no redundant leading zeros, and not the scheme's
default port (which is otherwise stripped). Ports needing normalization
still defer to the general parser.

Bench (parse_url): port 149 -> 58 ns (-61%).
@crowlbot

Copy link
Copy Markdown
Owner Author

Extended the fast path to also cover a canonical host:port authority (all-digits, in-range, no leading zeros, non-default port). Ports that need normalization (default-port stripping, leading zeros) still defer to the general parser, and the debug cross-check + WPT suite continue to validate byte-for-byte equivalence.

New benchmark:

bench before after change
port (https://example.com:8080) 149 ns 58 ns −61%

try_fast_parse only accepts complete absolute special URLs (scheme +
"//"), which the WHATWG algorithm resolves without consulting the base.
So drop the base_url.is_none() gate: base.join(abs_url) now takes the
fast path and yields the same result as parsing abs_url standalone. The
debug cross-check now re-parses through the general parser with the same
base, so the WPT suite validates base-independence across its full
base+input corpus.

Adds a join_absolute benchmark.
Bench (parse_url/join_absolute): 141 -> 84 ns/iter (-40%).
@crowlbot

Copy link
Copy Markdown
Owner Author

Relaxed the base_url.is_none() gate so the fast path also serves Url::join/relative-parse with an absolute argument (e.g. resolving an absolute href against a page's base URL — a very common operation).

try_fast_parse only accepts complete absolute special URLs (scheme://…), which the WHATWG algorithm resolves without consulting the base, so base.join(abs) is guaranteed identical to parsing abs standalone. The debug cross-check now re-parses through the general parser with the same base, so the WPT urltestdata corpus (which is full of base + input combinations) validates base-independence.

New benchmark:

bench before after change
join_absolute 141 ns 84 ns −40%

For a host made only of ASCII [A-Za-z0-9.-], IDNA's sole transformation
is ASCII lower-casing, so the fast path can handle upper-case hosts by
lower-casing the host itself (length-preserving, so all offsets stay
valid). The xn-- and ends_in_a_number checks run on the lower-cased host.
The debug cross-check (vs the general parser) and the WPT suite validate
equivalence, including the many mixed-case host cases in urltestdata.

Adds an uppercase_host benchmark.
Bench (parse_url/uppercase_host): 194 -> 82 ns/iter (-58%).
@crowlbot

Copy link
Copy Markdown
Owner Author

Extended the fast path to accept mixed-case ASCII hosts (e.g. https://GitHub.com from user input). For a host made only of ASCII [A-Za-z0-9.-], IDNA's only effect is ASCII lower-casing, so the fast path lower-cases the host itself (length-preserving, so all byte offsets stay valid); the xn-- and IPv4 checks run on the lower-cased form. The debug cross-check + WPT urltestdata (full of mixed-case host cases) validate equivalence.

New benchmark:

bench before after change
uppercase_host (https://Example.COM/bench) 194 ns 82 ns −58%

Adds try_fast_parse_nonspecial, a sibling of the special-scheme fast
path, for non-special URLs with an authority (redis://, postgres://,
web+demo://, ...). These serialize verbatim: the host is an opaque host
that is neither lower-cased nor IDNA/IPv4-processed, there is no default
port, and an empty path stays empty. It accepts only inputs needing no
transformation (clean ASCII opaque host, no credentials, canonical port,
verbatim path/query/fragment, no dot-segments) and defers everything else
to the general parser. Kept as a separate function so the special path is
untouched. The debug cross-check (vs the general parser, same base) and
the WPT suite validate equivalence.

Adds a nonspecial_scheme benchmark.
Bench (parse_url/nonspecial_scheme): 125 -> 51 ns/iter (-59%).
@crowlbot

Copy link
Copy Markdown
Owner Author

Added a sibling fast path for normalized absolute non-special URLs with an authority (redis://, postgres://, mongodb://, web+demo://, custom app schemes) — a common shape in backend/config parsing. These serialize verbatim (opaque host, no lower-casing, no IDNA/IPv4, no default port, empty path stays empty), so the fast path just validates and copies. Kept as a separate function (try_fast_parse_nonspecial) so the special-scheme path is untouched. Anything needing normalization/encoding defers to the general parser; the debug cross-check + WPT validate equivalence.

New benchmark:

bench before after change
nonspecial_scheme (redis://localhost:6379/0) 125 ns 51 ns −59%

The fast path previously bailed on any '%' in the path, sending common
percent-encoded URLs (e.g. /wiki/C%2B%2B) through the full parser. The
URL parser leaves '%' and its following bytes verbatim in the path; the
only path normalization is dot-segments. So allow '%' as a verbatim path
byte and expand the dot-segment guard from '.'/'..' to the exact set the
parser rewrites, including the %2e/%2E escape forms (is_dot_path_segment).
Applies to both the special and non-special path scans; the debug
cross-check and WPT (rich in %2e/dot-segment cases) validate equivalence.

Adds an encoded_path benchmark.
Bench (parse_url/encoded_path): 214 -> 103 ns/iter (-52%).
@crowlbot

Copy link
Copy Markdown
Owner Author

Extended the fast path to accept percent-escapes in the path (e.g. /wiki/C%2B%2B, /a%20b) — previously any % in the path forced the slow path, despite these being extremely common. The URL parser leaves % and its following bytes verbatim in the path; the only path normalization is dot-segments. So % is now a verbatim path byte and the dot-segment guard is expanded from ./.. to the exact set the parser rewrites (including the %2e/%2E escape forms — is_dot_path_segment). Applies to both special and non-special path scans; the debug cross-check + WPT (rich in %2e/dot-segment cases) validate equivalence.

New benchmark:

bench before after change
encoded_path (.../wiki/C%2B%2B%20programming) 214 ns 103 ns −52%

Special-scheme URLs with an IPv4 host previously always deferred (the
host 'ends in a number'). But an already-canonical dotted-decimal address
(four base-10 octets 0..=255, no leading zeros) serializes verbatim and
maps straight to HostInternal::Ipv4. parse_canonical_ipv4 recognizes
exactly that form and defers everything else (leading zeros, hex/octal,
fewer than four parts) to the general IPv4 parser. The debug cross-check
and WPT (extensive IPv4 coverage) validate equivalence.

Adds an ipv4_host benchmark.
Bench (parse_url/ipv4_host): 286 -> 81 ns/iter (-72%).
@crowlbot

Copy link
Copy Markdown
Owner Author

Extended the fast path to accept canonical dotted-decimal IPv4 hosts (https://192.168.1.1/, http://10.0.0.255/) — common in dev/internal/IoT contexts and previously always deferred (the host "ends in a number", ~286 ns). An already-canonical address (four base-10 octets 0–255, no leading zeros) serializes verbatim and maps directly to HostInternal::Ipv4; parse_canonical_ipv4 recognizes exactly that form and defers everything else (leading zeros, hex/octal, <4 parts) to the general parser. Debug cross-check + WPT (extensive IPv4 coverage) validate equivalence.

New benchmark:

bench before after change
ipv4_host (https://192.168.1.1/status) 286 ns 81 ns −72%

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