diff --git a/src/v/cloud_storage/partition_manifest.h b/src/v/cloud_storage/partition_manifest.h index 026037eb2ad3..38821fb5bd9c 100644 --- a/src/v/cloud_storage/partition_manifest.h +++ b/src/v/cloud_storage/partition_manifest.h @@ -129,8 +129,8 @@ class partition_manifest final : public base_manifest { model::offset lo, model::offset lco, model::offset insync, - const std::vector& segments, - const std::vector& replaced) + const fragmented_vector& segments, + const fragmented_vector& replaced) : _ntp(std::move(ntp)) , _rev(rev) , _last_offset(lo) diff --git a/src/v/cluster/archival_metadata_stm.cc b/src/v/cluster/archival_metadata_stm.cc index e26e2c1a6010..8cf98ce95d81 100644 --- a/src/v/cluster/archival_metadata_stm.cc +++ b/src/v/cluster/archival_metadata_stm.cc @@ -26,6 +26,7 @@ #include "serde/serde.h" #include "ssx/future-util.h" #include "storage/record_batch_builder.h" +#include "utils/fragmented_vector.h" #include "utils/named_type.h" #include "vlog.h" @@ -85,9 +86,9 @@ struct archival_metadata_stm::snapshot : public serde:: envelope, serde::compat_version<0>> { /// List of segments - std::vector segments; + fragmented_vector segments; /// List of replaced segments - std::vector replaced; + fragmented_vector replaced; /// Start offset (might be different from the base offset of the first /// segment). Default value means that the snapshot was old and didn't /// have start_offset. In this case we need to set it to compute it from @@ -186,12 +187,10 @@ command_batch_builder archival_metadata_stm::batch_start( return {*this, deadline, as}; } -std::vector +fragmented_vector archival_metadata_stm::segments_from_manifest( const cloud_storage::partition_manifest& manifest) { - std::vector segments; - segments.reserve(manifest.size()); - + fragmented_vector segments; for (auto [key, meta] : manifest) { if (meta.ntp_revision == model::initial_revision_id{}) { meta.ntp_revision = manifest.get_revision_id(); @@ -212,12 +211,11 @@ archival_metadata_stm::segments_from_manifest( return segments; } -std::vector +fragmented_vector archival_metadata_stm::replaced_segments_from_manifest( const cloud_storage::partition_manifest& manifest) { auto replaced = manifest.replaced_segments(); - std::vector segments; - segments.reserve(replaced.size()); + fragmented_vector segments; for (auto meta : replaced) { if (meta.ntp_revision == model::initial_revision_id{}) { meta.ntp_revision = manifest.get_revision_id(); diff --git a/src/v/cluster/archival_metadata_stm.h b/src/v/cluster/archival_metadata_stm.h index 33d59b269679..eba85425b4e1 100644 --- a/src/v/cluster/archival_metadata_stm.h +++ b/src/v/cluster/archival_metadata_stm.h @@ -175,10 +175,10 @@ class archival_metadata_stm final : public persisted_stm { friend segment segment_from_meta(const cloud_storage::segment_meta& meta); - static std::vector + static fragmented_vector segments_from_manifest(const cloud_storage::partition_manifest& manifest); - static std::vector replaced_segments_from_manifest( + static fragmented_vector replaced_segments_from_manifest( const cloud_storage::partition_manifest& manifest); void apply_add_segment(const segment& segment); diff --git a/src/v/utils/fragmented_vector.h b/src/v/utils/fragmented_vector.h index 82e294afb1e1..68c2d59f9cd1 100644 --- a/src/v/utils/fragmented_vector.h +++ b/src/v/utils/fragmented_vector.h @@ -13,8 +13,15 @@ #include "vassert.h" #include +#include +#include +#include #include +namespace test_details { +struct fragmented_vector_accessor; +} + /** * A very very simple fragmented vector that provides random access like a * vector, but does not store its data in contiguous memory. @@ -34,18 +41,41 @@ * all of the space at once we might benefit from the allocator helping with * giving us contiguous memory. */ -template +template class fragmented_vector { + // calculate the maximum number of elements per fragment while + // keeping the element count a power of two + static constexpr size_t calc_elems_per_frag(size_t esize) { + size_t max = max_fragment_size / esize; + assert(max > 0); + // round down to a power of two + size_t pow2 = 1; + while (pow2 * 2 <= max) { + pow2 *= 2; + } + return pow2; + } + + static constexpr size_t elems_per_frag = calc_elems_per_frag(sizeof(T)); + static_assert( - (fragment_size & (fragment_size - 1)) == 0, - "fragment size must be a power of 2"); - static_assert(fragment_size % sizeof(T) == 0); - static constexpr size_t elems_per_frag = fragment_size / sizeof(T); + (elems_per_frag & (elems_per_frag - 1)) == 0, + "element count per fragment must be a power of 2"); static_assert(elems_per_frag >= 1); public: + using this_type = fragmented_vector; using value_type = T; + /** + * The maximum number of bytes per fragment as specified in + * as part of the type. Note that for most types, the true + * number of bytes in a full fragment may as low as half + * of this amount (+1) since the number of elements is restricted + * to a power of two. + */ + static constexpr size_t max_frag_bytes = max_fragment_size; + fragmented_vector() noexcept = default; fragmented_vector& operator=(const fragmented_vector&) noexcept = delete; fragmented_vector(fragmented_vector&&) noexcept = default; @@ -82,9 +112,7 @@ class fragmented_vector { } T& operator[](size_t index) { - vassert(index < _size, "Index out of range {}/{}", index, _size); - auto& frag = _frags.at(index / elems_per_frag); - return frag.at(index % elems_per_frag); + return const_cast(std::as_const(*this)[index]); } const T& back() const { return _frags.back().back(); } @@ -101,53 +129,137 @@ class fragmented_vector { return o._frags == _frags; } - class const_iterator { + /** + * Returns the approximate in-memory size of this vector in bytes. + */ + size_t memory_size() const { + return _frags.size() * (sizeof(_frags[0]) + elems_per_frag * sizeof(T)); + } + + /** + * Returns the (maximum) number of elements in each fragment of this vector. + */ + static size_t elements_per_fragment() { return elems_per_frag; } + + /** + * Assign from a std::vector. + */ + fragmented_vector& operator=(const std::vector& rhs) noexcept { + clear(); + + for (auto& e : rhs) { + push_back(e); + } + + return *this; + } + + /** + * Remove all elements from the vector. + * + * Unlike std::vector, this also releases all the memory from + * the vector (since this vector already the same pointer + * and iterator stability guarantees that std::vector provides + * based on non-reallocation and capacity()). + */ + void clear() { + // do the swap dance to actually clear the memory held by the vector + std::vector>{}.swap(_frags); + _size = 0; + _capacity = 0; + } + + template + class iter { public: using iterator_category = std::random_access_iterator_tag; - using value_type = const T; + using value_type = typename std::conditional_t; using difference_type = std::ptrdiff_t; - using pointer = const T*; - using reference = const T&; + using pointer = value_type*; + using reference = value_type&; + + iter() = default; reference operator*() const { return _vec->operator[](_index); } - const_iterator& operator+=(ssize_t n) { + iter& operator+=(ssize_t n) { _index += n; return *this; } - const_iterator& operator++() { + iter& operator-=(ssize_t n) { + _index -= n; + return *this; + } + + iter& operator++() { ++_index; return *this; } - const_iterator& operator--() { + iter& operator--() { --_index; return *this; } - bool operator==(const const_iterator&) const = default; + iter operator++(int) { + auto tmp = *this; + ++*this; + return tmp; + } + + iter operator--(int) { + auto tmp = *this; + --*this; + return tmp; + } + + iter operator+(difference_type offset) { return iter{*this} += offset; } + iter operator-(difference_type offset) { return iter{*this} -= offset; } - friend ssize_t - operator-(const const_iterator& a, const const_iterator& b) { + bool operator==(const iter&) const = default; + auto operator<=>(const iter&) const = default; + + friend ssize_t operator-(const iter& a, const iter& b) { return a._index - b._index; } private: friend class fragmented_vector; + using vec_type = std::conditional_t; - const_iterator( - const fragmented_vector* vec, size_t index) + iter(vec_type* vec, size_t index) : _index(index) , _vec(vec) {} - size_t _index; - const fragmented_vector* _vec; + size_t _index{}; + vec_type* _vec{}; }; + using const_iterator = iter; + using iterator = iter; + + iterator begin() { return iterator(this, 0); } + iterator end() { return iterator(this, _size); } + const_iterator begin() const { return const_iterator(this, 0); } const_iterator end() const { return const_iterator(this, _size); } + const_iterator cbegin() const { return const_iterator(this, 0); } + const_iterator cend() const { return const_iterator(this, _size); } + + friend test_details::fragmented_vector_accessor; + + friend std::ostream& + operator<<(std::ostream& os, const fragmented_vector& v) { + os << "["; + for (auto& e : v) { + os << e << ","; + } + os << "]"; + return os; + } + private: fragmented_vector(const fragmented_vector&) noexcept = default; @@ -155,3 +267,10 @@ class fragmented_vector { size_t _capacity{0}; std::vector> _frags; }; + +/** + * An alias for a fragmented_vector using a larger fragment size, close + * to the limit of the maximum contiguous allocation size. + */ +template +using large_fragment_vector = fragmented_vector; diff --git a/src/v/utils/tests/fragmented_vector_test.cc b/src/v/utils/tests/fragmented_vector_test.cc index 8da37ec08659..a93c998d9af3 100644 --- a/src/v/utils/tests/fragmented_vector_test.cc +++ b/src/v/utils/tests/fragmented_vector_test.cc @@ -14,8 +14,84 @@ #include +#include +#include +#include #include +using fv_int = fragmented_vector; + +static_assert(std::forward_iterator); +static_assert(std::forward_iterator); + +namespace test_details { + +struct fragmented_vector_accessor { + // perform an internal consistency check of the vector structure + template + static void check_consistency(const fragmented_vector& v) { + BOOST_REQUIRE(v._size <= v._capacity); + BOOST_REQUIRE(v.size() < std::numeric_limits::max() / 2); + BOOST_REQUIRE(v._capacity < std::numeric_limits::max() / 2); + + size_t calc_size = 0, calc_cap = 0; + + for (size_t i = 0; i < v._frags.size(); ++i) { + auto& f = v._frags[i]; + + calc_size += f.size(); + calc_cap += f.capacity(); + + if (i + 1 < v._frags.size()) { + if (f.size() < v.elems_per_frag) { + throw std::runtime_error(fmt::format( + "fragment {} is undersized ({} < {})", + i, + f.size(), + v.elems_per_frag)); + } + } + } + + if (calc_size != v.size()) { + throw std::runtime_error(fmt::format( + "calculated size is wrong ({} != {})", calc_size, v.size())); + } + + if (calc_cap != v._capacity) { + throw std::runtime_error(fmt::format( + "calculated capacity is wrong ({} != {})", + calc_size, + v._capacity)); + } + } +}; +} // namespace test_details + +/** + * Proxy that applies a consistency check before deference + */ +template +struct checker { + using underlying = fragmented_vector; + + underlying* operator->() { + test_details::fragmented_vector_accessor::check_consistency(u); + return &u; + } + + underlying& get() { return *operator->(); } + + auto operator<=>(const checker&) const = default; + + friend std::ostream& operator<<(std::ostream& os, const checker& c) { + os << c.u; + return os; + } + + underlying u; +}; + template static void test_equal(std::vector& truth, fragmented_vector& other) { @@ -115,3 +191,127 @@ BOOST_AUTO_TEST_CASE(fragmented_vector_test) { std::distance(it, truth.end()), std::distance(it2, other.end())); } } + +template +static checker make(std::initializer_list in) { + checker ret; + for (auto& e : in) { + ret->push_back(e); + } + return ret; +} + +BOOST_AUTO_TEST_CASE(fragmented_vector_iterator_types) { + using vtype = fragmented_vector; + using iter = vtype::iterator; + using citer = vtype::const_iterator; + auto v = vtype{}; + + // const and non-const iterators should be different! + static_assert(!std::is_same_v); + + static_assert(std::is_same_v); + static_assert( + std::is_same_v); + static_assert(std::is_same_v< + decltype(std::as_const(v).begin()), + decltype(v)::const_iterator>); +} + +/** + * Get a fragmented vector for elements of size E, with max_fragment_size F. + */ +template +using sized_frag = fragmented_vector, F>; + +BOOST_AUTO_TEST_CASE(fragmented_vector_fragment_sizing) { + BOOST_CHECK_EQUAL((sized_frag<7, 32>::elements_per_fragment()), 4); + BOOST_CHECK_EQUAL((sized_frag<8, 32>::elements_per_fragment()), 4); + BOOST_CHECK_EQUAL((sized_frag<9, 32>::elements_per_fragment()), 2); + BOOST_CHECK_EQUAL((sized_frag<31, 32>::elements_per_fragment()), 1); + BOOST_CHECK_EQUAL((sized_frag<32, 32>::elements_per_fragment()), 1); +} + +BOOST_AUTO_TEST_CASE(fragmented_vector_iterator_arithmetic) { + auto v = make({0, 1, 2, 3}); + + auto b = v->begin(); + + BOOST_CHECK_EQUAL(*(b + 0), 0); + BOOST_CHECK_EQUAL(*(b + 1), 1); + BOOST_CHECK_EQUAL(*(b + 2), 2); + BOOST_CHECK_EQUAL(*(b + 3), 3); + + auto e = v->end(); + + BOOST_CHECK((e - 0) == e); + + BOOST_CHECK_EQUAL(*(e - 1), 3); + BOOST_CHECK_EQUAL(*(e - 2), 2); + BOOST_CHECK_EQUAL(*(e - 3), 1); + BOOST_CHECK_EQUAL(*(e - 4), 0); +} + +BOOST_AUTO_TEST_CASE(fragmented_vector_iterator_comparison) { + auto v = make({0, 1, 2, 3}); + + auto b = v->begin(); + + BOOST_CHECK(b == b); + BOOST_CHECK(b <= b); + BOOST_CHECK(!(b < b)); + BOOST_CHECK(!(b > b)); + BOOST_CHECK(!(b != b)); + + auto b1 = b + 1; + + BOOST_CHECK(b <= b1); + BOOST_CHECK(b < b1); + BOOST_CHECK(b1 >= b); + BOOST_CHECK(b1 > b); + BOOST_CHECK(b1 >= b); +} + +BOOST_AUTO_TEST_CASE(fragmented_vector_sort) { + auto v = make({3, 2, 1}); + auto expected = make({1, 2, 3}); + + std::sort(v->begin(), v->end()); + + BOOST_CHECK_EQUAL(v, expected); +} + +BOOST_AUTO_TEST_CASE(fragmented_vector_vector_clear) { + auto v = make({}); + + BOOST_CHECK_EQUAL(v->size(), 0); + + v->push_back(0); + BOOST_CHECK_EQUAL(v->size(), 1); + + v->push_back(1); + BOOST_CHECK_EQUAL(v->size(), 2); + + v->clear(); + BOOST_CHECK_EQUAL(v->size(), 0); + + v = make({5, 5, 5, 5}); + BOOST_CHECK_EQUAL(v->size(), 4); + + v.u = std::vector{1, 2, 3}; + BOOST_CHECK_EQUAL(v->size(), 3); +} + +BOOST_AUTO_TEST_CASE(fragmented_vector_vector_assign) { + std::vector vin0{1, 2, 3}; + std::vector vin1{4, 5}; + + checker v; + BOOST_CHECK_EQUAL(v, (make({}))); + + v.get() = std::vector{1}; + BOOST_CHECK_EQUAL(v, (make({1}))); + + v.get() = std::vector{2, 3, 4}; + BOOST_CHECK_EQUAL(v, (make({2, 3, 4}))); +}