diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 0a1936fcff374..686242d0641ff 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -355,7 +355,7 @@ "moduleExtensions": { "//bazel:extensions.bzl%non_module_dependencies": { "general": { - "bzlTransitiveDigest": "tip+8LMsbmCDOxcB10jmkXqLRsMM0PXN6MmYp30sn9Y=", + "bzlTransitiveDigest": "QK0XCW+BTz2nsWYrtviVuUGVhvcjmy31lieoIfTWB2w=", "usagesDigest": "FEiDyZe9eAU6yEqnarZf0XMEUk+prUyYClvq1RU1J98=", "recordedInputs": [ "REPO_MAPPING:,bazel_tools bazel_tools", @@ -528,9 +528,9 @@ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { "build_file": "@@//bazel/thirdparty:seastar.BUILD", - "sha256": "e800bfbfeaf514ad90cb480aa5e317c01c514f99ed97ee0ac2ef3c9c55dc43a5", - "strip_prefix": "seastar-5d474c884fb54f1bbef9fe9bfada0eb48feb47b5", - "url": "https://github.com/redpanda-data/seastar/archive/5d474c884fb54f1bbef9fe9bfada0eb48feb47b5.tar.gz" + "sha256": "7f395262aa42d3ffec092a112971c36e5ce7513c8729be6a1a0c606cc5d4fc12", + "strip_prefix": "seastar-4027b6ed4ad267594fcfaaada877b1c30fc05e0c", + "url": "https://github.com/redpanda-data/seastar/archive/4027b6ed4ad267594fcfaaada877b1c30fc05e0c.tar.gz" } }, "unordered_dense": { diff --git a/bazel/repositories.bzl b/bazel/repositories.bzl index fe210d1a60eba..c9b8d634726dd 100644 --- a/bazel/repositories.bzl +++ b/bazel/repositories.bzl @@ -164,9 +164,9 @@ def data_dependency(): http_archive( name = "seastar", build_file = "//bazel/thirdparty:seastar.BUILD", - sha256 = "e800bfbfeaf514ad90cb480aa5e317c01c514f99ed97ee0ac2ef3c9c55dc43a5", - strip_prefix = "seastar-5d474c884fb54f1bbef9fe9bfada0eb48feb47b5", - url = "https://github.com/redpanda-data/seastar/archive/5d474c884fb54f1bbef9fe9bfada0eb48feb47b5.tar.gz", + sha256 = "7f395262aa42d3ffec092a112971c36e5ce7513c8729be6a1a0c606cc5d4fc12", + strip_prefix = "seastar-4027b6ed4ad267594fcfaaada877b1c30fc05e0c", + url = "https://github.com/redpanda-data/seastar/archive/4027b6ed4ad267594fcfaaada877b1c30fc05e0c.tar.gz", ) http_archive( diff --git a/bazel/thirdparty/seastar.BUILD b/bazel/thirdparty/seastar.BUILD index 0e0d02d427fda..d260ee23bce48 100644 --- a/bazel/thirdparty/seastar.BUILD +++ b/bazel/thirdparty/seastar.BUILD @@ -335,6 +335,9 @@ cc_library( "include/seastar/core/cacheline.hh", "include/seastar/core/checked_ptr.hh", "include/seastar/core/chunked_fifo.hh", + "include/seastar/core/chunked_hash_map.hh", + "include/seastar/core/chunked_vector.hh", + "include/seastar/core/chunked_vector_async.hh", "include/seastar/core/circular_buffer.hh", "include/seastar/core/circular_buffer_fixed_capacity.hh", "include/seastar/core/condition-variable.hh", @@ -660,6 +663,7 @@ cc_library( ], deps = [ ":metrics_cc_proto", + "@abseil-cpp//absl/hash", "@boost//:algorithm", "@boost//:asio", "@boost//:endian", @@ -671,6 +675,7 @@ cc_library( "@lksctp", "@lz4", "@protobuf", + "@unordered_dense", "@yaml-cpp", ] + select({ ":use_io_uring": ["@liburing"], diff --git a/src/v/cloud_topics/level_one/domain/BUILD b/src/v/cloud_topics/level_one/domain/BUILD index 3de63cd237029..5f34ec4bc20c2 100644 --- a/src/v/cloud_topics/level_one/domain/BUILD +++ b/src/v/cloud_topics/level_one/domain/BUILD @@ -141,6 +141,7 @@ redpanda_cc_library( "//src/v/cloud_topics/level_one/metastore/lsm:replicated_db", "//src/v/cloud_topics/level_one/metastore/lsm:stm", "//src/v/container:chunked_hash_map", + "//src/v/container:chunked_vector", "//src/v/model", "//src/v/ssx:checkpoint_mutex", "@abseil-cpp//absl/container:btree", diff --git a/src/v/cloud_topics/level_one/domain/entity_lock_map.h b/src/v/cloud_topics/level_one/domain/entity_lock_map.h index c6873fb856d92..ce321b3a17c27 100644 --- a/src/v/cloud_topics/level_one/domain/entity_lock_map.h +++ b/src/v/cloud_topics/level_one/domain/entity_lock_map.h @@ -12,6 +12,7 @@ #include "absl/container/btree_set.h" #include "base/seastarx.h" #include "container/chunked_hash_map.h" +#include "container/chunked_vector.h" #include "ssx/checkpoint_mutex.h" #include diff --git a/src/v/cluster_link/model/BUILD b/src/v/cluster_link/model/BUILD index b6eef9c3153f2..79559d52e7693 100644 --- a/src/v/cluster_link/model/BUILD +++ b/src/v/cluster_link/model/BUILD @@ -19,6 +19,7 @@ redpanda_cc_library( "//src/v/base", "//src/v/cluster_link:errc", "//src/v/container:chunked_hash_map", + "//src/v/container:chunked_vector", "//src/v/kafka/protocol", "//src/v/model", "//src/v/serde", diff --git a/src/v/cluster_link/model/types.h b/src/v/cluster_link/model/types.h index 2af733022087f..646914b4ae5a8 100644 --- a/src/v/cluster_link/model/types.h +++ b/src/v/cluster_link/model/types.h @@ -15,6 +15,7 @@ #include "base/format_to.h" #include "cluster_link/errc.h" #include "container/chunked_hash_map.h" +#include "container/chunked_vector.h" #include "kafka/protocol/topic_properties.h" #include "model/fundamental.h" #include "model/metadata.h" diff --git a/src/v/compaction/key_offset_map.cc b/src/v/compaction/key_offset_map.cc index c36042a56855f..0dca61274385d 100644 --- a/src/v/compaction/key_offset_map.cc +++ b/src/v/compaction/key_offset_map.cc @@ -10,7 +10,7 @@ */ #include "compaction/key_offset_map.h" -#include "container/chunked_vector_async.h" +#include namespace compaction { @@ -189,7 +189,7 @@ size_t hash_key_offset_map::size() const { return size_; } size_t hash_key_offset_map::capacity() const { return capacity_; } seastar::future<> hash_key_offset_map::initialize(size_t size_bytes) { - co_await chunked_vector_clear_async(entries_); + co_await ss::chunked_vector_clear_async(entries_); while (entries_.memory_size() < size_bytes) { const auto required_size = size_bytes - entries_.memory_size(); const auto required_entries = std::max( @@ -218,7 +218,7 @@ seastar::future<> hash_key_offset_map::initialize(size_t size_bytes) { } seastar::future<> hash_key_offset_map::reset() { - co_await chunked_vector_fill_async(entries_, entry{}); + co_await ss::chunked_vector_fill_async(entries_, entry{}); size_ = 0; max_offset_ = model::offset{}; search_count_ = 0; diff --git a/src/v/container/BUILD b/src/v/container/BUILD index 3c6ac4ac052d4..72a94e55b8bd0 100644 --- a/src/v/container/BUILD +++ b/src/v/container/BUILD @@ -15,12 +15,11 @@ redpanda_cc_library( name = "chunked_vector", hdrs = [ "chunked_vector.h", - "chunked_vector_async.h", ], visibility = ["//visibility:public"], deps = [ "//src/v/base", - "@fmt", + "@seastar", ], ) diff --git a/src/v/container/chunked_hash_map.h b/src/v/container/chunked_hash_map.h index 2ac0eee94697d..0394a08fc1e18 100644 --- a/src/v/container/chunked_hash_map.h +++ b/src/v/container/chunked_hash_map.h @@ -10,234 +10,9 @@ */ #pragma once -#include "absl/hash/hash.h" -#include "container/chunked_vector.h" +#include "base/seastarx.h" -#include -#include -#include +#include -#include - -namespace detail { - -template -concept has_absl_hash = requires(T val) { - { AbslHashValue(std::declval(), val) }; -}; - -/// Wrapper around absl::Hash that disables the extra hash mixing in -/// unordered_dense -template -struct avalanching_absl_hash { - // absl always hash mixes itself so no need to do it again - using is_avalanching = void; - - auto operator()(const T& x) const noexcept -> uint64_t { - return absl::Hash()(x); - } -}; - -} // namespace detail - -/** - * @brief A hash map that uses a chunked vector as the underlying storage. - * - * Use when the hash map is expected to have a large number of elements (e.g.: - * scales with partitions or topics). Performance wise it's equal to the abseil - * hashmaps. - * - * NB: References and iterators are not stable across insertions and deletions. - * - * Both std::hash and abseil's AbslHashValue are supported. We dispatch to the - * latter if available. Given AbslHashValue also supports std::hash we could - * also unconditionally dispatch to it. However, absl's hash mixing seems more - * extensive (and hence less performant) so we only do that when needed. - * - * For more info please see - * https://github.com/martinus/unordered_dense/?tab=readme-ov-file#1-overview - */ -template< - typename Key, - typename Value, - typename Hash = std::conditional_t< - detail::has_absl_hash, - detail::avalanching_absl_hash, - ankerl::unordered_dense::hash>, - typename EqualTo = std::equal_to> -using chunked_hash_map = ankerl::unordered_dense::segmented_map< - Key, - Value, - Hash, - EqualTo, - chunked_vector>, - ankerl::unordered_dense::bucket_type::standard, - chunked_vector>; - -namespace detail { -template -struct chunked_hash_map_from_range_impl { - using value_t = std::ranges::range_value_t>; - using first_t = typename value_t::first_type; - using second_t = typename value_t::second_type; - using ret_t = chunked_hash_map; -}; -} // namespace detail - -// reserves if range size is known -template -typename detail::chunked_hash_map_from_range_impl::ret_t -chunked_hash_map_from_range(Range&& range) { - size_t size = 0; - if constexpr (std::ranges::sized_range) { - size = std::ranges::size(range); - } - return {std::ranges::begin(range), std::ranges::end(range), size}; -}; - -/** - * @brief A set counterpart of chunked_hash_map (uses a chunked vector as the - * underlying storage). - */ -template< - typename Key, - typename Hash = std::conditional_t< - detail::has_absl_hash, - detail::avalanching_absl_hash, - ankerl::unordered_dense::hash>, - typename EqualTo = std::equal_to> -using chunked_hash_set = ankerl::unordered_dense::segmented_set< - Key, - Hash, - EqualTo, - chunked_vector, - ankerl::unordered_dense::bucket_type::standard, - chunked_vector>; - -// Disable fmt's range formatter for chunked_hash_map/set to use our custom -// formatters instead. -template< - typename K, - typename V, - typename H, - typename E, - typename AllocatorOrContainer, - typename Bucket, - typename BucketContainer, - bool IsSegmented, - typename Char> -struct fmt::range_format_kind< - ankerl::unordered_dense::detail::table< - K, - V, - H, - E, - AllocatorOrContainer, - Bucket, - BucketContainer, - IsSegmented>, - Char> - : std::integral_constant {}; - -template< - typename K, - typename V, - typename H, - typename E, - typename AllocatorOrContainer, - typename Bucket, - typename BucketContainer, - bool IsSegmented> -struct fmt::formatter> { - using type = ankerl::unordered_dense::detail::table< - K, - V, - H, - E, - AllocatorOrContainer, - Bucket, - BucketContainer, - IsSegmented>; - - constexpr auto parse(format_parse_context& ctx) const { - return ctx.begin(); - } - - template - typename FormatContext::iterator - format(const type& map, FormatContext& ctx) const { - auto out = ctx.out(); - out = fmt::format_to(out, "["); - auto it = map.begin(); - if (it != map.end()) { - out = fmt::format_to(out, "{{{} -> {}}}", it->first, it->second); - for (++it; it != map.end(); ++it) { - out = fmt::format_to( - out, ", {{{} -> {}}}", it->first, it->second); - } - } - return fmt::format_to(out, "]"); - } -}; - -/// Returns a lower bound on the memory currently being held by `m`. -template< - typename K, - typename V, - typename Hash = std::conditional_t< - detail::has_absl_hash, - detail::avalanching_absl_hash, - ankerl::unordered_dense::hash>, - typename EqualTo = std::equal_to> -size_t -memory_usage_lower_bound(const chunked_hash_map& m) { - return m.bucket_count() - * sizeof(typename chunked_hash_map::bucket_type) - + m.values().capacity() * sizeof(m.values()[0]); -} - -template< - typename K, - typename H, - typename E, - typename AllocatorOrContainer, - typename Bucket, - typename BucketContainer, - bool IsSegmented> -struct fmt::formatter> { - using type = ankerl::unordered_dense::detail::table< - K, - void, - H, - E, - AllocatorOrContainer, - Bucket, - BucketContainer, - IsSegmented>; - - constexpr auto parse(format_parse_context& ctx) const { - return ctx.begin(); - } - - template - typename FormatContext::iterator - format(const type& set, FormatContext& ctx) const { - return fmt::format_to(ctx.out(), "[{}]", fmt::join(set, ",")); - } -}; +using ss::chunked_hash_map; +using ss::chunked_hash_set; diff --git a/src/v/container/chunked_vector.h b/src/v/container/chunked_vector.h index 1008607597ede..175891422ee2f 100644 --- a/src/v/container/chunked_vector.h +++ b/src/v/container/chunked_vector.h @@ -10,587 +10,8 @@ */ #pragma once -#include "base/seastar_fwd.h" -#include "base/vassert.h" +#include "base/seastarx.h" -#include -#include -#include +#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/** - * A chunked vector is a container that provides random access like a - * vector, but does not store its data in contiguous memory. - * - * Instead the allocations are broken up across many different individual - * vectors, but the exposed view is of a single container. - * - * Additionally the allocation strategy is like a "normal" vector until the - * first chunk is full, after which we will then only allocate full chunks. - * - * The iterator implementation works for a few things like std::lower_bound, - * upper_bound, distance, etc... see chunked_vector_test. - */ -template -class chunked_vector { - static constexpr size_t max_allocation_size = 128UL * 1024; - - // calculate the maximum number of elements per fragment while - // keeping the element count a power of two - static consteval size_t calc_elems_per_frag() { - size_t max = max_allocation_size / sizeof(T); - return std::bit_floor(max); - } - - /** - * 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 consteval size_t calc_max_frag_bytes() { - return calc_elems_per_frag() * sizeof(T); - } - -public: - using this_type = chunked_vector; - using backing_type = std::vector>; - using value_type = T; - using reference = std::conditional_t, bool, T&>; - using const_reference - = std::conditional_t, bool, const T&>; - using size_type = size_t; - using allocator_type = backing_type::allocator_type; - using difference_type = backing_type::difference_type; - using pointer = T*; - using const_pointer = const T*; - - chunked_vector() noexcept = default; - explicit chunked_vector(allocator_type alloc) - : _frags(alloc) {} - chunked_vector& operator=(const chunked_vector&) noexcept = delete; - chunked_vector(chunked_vector&& other) noexcept { - *this = std::move(other); - } - - /** - * @brief Create a vector from a begin, end iterator pair. - * - * This has the same semantics as the corresponding std::vector - * constructor. - */ - template - requires std::input_iterator - chunked_vector(Iter begin, Iter end) - : chunked_vector() { - if constexpr (std::random_access_iterator) { - reserve(std::distance(begin, end)); - } - // Improvement: Write a more efficient implementation for - // std::contiguous_iterator - for (auto it = begin; it != end; ++it) { - push_back(*it); - } - } - - /** - * @brief Construct a new vector using an initializer list - * - * In the same manner as the corresponding std::vector method. - */ - chunked_vector(std::initializer_list elems) - : chunked_vector(elems.begin(), elems.end()) {} - - /** - * @brief Construct a new vector from a range - * - * This constructor will copy or move from the range depending on the value - * category of the elements NOT the one of the range. I.e. - * `chunked_vector(std::move(src))` will not necessarily invoke move - * constructor on the elements of `src`. For example, an rvalue `std::span` - * is a non-owning view, and moving from its elements would be a bug. - * Similar for most of the standard library views. - * - * To ensure move semantics from the range elements, use - * `std::views::as_rvalue`. - * - * https://en.cppreference.com/w/cpp/ranges/as_rvalue_view.html - */ - template - requires(std::ranges::range) - // NOLINTNEXTLINE(cppcoreguidelines-missing-std-forward) - chunked_vector(std::from_range_t, Range&& range) - : chunked_vector() { - if constexpr (std::ranges::sized_range) { - reserve(std::ranges::size(range)); - } - std::copy( - std::ranges::begin(range), - std::ranges::end(range), - std::back_inserter(*this)); - } - - chunked_vector& operator=(chunked_vector&& other) noexcept { - if (this != &other) { - this->_size = other._size; - this->_capacity = other._capacity; - this->_frags = std::move(other._frags); - // Move compatibility with std::vector that post move - // the vector is empty(). - other._size = other._capacity = 0; - other.update_generation(); - update_generation(); - } - return *this; - } - ~chunked_vector() noexcept = default; - - chunked_vector copy() const noexcept { return *this; } - - auto get_allocator() const { return _frags.get_allocator(); } - - void swap(chunked_vector& other) noexcept { - std::swap(_size, other._size); - std::swap(_capacity, other._capacity); - std::swap(_frags, other._frags); - other.update_generation(); - update_generation(); - } - - template - void push_back(E&& elem) { - maybe_add_capacity(); - _frags.back().push_back(std::forward(elem)); - ++_size; - update_generation(); - } - - template - T& emplace_back(Args&&... args) { - maybe_add_capacity(); - T& emplaced = _frags.back().emplace_back(std::forward(args)...); - ++_size; - update_generation(); - return emplaced; - } - - void pop_back() { - vassert(_size > 0, "Cannot pop from empty container"); - _frags.back().pop_back(); - --_size; - if (_frags.back().empty()) { - _frags.pop_back(); - _capacity -= std::min(calc_elems_per_frag(), _capacity); - } - update_generation(); - } - - /* - * Replacement for `erase(some_it, end())` but more efficient than n - * `pop_back()s` - */ - void pop_back_n(size_t n) { - vassert( - _size >= n, "Cannot pop more than size() elements in container"); - - if (_size == n) { - clear(); - return; - } - - _size -= n; - - while (n >= _frags.back().size()) { - n -= _frags.back().size(); - _frags.pop_back(); - _capacity -= calc_elems_per_frag(); - } - - for (size_t i = 0; i < n; ++i) { - _frags.back().pop_back(); - } - update_generation(); - } - - const_reference at(size_t index) const { - static constexpr size_t elems_per_frag = calc_elems_per_frag(); - return _frags.at(index / elems_per_frag).at(index % elems_per_frag); - } - - reference at(size_t index) { - static constexpr size_t elems_per_frag = calc_elems_per_frag(); - return _frags.at(index / elems_per_frag).at(index % elems_per_frag); - } - - const_reference operator[](size_t index) const { - static constexpr size_t elems_per_frag = calc_elems_per_frag(); - return _frags[index / elems_per_frag][index % elems_per_frag]; - } - - reference operator[](size_t index) { - static constexpr size_t elems_per_frag = calc_elems_per_frag(); - return _frags[index / elems_per_frag][index % elems_per_frag]; - } - - const_reference front() const { return _frags.front().front(); } - const_reference back() const { return _frags.back().back(); } - reference front() { return _frags.front().front(); } - reference back() { return _frags.back().back(); } - bool empty() const noexcept { return _size == 0; } - size_t size() const noexcept { return _size; } - size_t capacity() const noexcept { return _capacity; } - - /** - * Requests the removal of unused capacity. - * - * If reallocation occurs, all iterators (including the end() iterator) and - * all references to the elements are invalidated. - */ - void shrink_to_fit() { - // Calling shrink to fix then modifying the container could result - // in allocations that overshoot our max_frag_bytes, except when - // we're managing the dynamic size of the first fragment. - if (_frags.size() == 1) { - auto& front = _frags.front(); - front.shrink_to_fit(); - _capacity = front.capacity(); - } - update_generation(); - } - - /** - * Increase the capacity of the vector (the total number of elements that - * the vector can hold without requiring reallocation) to a value that's - * greater or equal to new_cap. - * - * This method has the same guarantees as `std::vector::reserve`, that being - * calling reserve doesn't preserve pointer or iterator stability when - * new_cap is larger than the current capacity. There maybe cases where this - * class invalidates less than `std::vector`, but that should not be relied - * upon. - * - * Performance wise if you know the intended size of this structure is - * desired to call this to prevent costly reallocations when inserting - * elements. - */ - void reserve(size_t new_cap) { - static constexpr size_t elems_per_frag = calc_elems_per_frag(); - if (new_cap > _capacity) { - if (_frags.empty()) { - auto& frag = _frags.emplace_back(); - frag.reserve(std::min(elems_per_frag, new_cap)); - _capacity = frag.capacity(); - } else if (_frags.size() == 1) { - auto& frag = _frags.front(); - frag.reserve(std::min(elems_per_frag, new_cap)); - _capacity = frag.capacity(); - } - // We only reserve the first fragment as all fragments after the - // first are allocated at the maximum size, so we don't save - // anything in terms of reallocs after fully allocating the - // first fragment. In addition, due to cache locality, it's - // better to delay the allocations of those other fragments - // until they're going to be used. - } - update_generation(); - } - - bool operator==(const chunked_vector& o) const noexcept { - return o._frags == _frags; - } - - /** - * Returns the approximate in-memory size of this vector in bytes. - */ - size_t memory_size() const { - return (_frags.size() * sizeof(_frags[0])) + (_capacity * sizeof(T)); - } - - /** - * Returns the (maximum) number of elements in each fragment of this vector. - */ - static constexpr size_t elements_per_fragment() { - return calc_elems_per_frag(); - } - - static constexpr size_t max_frag_bytes() { return calc_max_frag_bytes(); } - - /** - * 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; - update_generation(); - } - - template - class iter { - public: - using iterator_category = std::random_access_iterator_tag; - using iterator_concept = std::random_access_iterator_tag; - using value_type = T; - using difference_type = std::ptrdiff_t; - using pointer = std::conditional_t< - std::is_same_v, - bool, - std::conditional_t>; - using reference = std::conditional_t< - std::is_same_v, - bool, - std::conditional_t>; - - iter() = default; - - /** - * Conversion operator allowing iterator to be converted to - * const_iterator, as required by the general iterator contract. - */ - operator iter() const { // NOLINT(hicpp-explicit-conversions) - check_generation(); - iter ret; - ret._vec = _vec; - ret._index = _index; -#ifndef NDEBUG - ret._my_generation = _my_generation; -#endif - return ret; - } - - reference operator*() const { - check_generation(); - return _vec->operator[](_index); - } - pointer operator->() const { - check_generation(); - return &_vec->operator[](_index); - } - - iter& operator+=(ssize_t n) { - check_generation(); - _index += n; - return *this; - } - - iter& operator-=(ssize_t n) { - check_generation(); - _index -= n; - return *this; - } - - iter& operator++() { - check_generation(); - ++_index; - return *this; - } - - iter& operator--() { - check_generation(); - --_index; - return *this; - } - - iter operator++(int) { - check_generation(); - auto tmp = *this; - ++*this; - return tmp; - } - - iter operator--(int) { - check_generation(); - auto tmp = *this; - --*this; - return tmp; - } - - iter operator+(difference_type offset) const { - check_generation(); - return iter{*this} += offset; - } - - iter operator-(difference_type offset) const { - check_generation(); - return iter{*this} -= offset; - } - - reference operator[](difference_type offset) const { - check_generation(); - return *(*this + offset); - } - - bool operator==(const iter& o) const { - check_generation(); - dassert( - _vec == o._vec, - "iterator compared with different chunked_vector"); - return std::tie(_index, _vec) == std::tie(o._index, o._vec); - }; - auto operator<=>(const iter& o) const { - check_generation(); - dassert( - _vec == o._vec, - "iterator compared with different chunked_vector"); - return std::tie(_index, _vec) <=> std::tie(o._index, o._vec); - }; - - friend ssize_t operator-(const iter& a, const iter& b) { - return a._index - b._index; - } - - friend iter operator+(const difference_type& offset, const iter& i) { - return i + offset; - } - - private: - friend class chunked_vector; - using vec_type = std::conditional_t; - - iter(vec_type* vec, size_t index) - : _index(index) -#ifndef NDEBUG - , _my_generation(vec->_generation) -#endif - , _vec(vec) { - } - - inline void check_generation() const { - dassert( - _vec->_generation == _my_generation, - "Attempting to use an invalidated iterator. The corresponding " - "chunked_vector container has been mutated since this " - "iterator was constructed."); - } - - size_t _index{}; -#ifndef NDEBUG - size_t _my_generation{}; -#endif - vec_type* _vec{}; - }; - - using const_iterator = iter; - using iterator = iter; - - using const_reverse_iterator = std::reverse_iterator; - using reverse_iterator = std::reverse_iterator; - - 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); } - - reverse_iterator rbegin() { return reverse_iterator(end()); } - reverse_iterator rend() { return reverse_iterator(begin()); } - - const_reverse_iterator rbegin() const { - return const_reverse_iterator(end()); - } - const_reverse_iterator rend() const { - return const_reverse_iterator(begin()); - } - - const_reverse_iterator crbegin() const { - return const_reverse_iterator(end()); - } - const_reverse_iterator crend() const { - return const_reverse_iterator(begin()); - } - - /** - * @brief Erases all elements from begin to the end of the vector. - */ - void erase_to_end(const_iterator begin) { pop_back_n(cend() - begin); } - - template - static chunked_vector single(Args&&... args) { - chunked_vector v; - v.emplace_back(std::forward(args)...); - return v; - } - - friend std::ostream& - operator<<(std::ostream& os, const chunked_vector& v) { - fmt::print(os, "{}", v); - return os; - } - -private: - [[gnu::always_inline]] void maybe_add_capacity() { - if (_size == _capacity) [[unlikely]] { - add_capacity(); - } - } - - void add_capacity() { - static constexpr size_t elems_per_frag = calc_elems_per_frag(); - static_assert( - calc_max_frag_bytes() <= max_allocation_size, - "max size of a fragment must be <= 128KiB"); - if (_frags.size() == 1 && _frags.back().capacity() < elems_per_frag) { - auto& frag = _frags.back(); - auto new_cap = std::min(elems_per_frag, frag.capacity() * 2); - frag.reserve(new_cap); - _capacity = new_cap; - return; - } else if (_frags.empty()) { - // At least one element or 32 bytes worth of elements for small - // items. - static constexpr size_t initial_cap = std::max( - 1UL, 32UL / sizeof(T)); - _capacity = initial_cap; - _frags.emplace_back(_frags.get_allocator()).reserve(_capacity); - return; - } - _frags.emplace_back(_frags.get_allocator()).reserve(elems_per_frag); - _capacity += elems_per_frag; - } - - inline void update_generation() { -#ifndef NDEBUG - ++_generation; -#endif - } - -private: - friend class chunked_vector_validator; - chunked_vector(const chunked_vector&) noexcept = default; - - template - friend seastar::future - chunked_vector_fill_async(chunked_vector&, const TT&); - - template - friend seastar::future - chunked_vector_clear_async(chunked_vector&); - - size_t _size{0}; - size_t _capacity{0}; - backing_type _frags; -#ifndef NDEBUG - // Add a generation number that is incremented on every mutation to catch - // invalidated iterator accesses. - size_t _generation{0}; -#endif -}; +using ss::chunked_vector; diff --git a/src/v/container/chunked_vector_async.h b/src/v/container/chunked_vector_async.h deleted file mode 100644 index 84c2db5ca4e85..0000000000000 --- a/src/v/container/chunked_vector_async.h +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2025 Redpanda Data, Inc. - * - * Use of this software is governed by the Business Source License - * included in the file licenses/BSL.md - * - * As of the Change Date specified in that file, in accordance with - * the Business Source License, use of this software will be governed - * by the Apache License, Version 2.0 - */ - -#pragma once - -#include "container/chunked_vector.h" - -#include -#include -#include - -// Async helpers for chunked_vector. -// Not in the main header to avoid pulling in all of seastar if you just need -// chunked_vector. - -/** - * A futurized version of std::fill optimized for fragmented vector. It is - * futurized to allow for large vectors to be filled without incurring reactor - * stalls. It is optimized by circumventing the indexing indirection incurred by - * using the fragmented vector interface directly. - */ -template -inline seastar::future<> -chunked_vector_fill_async(chunked_vector& vec, const T& value) { - auto remaining = vec._size; - for (auto& frag : vec._frags) { - const auto n = std::min(frag.size(), remaining); - if (n == 0) { - break; - } - std::fill_n(frag.begin(), n, value); - remaining -= n; - if (seastar::need_preempt()) { - co_await seastar::yield(); - } - } - vassert( - remaining == 0, - "fragmented vector inconsistency filling remaining {} size {} cap {} " - "nfrags {}", - remaining, - vec._size, - vec._capacity, - vec._frags.size()); -} - -/** - * A futurized version of chunked_vector::clear that allows clearing a large - * vector without incurring a reactor stall. - */ -template -inline seastar::future<> chunked_vector_clear_async(chunked_vector& vec) { - while (!vec._frags.empty()) { - vec._frags.pop_back(); - if (seastar::need_preempt()) { - co_await seastar::yield(); - } - } - vec.clear(); -} diff --git a/src/v/container/tests/BUILD b/src/v/container/tests/BUILD index 3d3166eb9a09b..79f8c7b438853 100644 --- a/src/v/container/tests/BUILD +++ b/src/v/container/tests/BUILD @@ -1,17 +1,4 @@ -load("//bazel:test.bzl", "redpanda_cc_bench", "redpanda_cc_btest", "redpanda_cc_fuzz_test", "redpanda_cc_gtest", "redpanda_test_cc_library") - -redpanda_cc_gtest( - name = "chunked_hash_map_test", - timeout = "short", - srcs = [ - "chunked_hash_map_test.cc", - ], - deps = [ - "//src/v/container:chunked_hash_map", - "//src/v/test_utils:gtest", - "@googletest//:gtest", - ], -) +load("//bazel:test.bzl", "redpanda_cc_bench", "redpanda_cc_fuzz_test", "redpanda_cc_gtest", "redpanda_test_cc_library") redpanda_cc_gtest( name = "radix_tree_test", @@ -42,22 +29,6 @@ redpanda_cc_fuzz_test( ], ) -redpanda_cc_gtest( - name = "chunked_vector_test", - timeout = "short", - srcs = [ - "chunked_vector_test.cc", - ], - deps = [ - "//src/v/container:chunked_vector", - "//src/v/random:generators", - "//src/v/serde", - "//src/v/serde:vector", - "//src/v/test_utils:gtest", - "@googletest//:gtest", - ], -) - redpanda_cc_gtest( name = "contiguous_range_map_test", timeout = "short", @@ -106,20 +77,6 @@ redpanda_cc_gtest( ], ) -redpanda_cc_btest( - name = "chunked_vector_async_test", - timeout = "short", - srcs = [ - "chunked_vector_async_test.cc", - ], - deps = [ - "//src/v/base", - "//src/v/container:chunked_vector", - "//src/v/test_utils:seastar_boost", - "@seastar//:testing", - ], -) - redpanda_test_cc_library( name = "bench_utils", hdrs = [ diff --git a/src/v/container/tests/chunked_hash_map_test.cc b/src/v/container/tests/chunked_hash_map_test.cc deleted file mode 100644 index 0c10b815a45c4..0000000000000 --- a/src/v/container/tests/chunked_hash_map_test.cc +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2024 Redpanda Data, Inc. - * - * Use of this software is governed by the Business Source License - * included in the file licenses/BSL.md - * - * As of the Change Date specified in that file, in accordance with - * the Business Source License, use of this software will be governed - * by the Apache License, Version 2.0 - */ - -#include "container/chunked_hash_map.h" -#include "gtest/gtest.h" - -#include - -struct foo_with_std_hash { - int a; - int b; - auto operator<=>(const foo_with_std_hash&) const = default; -}; - -namespace std { - -template<> -struct hash { - constexpr size_t operator()(const foo_with_std_hash& x) const { - return std::hash()(x.a + x.b); - } -}; - -} // namespace std - -struct foo_with_absl_hash { - int a; - int b; - - auto operator<=>(const foo_with_absl_hash&) const = default; - - template - friend H AbslHashValue(H h, const foo_with_absl_hash& x) { - return H::combine(std::move(h), x.a, x.b); - } -}; - -TEST(chunked_hash_map, basic_compile_std_hash) { - chunked_hash_map map; - map[{1, 2}] = 2; - EXPECT_EQ(map.size(), 1); -} - -TEST(chunked_hash_map, basic_compile_absl_hash) { - static_assert(detail::has_absl_hash); - chunked_hash_map map; - map[{1, 2}] = 2; - EXPECT_EQ(map.size(), 1); -} - -TEST(chunked_hash_map, test_move_assignment) { - chunked_hash_map map; - chunked_hash_map other_map; - other_map = std::move(map); -} - -TEST(chunked_hash_map, from_range_vector) { - std::vector> input{{1, 10}, {2, 20}, {3, 30}}; - auto map = chunked_hash_map_from_range(input); - chunked_hash_map expected{{1, 10}, {2, 20}, {3, 30}}; - EXPECT_EQ(map, expected); -} - -TEST(chunked_hash_map, from_range_list) { - std::list> input{ - {"one", 1}, {"two", 2}, {"three", 3}}; - auto map = chunked_hash_map_from_range(input); - chunked_hash_map expected{ - {"one", 1}, {"two", 2}, {"three", 3}}; - EXPECT_EQ(map, expected); -} - -TEST(chunked_hash_map, from_range_array) { - std::array, 2> input{{{1, "one"}, {2, "two"}}}; - auto map = chunked_hash_map_from_range(input); - chunked_hash_map expected{{1, "one"}, {2, "two"}}; - EXPECT_EQ(map, expected); -} diff --git a/src/v/container/tests/chunked_vector_async_test.cc b/src/v/container/tests/chunked_vector_async_test.cc deleted file mode 100644 index 922eb0eb916f1..0000000000000 --- a/src/v/container/tests/chunked_vector_async_test.cc +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2023 Redpanda Data, Inc. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.md -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the Apache License, Version 2.0 - -#include "base/seastarx.h" -#include "container/chunked_vector.h" -#include "container/chunked_vector_async.h" - -#include - -SEASTAR_THREAD_TEST_CASE(chunked_vector_fill_async_test) { - chunked_vector v; - chunked_vector_fill_async(v, 0).get(); - BOOST_REQUIRE(v.size() == 0); - - // fill with non-zero - for (int i = 1; i <= 10; ++i) { - v.push_back(i); - } - BOOST_REQUIRE(v.size() == 10); - for (const auto& e : v) { - BOOST_REQUIRE(e > 0); - } - - // fill with zero - chunked_vector_fill_async(v, 0).get(); - BOOST_REQUIRE(v.size() == 10); - for (const auto& e : v) { - BOOST_REQUIRE(e == 0); - } -} - -SEASTAR_THREAD_TEST_CASE(chunked_vector_clear_async_test) { - chunked_vector v; - chunked_vector_clear_async(v).get(); - BOOST_REQUIRE(v.size() == 0); - - // one element - v.push_back(0); - BOOST_REQUIRE(v.size() == 1); - chunked_vector_clear_async(v).get(); - BOOST_REQUIRE(v.size() == 0); - - // many fragments - for (size_t i = 0; i < 5; ++i) { - for (size_t j = 0; j < v.elements_per_fragment(); ++j) { - v.push_back(j); - } - } - BOOST_REQUIRE(v.size() == (5 * v.elements_per_fragment())); - - chunked_vector_clear_async(v).get(); - BOOST_REQUIRE(v.size() == 0); -} diff --git a/src/v/container/tests/chunked_vector_test.cc b/src/v/container/tests/chunked_vector_test.cc deleted file mode 100644 index 01f33d41c227f..0000000000000 --- a/src/v/container/tests/chunked_vector_test.cc +++ /dev/null @@ -1,666 +0,0 @@ -/* - * Copyright 2024 Redpanda Data, Inc. - * - * Use of this software is governed by the Business Source License - * included in the file licenses/BSL.md - * - * As of the Change Date specified in that file, in accordance with - * the Business Source License, use of this software will be governed - * by the Apache License, Version 2.0 - */ - -#include "container/chunked_vector.h" -#include "gmock/gmock.h" -#include "gtest/gtest.h" -#include "random/generators.h" -#include "serde/rw/rw.h" -#include "serde/rw/vector.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using testing::AssertionFailure; -using testing::AssertionResult; -using testing::AssertionSuccess; -using vec = chunked_vector; - -static_assert(std::random_access_iterator); -static_assert(std::random_access_iterator); -static_assert(std::ranges::random_access_range); -static_assert(std::ranges::random_access_range); - -class chunked_vector_validator { -public: - // perform an internal consistency check of the vector structure - template - static AssertionResult validate(const chunked_vector& v) { - if (v._size > v._capacity) { - return AssertionFailure() << "size greater than capacity"; - } - if (v._size >= std::numeric_limits::max() / 2) { - return AssertionFailure() << "size too big"; - } - if (v._capacity >= std::numeric_limits::max() / 2) { - return AssertionFailure() << "capacity too big"; - } - 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.elements_per_fragment()) { - return AssertionFailure() << fmt::format( - "fragment {} is undersized ({} < {})", - i, - f.size(), - v.elements_per_fragment()); - } - } - if (f.capacity() > std::decay_t::max_frag_bytes()) { - return AssertionFailure() << fmt::format( - "fragment {} capacity over max_frag_bytes ({})", - i, - calc_cap); - } - } - - if (calc_size != v.size()) { - return AssertionFailure() << fmt::format( - "calculated size is wrong ({} != {})", - calc_size, - v.size()); - } - if (calc_cap != v._capacity) { - return AssertionFailure() << fmt::format( - "calculated capacity is wrong ({} != {})", - calc_cap, - v._capacity); - } - return AssertionSuccess(); - } - - /** - * Lets tests access private members for when they - * need to inspect internals. - */ - template - const MemberType& - access(const chunked_vector& v, MemberType chunked_vector::* member) { - return v.*member; - } -}; - -MATCHER(IsValid, "") { - AssertionResult result = chunked_vector_validator::validate(arg); - *result_listener << result.message(); - return result; -} - -namespace { - -/** - * Proxy that applies a consistency check before deference - */ -template -struct checker { - using underlying = chunked_vector; - - static checker make(std::vector in) { - checker ret; - for (auto& e : in) { - ret->push_back(e); - } - return ret; - } - - underlying* operator->() { - auto is_valid = chunked_vector_validator::validate(u); - if (!is_valid) { - throw std::runtime_error(is_valid.message()); - } - 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; -}; - -using testing::ElementsAre; -using testing::ElementsAreArray; -using testing::Gt; -using testing::IsEmpty; -using testing::Lt; -using testing::Ne; -using testing::Not; - -template -AssertionResult is_eq(chunked_vector& impl, std::vector& shadow) { - if (!std::equal(impl.begin(), impl.end(), shadow.begin(), shadow.end())) { - return testing::AssertionFailure() - << "iterators not equal: " << testing::PrintToString(impl) - << " vs " << testing::PrintToString(shadow); - } - if (impl.empty() != shadow.empty()) { - return testing::AssertionFailure() - << "empty not equal: " << testing::PrintToString(impl) << " vs " - << testing::PrintToString(shadow); - } - if (impl.size() != shadow.size()) { - return testing::AssertionFailure() - << "size not equal: " << testing::PrintToString(impl) << " vs " - << testing::PrintToString(shadow); - } - return chunked_vector_validator::validate(impl); -} - -AssertionResult -push(chunked_vector& impl, std::vector& shadow, int count) { - for (int i = 0; i < count; ++i) { - shadow.push_back(i); - impl.push_back(i); - auto r = is_eq(impl, shadow); - if (!r) { - return r; - } - impl = serde::from_iobuf>( - serde::to_iobuf(std::move(impl))); - r = is_eq(impl, shadow); - if (!r) { - return r; - } - } - return testing::AssertionSuccess(); -} - -AssertionResult -pop(chunked_vector& impl, std::vector& shadow, int count) { - for (int i = 0; i < count; ++i) { - shadow.pop_back(); - impl.pop_back(); - auto r = is_eq(impl, shadow); - if (!r) { - return r; - } - impl = serde::from_iobuf>( - serde::to_iobuf(std::move(impl))); - r = is_eq(impl, shadow); - if (!r) { - return r; - } - } - return testing::AssertionSuccess(); -} - -TEST(Vector, PushPop) { - std::vector shadow; - chunked_vector impl; - EXPECT_TRUE(impl.empty()); - ASSERT_TRUE(push(impl, shadow, 2500)); - EXPECT_TRUE(pop(impl, shadow, 1234)); - ASSERT_TRUE(push(impl, shadow, 123)); - EXPECT_TRUE(pop(impl, shadow, 1389)); - EXPECT_THAT(impl, ElementsAreArray(shadow)); - EXPECT_EQ(impl.empty(), shadow.empty()); - EXPECT_EQ(impl.size(), shadow.size()); -} - -TEST(Vector, Iterator) { - std::vector shadow; - chunked_vector impl; - - EXPECT_TRUE(push(impl, shadow, 2000)); - for (int i = 0; i < 6000; i++) { - auto val = random_generators::get_int(0, 4000); - - auto it = std::lower_bound(shadow.begin(), shadow.end(), val); - auto it2 = std::lower_bound(impl.begin(), impl.end(), val); - EXPECT_EQ(it == shadow.end(), it2 == impl.end()); - EXPECT_EQ( - std::distance(shadow.begin(), it), std::distance(impl.begin(), it2)); - EXPECT_EQ( - std::distance(it, shadow.end()), std::distance(it2, impl.end())); - - it = std::upper_bound(shadow.begin(), shadow.end(), val); - it2 = std::upper_bound(impl.begin(), impl.end(), val); - EXPECT_EQ(it == shadow.end(), it2 == impl.end()); - EXPECT_EQ( - std::distance(shadow.begin(), it), std::distance(impl.begin(), it2)); - EXPECT_EQ( - std::distance(it, shadow.end()), std::distance(it2, impl.end())); - - it = std::find(shadow.begin(), shadow.end(), val); - it2 = std::find(impl.begin(), impl.end(), val); - EXPECT_EQ(it == shadow.end(), it2 == impl.end()); - EXPECT_EQ( - std::distance(shadow.begin(), it), std::distance(impl.begin(), it2)); - EXPECT_EQ( - std::distance(it, shadow.end()), std::distance(it2, impl.end())); - } -} - -TEST(Vector, IteratorTypes) { - using vtype = chunked_vector; - using iter = vtype::iterator; - using citer = vtype::const_iterator; - using riter = vtype::reverse_iterator; - using criter = vtype::const_reverse_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>); - - // const and non-const reverse 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).rbegin()), - decltype(v)::const_reverse_iterator>); -} - -struct foo { - int a; - friend std::ostream& operator<<(std::ostream& os, const foo& f) { - return os << f.a; - } - bool operator==(const foo&) const = default; -}; - -TEST(Vector, IteratorAccess) { - using vtype = chunked_vector; - auto vec = vtype{}; - vec.push_back(foo{2}); - - EXPECT_EQ(*vec.begin(), foo{2}); - EXPECT_EQ((*vec.begin()).a, 2); - EXPECT_EQ(vec.begin()->a, 2); -} - -TEST(Vector, ReverseIteratorAccess) { - using vtype = chunked_vector; - auto vec = vtype{}; - vec.push_back(foo{2}); - vec.push_back(foo{3}); - - EXPECT_EQ(*vec.rbegin(), foo{3}); - EXPECT_EQ((*vec.rbegin()).a, 3); - EXPECT_EQ(vec.rbegin()->a, 3); -} - -TEST(Vector, IteratorArithmetic) { - auto v = checker::make({0, 1, 2, 3}); - - auto b = v->begin(); - - EXPECT_EQ(*(b + 0), 0); - EXPECT_EQ(*(b + 1), 1); - EXPECT_EQ(*(b + 2), 2); - EXPECT_EQ(*(b + 3), 3); - - auto e = v->end(); - - EXPECT_EQ((e - 0), e); - - EXPECT_EQ(*(e - 1), 3); - EXPECT_EQ(*(e - 2), 2); - EXPECT_EQ(*(e - 3), 1); - EXPECT_EQ(*(e - 4), 0); -} - -TEST(Vector, ReverseIteratorArithmetic) { - auto v = checker::make({0, 1, 2, 3}); - - auto b = v->rbegin(); - - EXPECT_EQ(*(b + 0), 3); - EXPECT_EQ(*(b + 1), 2); - EXPECT_EQ(*(b + 2), 1); - EXPECT_EQ(*(b + 3), 0); - - auto e = v->rend(); - - EXPECT_EQ((e - 0), e); - - EXPECT_EQ(*(e - 1), 0); - EXPECT_EQ(*(e - 2), 1); - EXPECT_EQ(*(e - 3), 2); - EXPECT_EQ(*(e - 4), 3); -} - -TEST(Vector, IteratorCmp) { - auto v = checker::make({0, 1, 2, 3}); - - auto b = v->begin(); - - EXPECT_EQ(b, b); - EXPECT_LE(b, b); - EXPECT_THAT(b, Not(Lt(b))); - EXPECT_THAT(b, Not(Gt(b))); - EXPECT_THAT(b, Not(Ne(b))); - - auto b1 = b + 1; - - EXPECT_LE(b, b1); - EXPECT_LT(b, b1); - EXPECT_GE(b1, b); - EXPECT_GT(b1, b); - EXPECT_NE(b1, b); -} - -TEST(Vector, EmptyAfterMove) { - // Checks that post move, the source vector is empty(). - // This is inline with std::vector guarantees. - chunked_vector v1; - v1.push_back(1); - EXPECT_FALSE(v1.empty()); - - auto v2 = std::move(v1); - // NOLINTNEXTLINE(bugprone-use-after-move) - EXPECT_TRUE(v1.empty()); - EXPECT_EQ(v1.begin(), v1.end()); - - auto v3(std::move(v2)); - // NOLINTNEXTLINE(bugprone-use-after-move) - EXPECT_TRUE(v2.empty()); - EXPECT_EQ(v2.begin(), v2.end()); -} - -TEST(Vector, Sort) { - vec v; - v.push_back(3); - v.push_back(2); - v.push_back(1); - - std::sort(v.begin(), v.end()); - - EXPECT_THAT(v, ElementsAre(1, 2, 3)); -} - -TEST(Vector, Heap) { - vec v{1, 2, 3}; - std::ranges::make_heap(v); - EXPECT_TRUE(std::ranges::is_heap(v)); -} - -TEST(Vector, Clear) { - auto v = checker::make({}); - EXPECT_EQ(v->size(), 0); - v->push_back(3); - EXPECT_EQ(v->size(), 1); - v->push_back(2); - EXPECT_EQ(v->size(), 2); - v->push_back(1); - EXPECT_EQ(v->size(), 3); - v->clear(); - EXPECT_EQ(v.get(), vec{}); - EXPECT_EQ(v->size(), 0); - EXPECT_THAT(v.get(), IsEmpty()); - v = checker::make({5, 5, 5, 5}); - EXPECT_EQ(v->size(), 4); -} - -TEST(Vector, PopBackN) { - const int elements = 6; - for (int i = 0; i <= elements; ++i) { - std::vector start_values(elements); - std::iota(start_values.begin(), start_values.end(), 0); - auto vec = checker::make(start_values); - - vec->pop_back_n(i); - - std::vector expected_values(elements - i); - std::iota(expected_values.begin(), expected_values.end(), 0); - EXPECT_EQ(vec->size(), expected_values.size()); - EXPECT_THAT(vec.get(), ElementsAreArray(expected_values)); - - if (elements - i > 0) { - EXPECT_EQ(vec->back(), expected_values.back()); - } - } -} - -TEST(Vector, EraseToEnd) { - chunked_vector v; - - EXPECT_THAT(v, IsValid()); - - v.erase_to_end(v.begin()); - EXPECT_THAT(v, IsValid()); - EXPECT_EQ(v.size(), 0); - - v.erase_to_end(v.end()); - EXPECT_THAT(v, IsValid()); - EXPECT_EQ(v.size(), 0); - - v.push_back(0); - v.erase_to_end(v.end()); - EXPECT_THAT(v, ElementsAre(0)); - v.erase_to_end(v.begin()); - EXPECT_THAT(v, ElementsAre()); - - v.push_back(0); - v.push_back(1); - v.push_back(2); - v.push_back(3); - EXPECT_THAT(v, ElementsAre(0, 1, 2, 3)); - v.erase_to_end(v.begin() + 1); - EXPECT_THAT(v, ElementsAre(0)); -} - -TEST(Vector, FromIterRangeConstructor) { - std::vector vals{1, 2, 3}; - - chunked_vector fv(vals.begin(), vals.end()); - - EXPECT_THAT(fv, IsValid()); - EXPECT_THAT(fv, ElementsAre(1, 2, 3)); -} - -TEST(Vector, FromInitializerListConstructor) { - { - chunked_vector fv({}); - - EXPECT_THAT(fv, IsValid()); - EXPECT_THAT(fv, ElementsAre()); - } - - { - chunked_vector fv({1, 2, 3}); - - EXPECT_THAT(fv, IsValid()); - EXPECT_THAT(fv, ElementsAre(1, 2, 3)); - } - - { - chunked_vector fv({1, 2, 3}); - - EXPECT_THAT(fv, IsValid()); - EXPECT_THAT(fv, ElementsAre(1, 2, 3)); - // chunked_vector should have a "tight" capacity when constructed - // from a list - EXPECT_EQ(fv.capacity(), 3); - } -} - -TEST(ChunkedVector, PushPop) { - for (int i = 0; i < 100; ++i) { - chunked_vector vec; - for (size_t i = 0; i < vec.elements_per_fragment(); ++i) { - bool push_back = vec.empty() || bool(random_generators::get_int(1)); - if (push_back) { - vec.push_back(i); - } else { - vec.pop_back(); - } - EXPECT_TRUE(chunked_vector_validator::validate(vec)); - } - } -} - -TEST(ChunkedVector, PushPopN) { - for (int i = 0; i < 100; ++i) { - chunked_vector vec; - for (size_t i = 0; i < vec.elements_per_fragment(); ++i) { - // Slight preference to make larger vectors because we could be - // popping back multiple - switch (random_generators::get_int(4)) { - case 0: - case 1: - case 2: - vec.push_back(i); - break; - case 3: - vec.pop_back_n(random_generators::get_int(vec.size())); - break; - case 4: - vec.erase_to_end( - vec.begin() + random_generators::get_int(vec.size())); - break; - } - - EXPECT_TRUE(chunked_vector_validator::validate(vec)); - } - } -} - -TEST(ChunkedVector, FirstChunkCapacityDoubles) { - chunked_vector vec; - for (size_t i = 0; i < vec.elements_per_fragment(); ++i) { - vec.push_back(i); - EXPECT_TRUE(chunked_vector_validator::validate(vec)); - } -} - -TEST(ChunkedVector, ReserveAndPushBack) { - chunked_vector vec; - vec.reserve(vec.elements_per_fragment()); - vec.push_back(-1); - int* initial_location = &vec.front(); - for (size_t i = 0; i < vec.elements_per_fragment(); ++i) { - vec.push_back(i); - EXPECT_EQ(initial_location, &vec.front()); - } -} - -TEST(ChunkedVector, Reserve) { - chunked_vector growing_vec; - for (size_t i = 0; i < chunked_vector::elements_per_fragment(); - ++i) { - growing_vec.reserve(i); - EXPECT_EQ(growing_vec.capacity(), i); - EXPECT_TRUE(chunked_vector_validator::validate(growing_vec)); - // Also ensure "jumping" to that reserved size does the right thing. - chunked_vector new_vec; - new_vec.reserve(i); - EXPECT_EQ(new_vec.capacity(), i); - EXPECT_TRUE(chunked_vector_validator::validate(new_vec)); - } -} - -TEST(ChunkedVector, ShrinkToFit) { - chunked_vector vec; - vec.reserve(32); - EXPECT_TRUE(chunked_vector_validator::validate(vec)); - for (int i = 0; i < 10; ++i) { - vec.push_back(1); - EXPECT_TRUE(chunked_vector_validator::validate(vec)); - } - vec.shrink_to_fit(); - EXPECT_TRUE(chunked_vector_validator::validate(vec)); - EXPECT_EQ(vec.capacity(), 10); -} - -TEST(ChunkedVector, FromRange) { - std::vector buffer; - buffer.reserve(10); - for (int i = 0; i < 10; ++i) { - buffer.push_back(i); - } - auto vec = chunked_vector(std::from_range, buffer); - EXPECT_THAT(vec, ElementsAreArray(buffer)); -} - -TEST(ChunkedVector, FromRangeUnsized) { - std::list input{1, 2, 3, 4, 5}; - auto vec = chunked_vector(std::from_range, input); - EXPECT_THAT(vec, ElementsAreArray(input)); -} - -struct move_only_t { - move_only_t() = default; - move_only_t(move_only_t&) = delete; - move_only_t& operator=(move_only_t&) = delete; - move_only_t(move_only_t&&) = default; - move_only_t& operator=(move_only_t&&) = default; - ~move_only_t() = default; -}; - -TEST(ChunkedVector, FromRangeMove) { - static_assert( - !(std::is_copy_constructible_v - || std::is_copy_assignable_v)); - - auto src = chunked_vector(); - - // From ref. - auto vec = chunked_vector( - std::from_range, src | std::views::as_rvalue); - - // From temporary. - auto vec2 = chunked_vector( - std::from_range, chunked_vector() | std::views::as_rvalue); -} - -TEST(ChunkedVector, FromRangeCopy) { - chunked_vector src; - static_assert(!std::is_copy_constructible_v); - auto vec = chunked_vector(std::from_range, src); -} - -struct adl_range_t { - std::vector data{1, 2, 3}; -}; -auto begin(const adl_range_t& r) { return r.data.begin(); } -auto end(const adl_range_t& r) { return r.data.end(); } - -TEST(ChunkedVector, FromRangeADL) { - adl_range_t r; - auto vec = chunked_vector(std::from_range, r); - EXPECT_THAT(vec, ElementsAreArray(r.data)); -} - -TEST(ChunkedVector, InPlaceSingleElement) { - auto v = chunked_vector>::single("a2", 3); - ASSERT_THAT(v, ElementsAre(std::make_pair("a2", 3))); -} - -} // namespace diff --git a/src/v/datalake/translation/scheduling.h b/src/v/datalake/translation/scheduling.h index c917d1b16d75e..5a35b2a6047bb 100644 --- a/src/v/datalake/translation/scheduling.h +++ b/src/v/datalake/translation/scheduling.h @@ -13,6 +13,7 @@ #include "base/format_to.h" #include "config/property.h" #include "container/chunked_hash_map.h" +#include "container/chunked_vector.h" #include "container/intrusive_list_helpers.h" #include "model/fundamental.h" #include "ssx/semaphore.h" diff --git a/src/v/iceberg/datatypes.cc b/src/v/iceberg/datatypes.cc index 9423f1089c797..304e19d66dc5e 100644 --- a/src/v/iceberg/datatypes.cc +++ b/src/v/iceberg/datatypes.cc @@ -10,6 +10,8 @@ #include "iceberg/datatypes.h" +#include "base/vassert.h" + #include namespace iceberg { diff --git a/src/v/serde/parquet/schema.cc b/src/v/serde/parquet/schema.cc index 23cac929fe665..81f97bcb473c9 100644 --- a/src/v/serde/parquet/schema.cc +++ b/src/v/serde/parquet/schema.cc @@ -11,6 +11,8 @@ #include "serde/parquet/schema.h" +#include "base/vassert.h" + #include #include #include diff --git a/src/v/utils/chunked_kv_cache.h b/src/v/utils/chunked_kv_cache.h index a4aee6c482cfe..9bccf56ec17fe 100644 --- a/src/v/utils/chunked_kv_cache.h +++ b/src/v/utils/chunked_kv_cache.h @@ -31,8 +31,8 @@ template< typename Key, typename Value, typename Hash = std::conditional_t< - detail::has_absl_hash, - detail::avalanching_absl_hash, + ss::internal::has_absl_hash, + ss::internal::avalanching_absl_hash, ankerl::unordered_dense::hash>, typename EqualTo = std::equal_to> class chunked_kv_cache {