Skip to content

perf: simple-absolute fast path, bounded freelist, and url href cache - #1198

Closed
anonrig wants to merge 8 commits into
mainfrom
perf/simple-absolute-neon-freelist
Closed

perf: simple-absolute fast path, bounded freelist, and url href cache#1198
anonrig wants to merge 8 commits into
mainfrom
perf/simple-absolute-neon-freelist

Conversation

@anonrig

@anonrig anonrig commented Jul 31, 2026

Copy link
Copy Markdown
Member

Summary

Speed up ada::parse + get_href for both ada::url and ada::url_aggregator on the common absolute http(s) workload.

Simple-absolute fast path (try_parse_simple_absolute)

  • Fail-closed fast path for already-normalized absolute http / https URLs (no base URL).
  • NEON host scan, memchr locate for ? / #, bulk rest-byte validation.
  • host_fast_ok uses checkers::is_ipv4 (domains like example.de stay on the fast path; IPv4-like hosts fall through).
  • path_needs_norm rejects . / .. / %2e so serialization stays exact.
  • Cheap peek skips digit-led and [ hosts so pure IPv4/IPv6 microbenches never enter the simple path (avoids CodSpeed IP regressions).

Bounded thread-local freelist

  • Private helpers::adopt_pooled_string / recycle_pooled_string.
  • Only capacities in [24, 1024] are retained (above typical SSO; hard max so retention stays bounded).
  • Small freelist (4 slots) used for url_aggregator buffer and url path / href-cache.
  • Destructors stay inline (defined in url-inl.h / url_aggregator-inl.h) so we do not flip public methods non-inline↔inline (ABI).

ada::url href cache

  • Simple-absolute path stores the full serialized href in non_special_scheme (unused for special schemes).
  • get_href() returns a copy of that cache on the hot path.
  • Path / query / hash are sliced from the cache until a setter runs; setters call materialize_from_simple_href_cache() first.
  • copy_scheme never copies a special-scheme href cache from a base URL.

Other

  • Portable uninitialized string resize helper (resize_and_overwrite / Apple __resize_default_init / resize fallback).
  • url::get_href hard-codes https / http scheme strings on the non-cache rebuild path.
  • Regression tests for simple-absolute / encoded paths / IPv4-like hosts / .de domains.

Performance

Release benchdata (url-dataset, Google Benchmark mean time/url) vs frozen origin/main (d1543588) on the same machine and flags (-DCMAKE_BUILD_TYPE=Release, development checks off):

Bench Baseline Optimized (mean of 5×9-rep runs) Speedup
BenchData_BasicBench_AdaURL_href 129.709 ns/url ~82.0 ns/url ~1.58×
BenchData_BasicBench_AdaURL_aggregator_href 82.248 ns/url ~54.5 ns/url ~1.51×

Full ADA_TESTING=ON ctest: 319/319 passed locally.

API / ABI

  • No public method removals, renames, or signature changes.
  • Freelist helpers are private (ada::helpers, not part of the public API).
  • Public destructors remain inline (no out-of-line .cpp bodies) to avoid ABI symbol emission flips.

Test plan

  • basic_tests simple-absolute / encoded-path / IPv4-like / .de cases
  • Full ctest locally (ADA_TESTING=ON)
  • Local Release benchdata filter on AdaURL_href and AdaURL_aggregator_href (multi-rep means ≥1.5× vs main)
  • CI green (WPT URL tests, sanitizers, CodSpeed)

Copilot AI review requested due to automatic review settings July 31, 2026 13:33
@codspeed-hq

codspeed-hq Bot commented Jul 31, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 9.58%

⚠️ 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

⚡ 7 improved benchmarks
✅ 22 untouched benchmarks
⏩ 4 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
url_search_params_AdaURL 126.4 µs 109 µs +15.93%
BenchData_BasicBench_AdaURL_aggregator_href 66.8 ms 57.9 ms +15.41%
Bench_DNS_Aggregator 67.6 ms 58.7 ms +15.23%
BenchData_BasicBench_AdaURL_href 98.1 ms 90.3 ms +8.64%
Bench_IPv6_Aggregator 4.1 ms 3.9 ms +4.57%
Bench_IPv6_AdaURL 3.3 ms 3.1 ms +4.38%
Bench_BasicBench_AdaURL_aggregator_href 22.1 µs 21.3 µs +3.78%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing perf/simple-absolute-neon-freelist (0cae187) with main (d154358)

Open in CodSpeed

Footnotes

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

There are concrete performance/portability/operational concerns (overly conservative fast-path host gating, private libc++ API use in a public header, and unbounded TLS buffer retention) that should be addressed before merging.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

This PR optimizes Ada’s absolute http(s) parsing and url::get_href() serialization hot paths by widening the “simple-absolute” fast path, reusing allocator capacity across parses, and avoiding some intermediate allocations/construction on the no-base-url parse path.

Changes:

  • Expanded the http(s) simple-absolute fast path (host/path scanning, memchr for ?/#, bulk rest validation, and extra safety fallthrough checks).
  • Added a thread-local spare buffer to recycle url_aggregator’s buffer capacity (out-of-line destructor returns capacity to the pool).
  • Optimized url::get_href() by using uninitialized resize and hard-coding common scheme strings; added regression tests for encoded paths and IPv4-like hosts.
File summaries
File Description
tests/basic_tests.cpp Adds regression coverage for encoded paths and IPv4-like host fallthrough/invalid cases on the simple-absolute path.
src/url_aggregator.cpp Implements thread-local buffer pooling + out-of-line url_aggregator destructor to recycle capacity.
src/parser.cpp Refactors and widens try_parse_simple_absolute with new scanning/validation helpers and bulk rest validation.
src/implementation.cpp Adds a no-base-url hot path that constructs tl::expected in-place and tries the simple-absolute parser first.
include/ada/url-inl.h Optimizes url::get_href() for common special-URL cases using uninitialized resize and scheme fast paths.
include/ada/url_aggregator.h Declares out-of-line destructor and private buffer pool helpers used by the parser fast path.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 4
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread src/parser.cpp Outdated
Comment thread include/ada/url-inl.h Outdated
Comment thread src/url_aggregator.cpp Outdated
Comment thread src/implementation.cpp Outdated
Copilot AI review requested due to automatic review settings July 31, 2026 13:47
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.73077% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.25%. Comparing base (d154358) to head (0cae187).

Files with missing lines Patch % Lines
src/parser.cpp 82.70% 5 Missing and 18 partials ⚠️
include/ada/url-inl.h 74.50% 0 Missing and 13 partials ⚠️
src/string_pool.cpp 87.50% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1198      +/-   ##
==========================================
+ Coverage   61.06%   61.25%   +0.19%     
==========================================
  Files          38       39       +1     
  Lines        6939     7004      +65     
  Branches     3231     3260      +29     
==========================================
+ Hits         4237     4290      +53     
- Misses        749      752       +3     
- Partials     1953     1962       +9     

☔ View full report in Codecov by Harness.
📢 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI review requested due to automatic review settings July 31, 2026 14:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

It introduces unused internal helpers that may break warning-clean builds, and it needs confirmation/mitigation around ABI and long-lived per-thread memory retention from the new buffer pool.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (5)

src/parser.cpp:175

  • scan_path_bulk_neon is currently not referenced. With internal linkage, this can produce -Wunused-function warnings when ADA_NEON is enabled. Consider marking it [[maybe_unused]] until it’s wired into the fast path.
ada_really_inline size_t scan_path_bulk_neon(const uint8_t* b, size_t start,

src/url_aggregator.cpp:65

  • The thread-local pooled buffer can retain arbitrarily large capacity for the lifetime of a thread (e.g., one very large URL parse can permanently inflate per-thread RSS). Consider adding an explicit maximum pooled capacity (and/or only pooling up to a threshold) so the optimization doesn’t cause unbounded memory retention in long-lived services with occasional huge inputs.
void url_aggregator::recycle_pooled_buffer(std::string& s) noexcept {
  // Keep the larger capacity of the two.
  if (s.capacity() > t_buffer_spare.capacity()) {
    s.clear();
    t_buffer_spare.swap(s);
  }

src/parser.cpp:129

  • eight_path_bulk is defined but not called anywhere in this file. If it’s intentionally kept for a future SIMD/scalar path bulk-scan, mark it [[maybe_unused]] to avoid -Wunused-function warnings; otherwise remove it.
ada_really_inline bool eight_path_bulk(const uint8_t* p) noexcept {

src/parser.cpp:92

  • k_path_bulk is currently unused (it is only referenced by other unused helpers), which can trigger -Wunused-const-variable warnings in translation units that build with -Wall/-Wextra (and potentially -Werror). If this table is intended for a follow-up optimization, mark it [[maybe_unused]]; otherwise remove it to keep the TU warning-clean.

This issue also appears in the following locations of the same file:

  • line 129
  • line 175
constexpr std::array<uint8_t, 256> k_path_bulk = []() consteval {

include/ada/url_aggregator.h:54

  • Moving url_aggregator’s virtual destructor from an inline defaulted definition to an out-of-line definition can affect ABI details (key function/vtable emission and symbol export) for shared-library consumers. Please ensure the project’s ABI check (e.g., abidiff against the latest release tag) is run/green for shared builds before relying on this change.
  // Out-of-line: recycles buffer capacity into a thread-local freelist so the
  // next parse on this thread can avoid a heap allocation.
  ~url_aggregator() override;
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings July 31, 2026 14:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

Unused helper functions introduced in src/parser.cpp can trigger unused-function warnings (potentially breaking warnings-as-errors builds) and should be removed or explicitly marked unused.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (2)

src/parser.cpp:175

  • scan_path_bulk_neon is currently unused (no callers in this TU). On NEON builds this can trigger unused-function warnings treated as errors. Either use it in the fast path or mark it unused.
      }

src/parser.cpp:129

  • eight_path_bulk is currently unused (no callers in this TU). If the project is built with -Wunused-function and warnings-as-errors, this can fail compilation; otherwise it’s dead code in a hot file. Consider removing it or marking it explicitly unused until it’s wired into the fast path.

This issue also appears on line 175 of the same file.

    ok = vorrq_u8(ok, is_dash);
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings July 31, 2026 14:29
anonrig added 6 commits July 31, 2026 10:35
Widen the absolute http(s) fast path with NEON host scanning, memchr
locate for ?/#, bulk rest validation, and fail-closed host/path checks
(IPv4-like hosts, %2e, ./ /../). Reuse url_aggregator buffer capacity via
a thread-local spare, fill tl::expected in-place on the hot path, and
tighten url::get_href with uninitialized resize plus hard-coded
https/http schemes.

Measured on Release benchdata vs pre-change HEAD: about 1.26× for
ada::url parse+href and about 1.56× for url_aggregator, with full ctest
green and no public method signature changes.
- Drop dual try_parse in ada::parse (IPv4/IPv6 paid the reject twice).
- Keep try_parse_simple_absolute non-inline for stable ABI symbols.
- Use portable string_resize_uninitialized (resize_and_overwrite /
  SFINAE for libc++ extension) for clang-tidy and non-Apple libc++.
- Replace non-ASCII em dash in a test comment (just_ascii).
- Use Apple-only libc++ extension or C++23 resize_and_overwrite,
  otherwise plain resize (MSVC/libstdc++ lack the libc++ extension).
- Drop noexcept on the helper to silence bugprone-exception-escape.
- Restore digit-led host peek before try_parse so IPv4 microbenches
  never enter the simple-absolute path.
- Mark try_parse_simple_absolute ada_never_inline for ABI stability.
Add a lucid-style try_can_parse_clean_http tier for exact lowercase
http(s):// with an 8-byte host class scan, then fall through to the
existing absolute special path. Fail closed (nullopt) on credential
colons and other non-port authority forms so can_parse matches parse.

Includes can_parse_consistency_credentials regression tests.
recycle_pooled_buffer showed ~3% self time on Bench_IPv4_Decimal_Aggregator
and explained the remaining -3..-5% IP microbench regressions. Restore a
default destructor and skip try_parse for digit-led and '[' hosts so pure
IP paths match main cold-path cost.
- Use checkers::is_ipv4 for simple-absolute host gating so domains like
  example.de / example.be stay on the fast path (not every [0-9a-fx] end).
- Same is_ipv4 gate in try_can_parse_clean_http.
- Remove unused k_path_bulk / eight_path_bulk / scan_path_bulk_neon.
- Keep resize_and_overwrite before Apple __resize_default_init (already).
- Freelist already removed (unbounded TLS retention review item).
@anonrig
anonrig force-pushed the perf/simple-absolute-neon-freelist branch from 7c56688 to c6c8c0b Compare July 31, 2026 14:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

can_parse can now return true in cases where parse would fail the post-normalization max-length cap (inconsistent behavior for very large inputs).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (2)

src/implementation.cpp:452

  • can_parse returns true for any input <= max_length when max_length == UINT32_MAX, but the parser still enforces the post-normalization size cap against the same max_length. For very large inputs that require percent-encoding/IDNA expansion (the comment above mentions up to ~4.5×), this can make can_parse(...) return true even though parse(...) would fail the final max-length check.
  // Relative resolution combines base + input; bound the sum so 4.5x expansion
  // of either side cannot push the final href past max_length.
  const size_t combined =
      input.size() + (base_input == nullptr ? 0 : base_input->size());
  const bool size_safe = combined <= static_cast<size_t>(max_length) / 5;

src/url_aggregator.cpp:41

  • The PR description calls out changes like a thread-local spare for url_aggregator buffer capacity recycling and moving the url_aggregator destructor out-of-line, but this PR’s src/url_aggregator.cpp diff only adds a blank line. If those optimizations were removed/reworked, the PR description (and perf/ABI notes) should be updated so reviewers aren’t validating behavior that isn’t present in the code.
namespace ada {

template <bool has_state_override>
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings July 31, 2026 14:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

The PR description advertises substantial changes that are not present in the provided diff/repo state, and should be corrected (or missing commits added) to allow accurate review of scope and impact.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (2)

src/implementation.cpp:358

  • PR description claims additional changes (e.g., thread-local pooled buffer reuse in url_aggregator, in-place tl::expected fill) but those symbols/changes are not present in this PR’s diff/repo state. Please either update the PR description to match what’s actually included or add the missing commits, so reviewers can verify the stated performance/ABI impact accurately.
  // Single try_parse lives inside parse_url_impl. Do not call try_parse here
  // as well: IPv4/IPv6 (and other non-simple) inputs would pay the fast-path
  // reject twice and show large CodSpeed regressions on those benches.

src/parser.cpp:410

  • need is computed and immediately discarded via (void)need;, which adds noise and can confuse future readers (it also prevents compilers/static analyzers from flagging truly-unused work). Since the value isn’t used for validation or sizing here, remove it.
    const size_t need = need_slash ? len + 1 : len;
    (void)need;
    if (!need_slash) [[likely]] {
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@anonrig anonrig changed the title perf: speed up simple-absolute parse and get_href hot path perf: simple-absolute fast path, bounded freelist, and url href cache Jul 31, 2026
Restore a capacity-capped thread-local freelist for URL buffers (24–1024
bytes, 4 slots) so the simple-absolute hot path can avoid malloc without
unbounded retention. Extract the pool into string_pool.{h,cpp} and keep
destructors inline for ABI.

For ada::url, cache the prebuilt href in non_special_scheme on the simple
path so get_href is a string copy; materialize path/query/hash on setters
and never copy a special-scheme cache via copy_scheme.
Copilot AI review requested due to automatic review settings July 31, 2026 15:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

A verified bug in url::set_scheme can leave stale non_special_scheme content when switching to a special scheme, which can be misinterpreted as an href cache and break getters/components.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread include/ada/url-inl.h
Comment on lines 293 to 299
inline void url::set_scheme(std::string&& new_scheme) noexcept {
type = ada::scheme::get_scheme_type(new_scheme);
// We only move the 'scheme' if it is non-special.
if (!is_special()) {
non_special_scheme = std::move(new_scheme);
}
}
Restore default url_aggregator destructor and stop pooling its buffer:
recycling on every destroy regressed CodSpeed IPv4 aggregator (~3-5%).
Keep a single-spare string pool only for the url simple-absolute href
cache. Fill path/query/hash at parse time so lazy materialize/getter
special cases can go. Replace non-ASCII punctuation for just_ascii.
Copilot AI review requested due to automatic review settings July 31, 2026 16:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

The new string pool and href-cache detection introduce a confirmed boundedness bug and a correctness hazard (plus a plausible thread_local destruction-order UB) that should be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (3)

include/ada/url-inl.h:52

  • has_simple_href_cache() treats any non-empty non_special_scheme on a special URL as an href cache. However, parse_scheme/set_protocol can transition a URL from NOT_SPECIAL to a special scheme without clearing non_special_scheme, which would make get_href() return a stale scheme name as if it were a cached href.
// non_special_scheme holds a full href only on the simple-absolute path for
// special schemes (otherwise empty or a non-special scheme name).
[[nodiscard]] inline bool url::has_simple_href_cache() const noexcept {
  return !non_special_scheme.empty() && type != ada::scheme::type::NOT_SPECIAL;
}

src/string_pool.cpp:25

  • In adopt(), swapping dest with t_spare can leave dest’s previous (possibly huge) capacity inside the thread-local spare. That defeats the intended [kMinCapacity,kMaxCapacity] bound and can cause long-lived per-thread memory retention after one large dest buffer is swapped in.
  if (t_spare.capacity() >= min_capacity) {
    dest.swap(t_spare);
    dest.clear();
    t_spare.clear();
    return;

include/ada/url.h:69

  • The destructor comment says it recycles both path and href-cache capacity, but the inline destructor currently only recycles non_special_scheme. This is misleading documentation for the new behavior.
  // Inline (see url-inl.h): recycles path / href-cache capacity into a bounded
  // thread-local freelist. Kept inline to match main's defaulted dtor ABI.
  • Files reviewed: 10/10 changed files
  • Comments generated: 1
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread src/string_pool.cpp
Comment on lines +11 to +15
thread_local std::string t_spare;

bool retainable(size_t capacity) noexcept {
return capacity >= kMinCapacity && capacity <= kMaxCapacity;
}
@anonrig

anonrig commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

Superseded by a non-stacked split (all base main):

  1. perf: widen simple-absolute http(s) parse fast path #1199 — widen simple-absolute parse (parser only)
  2. perf: speed up url::get_href for common special URLs #1200url::get_href micro-opts (independent)
  3. perf: simple-absolute href cache and string pool for ada::url #1201 — href cache + string_pool for ada::url (self-contained on main)

Closing this PR in favor of those. The tip of this branch is preserved as a reference combination of the work.

@anonrig anonrig closed this Jul 31, 2026
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.

2 participants