diff --git a/include/ada/url_aggregator.h b/include/ada/url_aggregator.h index 7c9eea9ad..45aef1698 100644 --- a/include/ada/url_aggregator.h +++ b/include/ada/url_aggregator.h @@ -110,6 +110,17 @@ struct url_aggregator : url_base { */ [[nodiscard]] constexpr size_t get_href_size() const noexcept; + /** + * Estimates the bytes retained by the serialized URL buffer. + * + * The estimate is allocator-independent and reports the buffer capacity, + * not the current serialized URL length or allocator bookkeeping overhead. + * It is suitable for embedders that need to account for retained payload + * storage. + * @return The retained buffer capacity in bytes. + */ + [[nodiscard]] size_t estimated_memory_usage() const noexcept; + /** * Returns the URL's username component. * Does not allocate memory. The returned view becomes invalid if this diff --git a/include/ada/url_search_params.h b/include/ada/url_search_params.h index 5409e3fb0..d01bc3c11 100644 --- a/include/ada/url_search_params.h +++ b/include/ada/url_search_params.h @@ -74,6 +74,16 @@ struct url_search_params { url_search_params& operator=(const url_search_params& u) = default; ~url_search_params() = default; + /** + * Estimates the bytes retained by the parameter list and its strings. + * + * The estimate is allocator-independent and is the saturating sum of the + * parameter vector capacity times its element size and every live key and + * value string capacity. It excludes allocator bookkeeping overhead. + * @return The estimated retained payload capacity in bytes. + */ + [[nodiscard]] size_t estimated_memory_usage() const noexcept; + /** * Returns the number of key-value pairs. * @return The total count of parameters. diff --git a/include/ada_c.h b/include/ada_c.h index 98e4ce20c..2dedf23b0 100644 --- a/include/ada_c.h +++ b/include/ada_c.h @@ -76,6 +76,10 @@ ada_string ada_get_hostname(ada_url result); ada_string ada_get_pathname(ada_url result); ada_string ada_get_search(ada_url result); ada_string ada_get_protocol(ada_url result); +// Returns an allocator-independent estimate of the opaque URL object's +// retained storage, including its owning result block and URL buffer capacity. +// Invalid URL results still retain and report their owning result block. +size_t ada_url_estimated_memory_usage(ada_url result); uint8_t ada_get_host_type(ada_url result); uint8_t ada_get_scheme_type(ada_url result); @@ -136,6 +140,10 @@ ada_url_search_params ada_parse_search_params(const char* input, size_t length); void ada_free_search_params(ada_url_search_params result); size_t ada_search_params_size(ada_url_search_params result); +// Returns an allocator-independent estimate of the opaque parameter object's +// retained storage, including its owning result block, parameter vector, and +// live string capacities. +size_t ada_search_params_estimated_memory_usage(ada_url_search_params result); void ada_search_params_sort(ada_url_search_params result); ada_owned_string ada_search_params_to_string(ada_url_search_params result); diff --git a/src/ada.cpp b/src/ada.cpp index 321dbef16..50ce2bcad 100644 --- a/src/ada.cpp +++ b/src/ada.cpp @@ -8,6 +8,7 @@ #include "parser.cpp" #include "url_components.cpp" #include "url_aggregator.cpp" +#include "url_search_params.cpp" #if ADA_INCLUDE_URL_PATTERN #include "url_pattern.cpp" diff --git a/src/ada_c.cpp b/src/ada_c.cpp index 1c317d9d3..077fcd479 100644 --- a/src/ada_c.cpp +++ b/src/ada_c.cpp @@ -3,10 +3,31 @@ #include "ada/url_aggregator-inl.h" #include "ada/url_search_params-inl.h" +#include + ada::result& get_instance(void* result) noexcept { return *(ada::result*)result; } +namespace { + +template +[[nodiscard]] size_t estimated_c_object_memory_usage( + const ada::result& result) noexcept { + constexpr size_t owning_block_size = sizeof(ada::result); + if (!result) { + return owning_block_size; + } + + const size_t payload_size = result->estimated_memory_usage(); + constexpr size_t max = std::numeric_limits::max(); + return payload_size > max - owning_block_size + ? max + : owning_block_size + payload_size; +} + +} // namespace + extern "C" { typedef void* ada_url; typedef void* ada_url_search_params; @@ -220,6 +241,11 @@ ada_string ada_get_protocol(ada_url result) noexcept { return ada_string_create(out.data(), out.length()); } +size_t ada_url_estimated_memory_usage(ada_url result) noexcept { + ada::result& r = get_instance(result); + return estimated_c_object_memory_usage(r); +} + uint8_t ada_get_host_type(ada_url result) noexcept { ada::result& r = get_instance(result); if (!r) { @@ -498,6 +524,12 @@ size_t ada_search_params_size(ada_url_search_params result) { return r->size(); } +size_t ada_search_params_estimated_memory_usage(ada_url_search_params result) { + ada::result& r = + *(ada::result*)result; + return estimated_c_object_memory_usage(r); +} + void ada_search_params_sort(ada_url_search_params result) { ada::result& r = *(ada::result*)result; diff --git a/src/url_aggregator.cpp b/src/url_aggregator.cpp index ddeadc91a..9c0bd52f2 100644 --- a/src/url_aggregator.cpp +++ b/src/url_aggregator.cpp @@ -32,6 +32,11 @@ ada_really_inline void apply_shifted_non_scheme_offsets( } // namespace namespace ada { + +size_t url_aggregator::estimated_memory_usage() const noexcept { + return buffer.capacity(); +} + template [[nodiscard]] ada_really_inline bool url_aggregator::parse_scheme_with_colon( const std::string_view input_with_colon) { diff --git a/src/url_search_params.cpp b/src/url_search_params.cpp new file mode 100644 index 000000000..31515aa9a --- /dev/null +++ b/src/url_search_params.cpp @@ -0,0 +1,38 @@ +#include "ada/url_search_params.h" + +#include + +namespace { + +[[nodiscard]] constexpr size_t saturating_add(size_t left, + size_t right) noexcept { + constexpr size_t max = std::numeric_limits::max(); + return right > max - left ? max : left + right; +} + +[[nodiscard]] constexpr size_t saturating_multiply(size_t left, + size_t right) noexcept { + constexpr size_t max = std::numeric_limits::max(); + return left != 0 && right > max / left ? max : left * right; +} + +static_assert(saturating_add(std::numeric_limits::max(), 1) == + std::numeric_limits::max()); +static_assert(saturating_multiply(std::numeric_limits::max(), 2) == + std::numeric_limits::max()); + +} // namespace + +namespace ada { + +size_t url_search_params::estimated_memory_usage() const noexcept { + size_t estimate = + saturating_multiply(params.capacity(), sizeof(key_value_pair)); + for (const auto& [key, value] : params) { + estimate = saturating_add(estimate, key.capacity()); + estimate = saturating_add(estimate, value.capacity()); + } + return estimate; +} + +} // namespace ada diff --git a/tests/ada_c.cpp b/tests/ada_c.cpp index 7054c7082..c083cf118 100644 --- a/tests/ada_c.cpp +++ b/tests/ada_c.cpp @@ -81,6 +81,41 @@ TEST(ada_c, getters) { SUCCEED(); } +TEST(ada_c, estimated_memory_usage) { + const std::string short_input = "https://example.com/"; + ada_url url = ada_parse(short_input.data(), short_input.size()); + ASSERT_TRUE(ada_is_valid(url)); + const size_t initial_url_estimate = ada_url_estimated_memory_usage(url); + ASSERT_GT(initial_url_estimate, short_input.size()); + + const std::string large_input = + "https://example.com/" + std::string(8192, 'a'); + ASSERT_TRUE(ada_set_href(url, large_input.data(), large_input.size())); + ASSERT_GE(ada_url_estimated_memory_usage(url), large_input.size()); + ASSERT_GT(ada_url_estimated_memory_usage(url), initial_url_estimate); + ada_free(url); + + const std::string invalid_input = ":"; + url = ada_parse(invalid_input.data(), invalid_input.size()); + ASSERT_FALSE(ada_is_valid(url)); + ASSERT_GT(ada_url_estimated_memory_usage(url), 0); + ada_free(url); + + ada_url_search_params params = ada_parse_search_params("", 0); + const size_t initial_params_estimate = + ada_search_params_estimated_memory_usage(params); + ASSERT_GT(initial_params_estimate, 0); + const std::string key(256, 'k'); + const std::string value(256, 'v'); + for (size_t index = 0; index < 64; ++index) { + ada_search_params_append(params, key.data(), key.size(), value.data(), + value.size()); + } + ASSERT_GT(ada_search_params_estimated_memory_usage(params), + initial_params_estimate); + ada_free_search_params(params); +} + TEST(ada_c, setters) { std::string input = "https://username:password@www.google.com:8080/" diff --git a/tests/basic_tests.cpp b/tests/basic_tests.cpp index 4ba9f1243..fb35328a0 100644 --- a/tests/basic_tests.cpp +++ b/tests/basic_tests.cpp @@ -1073,6 +1073,19 @@ TYPED_TEST(basic_tests, get_href_size_matches_get_href) { } } +TEST(url_aggregator, estimated_memory_usage_tracks_retained_capacity) { + auto url = ada::parse("https://example.com/"); + ASSERT_TRUE(url); + + const size_t initial_estimate = url->estimated_memory_usage(); + ASSERT_GE(initial_estimate, url->get_href_size()); + + const std::string large_url = "https://example.com/" + std::string(8192, 'a'); + ASSERT_TRUE(url->set_href(large_url)); + ASSERT_GE(url->estimated_memory_usage(), url->get_href_size()); + ASSERT_GT(url->estimated_memory_usage(), initial_estimate); +} + TYPED_TEST(basic_tests, get_href_size_after_setters) { auto url = ada::parse("https://user:pass@example.com:8080/path?q=1#frag"); diff --git a/tests/url_search_params.cpp b/tests/url_search_params.cpp index bf2331bff..d3228418d 100644 --- a/tests/url_search_params.cpp +++ b/tests/url_search_params.cpp @@ -17,6 +17,33 @@ TEST(url_search_params, append) { SUCCEED(); } +TEST(url_search_params, estimated_memory_usage_tracks_retained_capacity) { + using key_value_pair = std::pair; + constexpr size_t entry_count = 64; + constexpr size_t string_size = 256; + + ada::url_search_params search_params; + const size_t initial_estimate = search_params.estimated_memory_usage(); + const std::string key(string_size, 'k'); + const std::string value(string_size, 'v'); + + for (size_t index = 0; index < entry_count; ++index) { + search_params.append(key, value); + } + + const size_t populated_estimate = search_params.estimated_memory_usage(); + const size_t populated_lower_bound = + entry_count * sizeof(key_value_pair) + + entry_count * (key.size() + value.size()); + ASSERT_GE(populated_estimate, populated_lower_bound); + ASSERT_GT(populated_estimate, initial_estimate); + + search_params.reset(""); + ASSERT_GE(search_params.estimated_memory_usage(), + entry_count * sizeof(key_value_pair)); + ASSERT_LE(search_params.estimated_memory_usage(), populated_estimate); +} + TEST(url_search_params, to_string) { auto search_params = ada::url_search_params(); search_params.append("key1", "value1");