diff --git a/include/hpack/dynamic_table.hpp b/include/hpack/dynamic_table.hpp index 0cb2902..8984fc3 100644 --- a/include/hpack/dynamic_table.hpp +++ b/include/hpack/dynamic_table.hpp @@ -2,7 +2,7 @@ #include -#include +#include #include "hpack/basic_types.hpp" #include "hpack/static_table.hpp" @@ -16,15 +16,33 @@ struct dynamic_table_t { private: struct key_of_entry { - using type = table_entry; - table_entry operator()(const entry_t& v) const noexcept; + using type = std::string_view; + std::string_view operator()(const entry_t& v) const noexcept; + }; + struct equal_by_namevalue { + bool operator()(const key_of_entry::type& l, const key_of_entry::type& r) const noexcept { + return l == r; + } + }; + struct hash_by_namevalue { + size_t operator()(key_of_entry::type) const noexcept; }; - // for forward declaring entry_t - using hook_type_option = bi::base_hook>>; // invariant: do not contain nullptrs std::vector entries; - bi::multiset, hook_type_option, bi::key_of_value> set; + using entry_set_hook = bi::unordered_set_base_hook, bi::store_hash, + bi::optimize_multikey>; + // hashed by name + using entry_set_t = bi::unordered_multiset, + bi::key_of_value, bi::equal, + bi::hash, bi::power_2_buckets>; + + static constexpr inline size_t initial_buckets_count = 4; + + // Note: must be before `set` because of destroy ordering + // invariant: .size is always pow of 2 + std::vector buckets; + entry_set_t set; // in bytes // invariant: <= _max_size size_type _current_size = 0; @@ -49,7 +67,9 @@ struct dynamic_table_t { Insertion Point Dropping Point */ public: - dynamic_table_t() = default; + // 4096 - default size by protocol + dynamic_table_t() : dynamic_table_t(4096) { + } // `user_protocol_max_size` and `max_size()` both initialized to `max_size` explicit dynamic_table_t(size_type max_size, std::pmr::memory_resource* m = std::pmr::get_default_resource()) noexcept; @@ -97,11 +117,15 @@ struct dynamic_table_t { return entries.size() + static_table_t::first_unused_index - 1; } + // searches both static and dynamic table find_result_t find(std::string_view name, std::string_view value) noexcept; + // searches both static and dynamic table + // precondition: name <= current_max_index() find_result_t find(index_type name, std::string_view value) noexcept; - // precondition: first_unused_index <= index <= current_max_index() + // precondition: 0 < index <= current_max_index() // Note: returned value may be invalidated on next .add_entry() + // searches both in static and dynamic tables table_entry get_entry(index_type index) const noexcept; void reset() noexcept; diff --git a/include/hpack/encoder.hpp b/include/hpack/encoder.hpp index 9c7b621..804fc53 100644 --- a/include/hpack/encoder.hpp +++ b/include/hpack/encoder.hpp @@ -4,6 +4,8 @@ #include "hpack/strings.hpp" #include "hpack/integers.hpp" +#include + namespace hpack { struct encoder { @@ -37,6 +39,11 @@ struct encoder { // only name indexed // precondition: header_index present in static or dynamic table + // + // Note: will encode to the cached header, but calling this function again will not result in more efficient + // encoding. + // Instead, it will send requests to cache the header and this will result in evicting from dynamic table. + // Its likely you want to use encode_with_cache instead template O encode_header_and_cache(index_type header_index, std::string_view value, O _out) { assert(header_index <= dyntab.current_max_index() && header_index != 0); @@ -51,6 +58,11 @@ struct encoder { // indexes value for future use // 'out_index' contains index of 'name' + 'value' pair after encode + // + // Note: will encode to the cached header, but calling this function again will not result in more efficient + // encoding. + // Instead, it will send requests to cache the header and this will result in evicting from dynamic table. + // Its likely you want to use encode_with_cache instead template O encode_header_and_cache(std::string_view name, std::string_view value, O _out) { /* @@ -76,6 +88,34 @@ struct encoder { return noexport::unadapt(encode_string(value, out)); } + // encodes header like 'encode_header_and_cache', but uses created cache, so next calls much more efficient + // than first call + // + // Note: does not use static_table. In this case its better to use `encode_header_fully_indexed` + template + O encode_with_cache(std::string_view name, std::string_view value, O out) { + find_result_t r = dyntab.find(name, value); + if (r.value_indexed) [[likely]] { + // its likely, because only first call will be not cached + return encode_header_fully_indexed(r.header_name_index, out); + } + return encode_header_and_cache(name, value, out); + } + + // encodes header like 'encode_header_and_cache', but uses created cache, so next calls much more efficient + // than first call + // + // Note: does not use static_table. In this case its better to use `encode_header_fully_indexed` + template + O encode_with_cache(index_type name, std::string_view value, O out) { + find_result_t r = dyntab.find(name, value); + if (r.value_indexed) [[likely]] { + // its likely, because only first call will be not cached + return encode_header_fully_indexed(r.header_name_index, out); + } + return encode_header_and_cache(name, value, out); + } + template O encode_header_without_indexing(index_type name, std::string_view value, O _out) { /* @@ -251,6 +291,35 @@ struct encoder { dyntab.update_size(new_size); return it; } + + // encodes :status pseudoheader for server + // precondition: + template + O encode_status(int status, O out) { + using enum static_table_t::values; + switch (status) { + case 200: + return encode_header_fully_indexed(status_200, out); + case 204: + return encode_header_fully_indexed(status_204, out); + case 206: + return encode_header_fully_indexed(status_206, out); + case 304: + return encode_header_fully_indexed(status_304, out); + case 400: + return encode_header_fully_indexed(status_400, out); + case 404: + return encode_header_fully_indexed(status_404, out); + case 500: + return encode_header_fully_indexed(status_500, out); + default: + char data[32]; + auto [ptr, ec] = std::to_chars(data, data + 32, status); + assert(ec == std::errc{}); + // its likely, that server will send this status again, so cache it + return encode_with_cache(status_200, std::string_view(+data, ptr), out); + } + } }; } // namespace hpack diff --git a/src/dynamic_table.cpp b/src/dynamic_table.cpp index 02b9f6d..50c3933 100644 --- a/src/dynamic_table.cpp +++ b/src/dynamic_table.cpp @@ -4,11 +4,9 @@ #include #include // memcpy -namespace bi = boost::intrusive; - namespace hpack { -struct dynamic_table_t::entry_t : bi::set_base_hook> { +struct dynamic_table_t::entry_t : entry_set_hook { const size_type name_end; const size_type value_end; const size_t _insert_c; @@ -46,6 +44,27 @@ struct dynamic_table_t::entry_t : bi::set_base_hook(byte); + hash *= fnv_prime; + } + return hash; +} + +size_t dynamic_table_t::hash_by_namevalue::operator()( + dynamic_table_t::key_of_entry::type str) const noexcept { + return hash_calc(str); +} + // precondition: 'e' now in entries index_type dynamic_table_t::indexof(const dynamic_table_t::entry_t& e) const noexcept { return static_table_t::first_unused_index + (_insert_count - e._insert_c); @@ -61,12 +80,10 @@ static size_type entry_size(const dynamic_table_t::entry_t& entry) noexcept { return entry.value_end + 32; } -table_entry dynamic_table_t::key_of_entry::operator()(const dynamic_table_t::entry_t& v) const noexcept { - return {v.name(), v.value()}; -} - dynamic_table_t::dynamic_table_t(size_type max_size, std::pmr::memory_resource* m) noexcept - : _current_size(0), + : buckets(initial_buckets_count), + set({buckets.data(), buckets.size()}), + _current_size(0), _max_size(max_size), _user_protocol_max_size(max_size), _insert_count(0), @@ -109,6 +126,14 @@ index_type dynamic_table_t::add_entry(std::string_view name, std::string_view va evict_until_fits_into(_max_size - new_entry_size); entries.push_back(entry_t::create(name, value, ++_insert_count, _resource)); set.insert(*entries.back()); + if (entries.size() > buckets.size() / 2) { + // https://github.com/boostorg/intrusive/issues/96 + // workaround: + // unordered set bucket copy(move) ctor does nothing, so .resize will be UB + decltype(buckets) new_buckets(buckets.size() * 2); + set.rehash({new_buckets.data(), new_buckets.size()}); + buckets = std::move(new_buckets); + } _current_size += new_entry_size; return static_table_t::first_unused_index; } @@ -131,36 +156,26 @@ void dynamic_table_t::update_size(size_type new_max_size) { find_result_t dynamic_table_t::find(std::string_view name, std::string_view value) noexcept { find_result_t r; - auto it = set.find(table_entry{name, value}); - if (it == set.end()) - return r; - if (name == it->name()) { - r.header_name_index = indexof(*it); - if (value == it->value()) - r.value_indexed = true; + auto i = set.bucket(name); + auto b = set.begin(i); + auto e = set.end(i); + for (; b != e; ++b) { + if (b->name() == name) { + r.header_name_index = indexof(*b); + if (b->value() == value) { + r.value_indexed = true; + return r; + } + } } return r; } find_result_t dynamic_table_t::find(index_type name, std::string_view value) noexcept { assert(name <= current_max_index()); - find_result_t r; - if (name < static_table_t::first_unused_index || name > current_max_index() || name == 0) - return r; - table_entry e = get_entry(name); - if (e.value == value) { - r.header_name_index = name; - r.value_indexed = true; - return r; - } - auto it = set.find(table_entry{e.name, value}); - assert(it != set.end()); - if (e.name == it->name()) { - r.header_name_index = indexof(*it); - if (value == it->value()) - r.value_indexed = true; - } - return r; + if (name == 0) [[unlikely]] + return {}; + return find(get_entry(name).name, value); } void dynamic_table_t::reset() noexcept { @@ -175,7 +190,7 @@ void dynamic_table_t::evict_until_fits_into(size_type bytes) noexcept { size_type i = 0; for (; _current_size > bytes; ++i) { _current_size -= entry_size(*entries[i]); - set.erase(set.s_iterator_to(*entries[i])); + set.erase(set.iterator_to(*entries[i])); entry_t::destroy(entries[i], _resource); } // evicts should be rare operation @@ -183,7 +198,9 @@ void dynamic_table_t::evict_until_fits_into(size_type bytes) noexcept { } table_entry dynamic_table_t::get_entry(index_type index) const noexcept { - assert(index >= static_table_t::first_unused_index && index <= current_max_index()); + assert(index != 0 && index <= current_max_index()); + if (index < static_table_t::first_unused_index) + return static_table_t::get_entry(index); auto& e = *(&entries.back() - (index - static_table_t::first_unused_index)); return table_entry{e->name(), e->value()}; } diff --git a/tests/test_hpack.cpp b/tests/test_hpack.cpp index 68a6f52..73b02ad 100644 --- a/tests/test_hpack.cpp +++ b/tests/test_hpack.cpp @@ -783,7 +783,156 @@ TEST(fuzzing) { } } +TEST(search_dynamic) { + hpack::dynamic_table_t table(4096); + // from static table + auto sr = table.find("abc", "511"); + + error_if(sr.value_indexed); + error_if(sr.header_name_index != 0); // not found + + table.add_entry("abc", "511"); + + sr = table.find("abc", "511"); + + error_if(!sr.value_indexed); + error_if(sr.header_name_index != hpack::static_table_t::first_unused_index); + + sr = table.find("abc", "123"); + + error_if(sr.value_indexed); + error_if(sr.header_name_index != hpack::static_table_t::first_unused_index); + + table.add_entry("abc", "511"); // same entry + + sr = table.find("abc", "511"); + + error_if(!sr.value_indexed); + error_if(sr.header_name_index == 0); + + sr = table.find("abc", "123"); + + error_if(sr.value_indexed); + error_if(sr.header_name_index != hpack::static_table_t::first_unused_index + 1); +} + +TEST(search) { + hpack::dynamic_table_t table(4096); + // from static table + auto sr = table.find(hpack::static_table_t::status_200, "511"); + + error_if(sr.value_indexed); + error_if(sr.header_name_index != 0); // not found + + table.add_entry(":status", "511"); + + sr = table.find(hpack::static_table_t::status_200, "511"); + + error_if(!sr.value_indexed); + error_if(sr.header_name_index != hpack::static_table_t::first_unused_index); + + sr = table.find(":status", "123"); + + error_if(sr.value_indexed); + error_if(sr.header_name_index != hpack::static_table_t::first_unused_index); + + table.add_entry(":status", "511"); // same entry + + sr = table.find(":status", "511"); + + error_if(!sr.value_indexed); + error_if(sr.header_name_index == 0); + + sr = table.find(":status", "123"); + + error_if(sr.value_indexed); + error_if(sr.header_name_index != hpack::static_table_t::first_unused_index + 1); + + table.set_user_protocol_max_size(0); + + sr = table.find(hpack::static_table_t::status_200, "511"); + + error_if(sr.value_indexed); + error_if(sr.header_name_index != 0); // not found + + table.add_entry(":status", "511"); + + sr = table.find(hpack::static_table_t::status_200, "511"); + + error_if(sr.value_indexed); + error_if(sr.header_name_index != 0); + + sr = table.find(":status", "123"); + + error_if(sr.value_indexed); + error_if(sr.header_name_index != 0); + + table.add_entry(":status", "511"); // same entry + + sr = table.find(":status", "511"); + + error_if(sr.value_indexed); + error_if(sr.header_name_index != 0); + + sr = table.find(":status", "123"); + + error_if(sr.value_indexed); + error_if(sr.header_name_index != 0); +} + +TEST(encode_status) { + hpack::encoder enc(4096); + + bytes_t bytes; + auto out = std::back_inserter(bytes); + + auto testone = [&](int status, int expected_len) { + enc.encode_status(status, out); + error_if(bytes.size() != expected_len); + bytes.clear(); + }; + // fully indexed (static table) + testone(200, 1); + testone(204, 1); + testone(206, 1); + testone(304, 1); + testone(400, 1); + testone(404, 1); + testone(500, 1); + error_if(enc.dyntab.current_size() != 0); + testone(101, 5); // cache first time + error_if(enc.dyntab.current_size() == 0); + testone(101, 1); // use cached value + + auto r = enc.dyntab.find(":status", "101"); + error_if(!r.value_indexed || r.header_name_index != hpack::static_table_t::first_unused_index); +} + +TEST(encode_with_cache) { + hpack::encoder enc(4096); + + bytes_t bytes; + auto out = std::back_inserter(bytes); + + auto testone = [&](hpack::static_table_t::values name, std::string_view value, int expected_len) { + enc.encode_with_cache(name, value, out); + error_if(bytes.size() != expected_len); + bytes.clear(); + }; + error_if(enc.dyntab.current_size() != 0); + + testone(hpack::static_table_t::status_204, "111", 5); + + error_if(enc.dyntab.current_size() == 0); + + testone(hpack::static_table_t::status_204, "111", 1); +} + int main() { + test_encode_status(); + test_encode_with_cache(); + test_search_dynamic(); + test_search(); test_fuzzing(); test_invalid_headers(); test_decode_headers_block_dyntab_update();