Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Comment on lines 196 to +200
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;
Comment on lines +68 to +70

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