Skip to content
Merged
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
40 changes: 32 additions & 8 deletions include/hpack/dynamic_table.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

#include <memory_resource>

#include <boost/intrusive/set.hpp>
#include <boost/intrusive/unordered_set.hpp>

#include "hpack/basic_types.hpp"
#include "hpack/static_table.hpp"
Expand All @@ -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<bi::set_base_hook<bi::link_mode<bi::normal_link>>>;

// invariant: do not contain nullptrs
std::vector<entry_t*> entries;
bi::multiset<entry_t, bi::constant_time_size<false>, hook_type_option, bi::key_of_value<key_of_entry>> set;
using entry_set_hook = bi::unordered_set_base_hook<bi::link_mode<bi::normal_link>, bi::store_hash<true>,
bi::optimize_multikey<true>>;
// hashed by name
using entry_set_t = bi::unordered_multiset<entry_t, bi::base_hook<entry_set_hook>,
bi::key_of_value<key_of_entry>, bi::equal<equal_by_namevalue>,
bi::hash<hash_by_namevalue>, bi::power_2_buckets<true>>;

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<entry_set_t::bucket_type> buckets;
entry_set_t set;
// in bytes
// invariant: <= _max_size
size_type _current_size = 0;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
69 changes: 69 additions & 0 deletions include/hpack/encoder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include "hpack/strings.hpp"
#include "hpack/integers.hpp"

#include <charconv>

namespace hpack {

struct encoder {
Expand Down Expand Up @@ -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 <bool Huffman = false, Out O>
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);
Expand All @@ -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 <bool Huffman = false, Out O>
O encode_header_and_cache(std::string_view name, std::string_view value, O _out) {
/*
Expand All @@ -76,6 +88,34 @@ struct encoder {
return noexport::unadapt<O>(encode_string<Huffman>(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 <bool Huffman = false, Out O>
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<Huffman>(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 <bool Huffman = false, Out O>
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<Huffman>(name, value, out);
}

template <bool Huffman = false, Out O>
O encode_header_without_indexing(index_type name, std::string_view value, O _out) {
/*
Expand Down Expand Up @@ -251,6 +291,35 @@ struct encoder {
dyntab.update_size(new_size);
return it;
}

// encodes :status pseudoheader for server
// precondition:
template <Out O>
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
85 changes: 51 additions & 34 deletions src/dynamic_table.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
#include <utility>
#include <cstring> // memcpy

namespace bi = boost::intrusive;

namespace hpack {

struct dynamic_table_t::entry_t : bi::set_base_hook<bi::link_mode<bi::normal_link>> {
struct dynamic_table_t::entry_t : entry_set_hook {
const size_type name_end;
const size_type value_end;
const size_t _insert_c;
Expand Down Expand Up @@ -46,6 +44,27 @@ struct dynamic_table_t::entry_t : bi::set_base_hook<bi::link_mode<bi::normal_lin
}
};

std::string_view dynamic_table_t::key_of_entry::operator()(const dynamic_table_t::entry_t& v) const noexcept {
return v.name();
}

static size_t hash_calc(std::string_view bytes) noexcept {
// standard hash is bad sometimes
constexpr uint64_t fnv_offset_basis = 14695981039346656037ULL;
constexpr uint64_t fnv_prime = 1099511628211ULL;
size_t hash = fnv_offset_basis;
for (auto byte : bytes) {
hash ^= static_cast<uint64_t>(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);
Expand All @@ -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),
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 {
Expand All @@ -175,15 +190,17 @@ 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
entries.erase(entries.begin(), entries.begin() + i);
}

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()};
}
Expand Down
Loading