Skip to content
32 changes: 32 additions & 0 deletions include/ada/string_pool.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* @file string_pool.h
* @brief Single thread-local spare for `std::string` heap capacity.
*
* @private Not part of the public Ada API; may change at any time.
*
* One spare buffer per thread. Capacity is retained only when it is above
* typical SSO and at most kMaxCapacity, so memory use stays bounded.
*/
#ifndef ADA_STRING_POOL_H
#define ADA_STRING_POOL_H

#include <cstddef>
#include <string>

namespace ada::string_pool {

/** Do not retain SSO-sized buffers. */
inline constexpr size_t kMinCapacity = 24;

/** Hard cap on retained capacity (bytes). */
inline constexpr size_t kMaxCapacity = 1024;

/** Prefer a recycled spare with capacity >= min_capacity; else reserve. */
void adopt(std::string& dest, size_t min_capacity);

/** Return capacity to the spare when within [kMinCapacity, kMaxCapacity]. */
void recycle(std::string& s) noexcept;

} // namespace ada::string_pool

#endif // ADA_STRING_POOL_H
128 changes: 99 additions & 29 deletions include/ada/url-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#include "ada/url.h"
#include "ada/url_components.h"
#include "ada/helpers.h"
#include "ada/string_pool.h"

#include <charconv>
#include <cstring>
Expand All @@ -17,6 +19,10 @@
#endif // ADA_REGULAR_VISUAL_STUDIO

namespace ada {

// Inline destructor: recycle href-cache capacity (see string_pool).
inline url::~url() { string_pool::recycle(non_special_scheme); }

[[nodiscard]] ada_really_inline bool url::has_credentials() const noexcept {
return !username.empty() || !password.empty();
}
Expand All @@ -39,6 +45,18 @@ inline std::ostream& operator<<(std::ostream& out, const ada::url& u) {
return out << u.to_string();
}

// 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;
}

inline void url::clear_simple_href_cache() noexcept {
if (type != ada::scheme::type::NOT_SPECIAL) {
non_special_scheme.clear();
}
}

[[nodiscard]] size_t url::get_pathname_length() const noexcept {
return path.size();
}
Expand Down Expand Up @@ -176,49 +194,98 @@ inline void url::set_scheme(std::string&& new_scheme) noexcept {
}

constexpr void url::copy_scheme(ada::url&& u) {
non_special_scheme = u.non_special_scheme;
type = u.type;
// non_special_scheme holds the scheme name only for non-special URLs. For
// special URLs it may hold a simple-absolute href cache - never copy that.
if (u.type == ada::scheme::type::NOT_SPECIAL) {
non_special_scheme = std::move(u.non_special_scheme);
} else {
non_special_scheme.clear();
}
}

constexpr void url::copy_scheme(const ada::url& u) {
non_special_scheme = u.non_special_scheme;
type = u.type;
if (u.type == ada::scheme::type::NOT_SPECIAL) {
non_special_scheme = u.non_special_scheme;
} else {
non_special_scheme.clear();
}
}

namespace detail {
// Grow string to n bytes without requiring value-init of new chars when the
// platform provides that API. Not noexcept: allocation may throw bad_alloc.
ada_really_inline void string_resize_uninitialized(std::string& s, size_t n) {
#if defined(__cpp_lib_string_resize_and_overwrite)
s.resize_and_overwrite(
n, [](char*, std::size_t count) noexcept { return count; });
#elif defined(_LIBCPP_VERSION) && defined(__APPLE__)
// Apple libc++ public extension; not available on all libc++ / libstdc++.
s.__resize_default_init(n);
#else
s.resize(n);
#endif
}
} // namespace detail

[[nodiscard]] ada_really_inline std::string url::get_href() const {
if (is_special() && host.has_value() && username.empty() &&
password.empty() && !port.has_value()) [[likely]] {
const std::string_view scheme = ada::scheme::details::is_special_list[type];
// Simple-absolute path prebuilds the full href in non_special_scheme.
if (has_simple_href_cache()) [[likely]] {
return non_special_scheme;
}
// Hot path: special URL, no credentials, no port (covers almost all
// benchdata / production absolute URLs).
if (host.has_value() && username.empty() && password.empty() &&
!port.has_value() && type != ada::scheme::type::NOT_SPECIAL) [[likely]] {
// Hardcode common schemes to avoid table load + size branch.
const char* scheme_ptr;
size_t scheme_len;
if (type == ada::scheme::type::HTTPS) [[likely]] {
scheme_ptr = "https";
scheme_len = 5;
} else if (type == ada::scheme::type::HTTP) {
scheme_ptr = "http";
scheme_len = 4;
} else {
const std::string_view scheme =
ada::scheme::details::is_special_list[type];
scheme_ptr = scheme.data();
scheme_len = scheme.size();
}
const size_t host_size = host->size();
const size_t path_size = path.size();
const size_t query_size = query.has_value() ? query->size() : 0;
const size_t hash_size = hash.has_value() ? hash->size() : 0;
const size_t total = scheme.size() + 3 + host_size + path_size +
(query.has_value() ? query_size + 1 : 0) +
(hash.has_value() ? hash_size + 1 : 0);
std::string output(total, '\0');
char* p = output.data();
std::memcpy(p, scheme.data(), scheme.size());
p += scheme.size();
p[0] = ':';
p[1] = '/';
p[2] = '/';
p += 3;
const bool has_q = query.has_value();
const bool has_h = hash.has_value();
const size_t query_size = has_q ? query->size() : 0;
const size_t hash_size = has_h ? hash->size() : 0;
const size_t total = scheme_len + 3 + host_size + path_size +
(has_q ? query_size + 1 : 0) +
(has_h ? hash_size + 1 : 0);
std::string output;
detail::string_resize_uninitialized(output, total);
char* d = output.data();
std::memcpy(d, scheme_ptr, scheme_len);
d += scheme_len;
d[0] = ':';
d[1] = '/';
d[2] = '/';
d += 3;
// NOLINTNEXTLINE(bugprone-not-null-terminated-result)
std::memcpy(p, host->data(), host_size);
p += host_size;
std::memcpy(p, path.data(), path_size);
p += path_size;
if (query.has_value()) {
*p++ = '?';
std::memcpy(d, host->data(), host_size);
d += host_size;
std::memcpy(d, path.data(), path_size);
d += path_size;
if (has_q) {
*d++ = '?';
// NOLINTNEXTLINE(bugprone-not-null-terminated-result)
std::memcpy(p, query->data(), query_size);
p += query_size;
std::memcpy(d, query->data(), query_size);
d += query_size;
}
if (hash.has_value()) {
*p++ = '#';
if (has_h) {
*d++ = '#';
// NOLINTNEXTLINE(bugprone-not-null-terminated-result)
std::memcpy(p, hash->data(), hash_size);
std::memcpy(d, hash->data(), hash_size);
}
return output;
}
Expand Down Expand Up @@ -273,6 +340,9 @@ constexpr void url::copy_scheme(const ada::url& u) {
}

[[nodiscard]] inline size_t url::get_href_size() const noexcept {
if (has_simple_href_cache()) {
return non_special_scheme.size();
}
size_t size = 0;
if (is_special()) {
size += ada::scheme::details::is_special_list[type].size() + 1;
Expand Down
16 changes: 15 additions & 1 deletion include/ada/url.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ struct url : url_base {
url(url&& u) noexcept = default;
url& operator=(url&& u) noexcept = default;
url& operator=(const url& u) = default;
~url() override = default;
// 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.
~url() override;

// Fields are ordered so that the most frequently accessed components
// tend to occupy earlier cache lines and remain close together in memory.
Expand Down Expand Up @@ -123,6 +125,18 @@ struct url : url_base {
*/
std::string password{};

/**
* @private
* True when non_special_scheme holds a simple-absolute href cache.
*/
[[nodiscard]] inline bool has_simple_href_cache() const noexcept;

/**
* @private
* Drop the simple-absolute href cache after mutation so get_href rebuilds.
*/
inline void clear_simple_href_cache() noexcept;

/**
* Checks if the URL has an empty hostname (host is set but empty string).
* @return `true` if host exists but is empty, `false` otherwise.
Expand Down
1 change: 1 addition & 0 deletions src/ada.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "serializers.cpp"
#include "implementation.cpp"
#include "helpers.cpp"
#include "string_pool.cpp"
#include "url.cpp"
#include "parser.cpp"
#include "url_components.cpp"
Expand Down
3 changes: 3 additions & 0 deletions src/implementation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,9 @@ std::optional<bool> try_can_parse_absolute_fast(
template <class result_type>
ada_warn_unused tl::expected<result_type, errors> parse(
std::string_view input, const result_type* base_url) {
// 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.
result_type u = ada::parser::parse_url_impl<result_type>(input, base_url);
if (!u.is_valid) {
return tl::unexpected(errors::type_error);
Expand Down
Loading
Loading