Skip to content
58 changes: 58 additions & 0 deletions include/ada/string_pool.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @file string_pool.h
* @brief Bounded thread-local freelist for `std::string` heap capacity.
*
* @private Not part of the public Ada API; may change at any time.
*
* The parse hot path allocates short-lived URL buffers on every call. This
* pool lets a thread reuse a small number of heap buffers across
* parse/destroy cycles, avoiding malloc/free on the steady-state path while
* keeping retention bounded.
*
* Policy:
* - Only capacities in [kMinCapacity, kMaxCapacity] are retained.
* Below kMinCapacity is typical SSO; recycling would thrash. Above
* kMaxCapacity would retain oversized rare URLs indefinitely.
* - At most kSlotCount buffers are kept per thread.
* - adopt() prefers a recycled buffer that already has enough capacity;
* otherwise it grows the destination with reserve().
* - recycle() returns capacity to the pool (or replaces a smaller spare).
*/
#ifndef ADA_STRING_POOL_H
#define ADA_STRING_POOL_H

#include <cstddef>
#include <string>

namespace ada::string_pool {

/** Do not retain buffers at or below typical std::string SSO size. */
inline constexpr size_t kMinCapacity = 24;

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

/** Maximum number of spare buffers held per thread. */
inline constexpr size_t kSlotCount = 4;

/**
* Ensure @p dest can hold at least @p min_capacity bytes, preferably by
* swapping in a recycled spare (retaining heap capacity without malloc).
*
* On return, @p dest is empty and has capacity >= min_capacity when a spare
* was available or after reserve().
*/
void adopt(std::string& dest, size_t min_capacity);

/**
* Offer @p s's heap capacity back to the pool.
*
* Clears and stores @p s when its capacity is within [kMinCapacity,
* kMaxCapacity] and useful to the pool; otherwise leaves @p s alone (its
* destructor will free as usual). Noexcept: never allocates.
*/
void recycle(std::string& s) noexcept;

} // namespace ada::string_pool

#endif // ADA_STRING_POOL_H
244 changes: 210 additions & 34 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,16 @@
#endif // ADA_REGULAR_VISUAL_STUDIO

namespace ada {

// Inline destructor: recycles freelist capacity without an out-of-line public
// symbol flip (Agents.md ABI: no non-inline↔inline public method changes).
inline url::~url() {
// Host/query/hash are typically SSO-sized; path and the simple-absolute
// href cache (non_special_scheme) may hold heap capacity.
string_pool::recycle(path);
string_pool::recycle(non_special_scheme);
}

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

// True when non_special_scheme holds a simple-absolute href cache for a
// special scheme (the field is otherwise empty for special URLs).
[[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();
}
}

// Materialize path/query/hash from the href cache, then drop the cache.
// Called by setters before mutating so components stay consistent.
inline void url::materialize_from_simple_href_cache() {
if (!has_simple_href_cache()) {
return;
}
const std::string& href = non_special_scheme;
const size_t auth = (type == ada::scheme::type::HTTPS) ? 8 : 7;
const size_t host_len = host.has_value() ? host->size() : 0;
const size_t path_begin = auth + host_len;
size_t path_end = href.size();
size_t q_pos = std::string::npos;
size_t h_pos = std::string::npos;
if (path_begin < href.size()) {
q_pos = href.find('?', path_begin);
h_pos = href.find('#', path_begin);
path_end = href.size();
if (q_pos != std::string::npos) {
path_end = q_pos;
}
if (h_pos != std::string::npos && h_pos < path_end) {
path_end = h_pos;
}
path.assign(href.data() + path_begin, path_end - path_begin);
} else {
path = "/";
}
if (q_pos != std::string::npos) {
const size_t q_end =
(h_pos != std::string::npos && h_pos > q_pos) ? h_pos : href.size();
query.emplace(href.data() + q_pos + 1, q_end - q_pos - 1);
}
if (h_pos != std::string::npos) {
hash.emplace(href.data() + h_pos + 1, href.size() - h_pos - 1);
}
non_special_scheme.clear();
}

[[nodiscard]] inline std::string_view url::simple_href_path() const noexcept {
const size_t auth = (type == ada::scheme::type::HTTPS) ? 8 : 7;
const size_t host_len = host.has_value() ? host->size() : 0;
const size_t path_begin = auth + host_len;
if (path_begin >= non_special_scheme.size()) {
return "/";
}
const size_t path_end =
non_special_scheme.find_first_of("?#", path_begin);
if (path_end == std::string::npos) {
return std::string_view(non_special_scheme).substr(path_begin);
}
return std::string_view(non_special_scheme)
.substr(path_begin, path_end - path_begin);
}

[[nodiscard]] size_t url::get_pathname_length() const noexcept {
if (has_simple_href_cache() && path.empty()) {
return simple_href_path().size();
}
return path.size();
}

[[nodiscard]] constexpr std::string_view url::get_pathname() const noexcept {
[[nodiscard]] inline std::string_view url::get_pathname() const noexcept {
if (has_simple_href_cache() && path.empty()) {
return simple_href_path();
}
return path;
}

[[nodiscard]] ada_really_inline ada::url_components url::get_components()
const {
// Simple-absolute href cache: offsets match the prebuilt href layout.
if (has_simple_href_cache()) {
url_components out{};
const uint32_t protocol_end =
(type == ada::scheme::type::HTTPS) ? 6u : 5u;
out.protocol_end = protocol_end;
out.username_end = protocol_end + 2;
out.host_start = protocol_end + 2;
const uint32_t host_len =
host.has_value() ? uint32_t(host->size()) : 0u;
// Match the non-credentials branch below: host_end is last host index.
out.host_end = out.host_start + host_len - (host_len > 0 ? 1u : 0u);
out.port = url_components::omitted;
const size_t path_begin = size_t(protocol_end) + 2 + host_len;
out.pathname_start = uint32_t(path_begin);
const size_t q = non_special_scheme.find('?', path_begin);
const size_t h = non_special_scheme.find('#', path_begin);
if (q != std::string::npos && (h == std::string::npos || q < h)) {
out.search_start = uint32_t(q);
}
if (h != std::string::npos) {
out.hash_start = uint32_t(h);
}
return out;
}

url_components out{};

// protocol ends with ':'. for example: "https:"
Expand Down Expand Up @@ -157,12 +268,24 @@ constexpr void url::clear_pathname() { path.clear(); }

constexpr void url::clear_search() { query = std::nullopt; }

[[nodiscard]] constexpr bool url::has_hash() const noexcept {
return hash.has_value();
[[nodiscard]] inline bool url::has_hash() const noexcept {
if (hash.has_value()) {
return true;
}
if (has_simple_href_cache()) {
return non_special_scheme.find('#') != std::string::npos;
}
return false;
}

[[nodiscard]] constexpr bool url::has_search() const noexcept {
return query.has_value();
[[nodiscard]] inline bool url::has_search() const noexcept {
if (query.has_value()) {
return true;
}
if (has_simple_href_cache()) {
return non_special_scheme.find('?') != std::string::npos;
}
return false;
}

constexpr void url::set_protocol_as_file() { type = ada::scheme::type::FILE; }
Expand All @@ -176,49 +299,99 @@ inline void url::set_scheme(std::string&& new_scheme) noexcept {
}
Comment on lines 188 to 194

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 cache: return a copy of the prebuilt href. (Stealing would
// break a second get_href / getters that slice the cache.)
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 +446,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
Loading
Loading