diff --git a/conan.lock b/conan.lock index 9aa5830..ebdb611 100644 --- a/conan.lock +++ b/conan.lock @@ -1,8 +1,8 @@ { "version": "0.5", "requires": [ - "abseil/20250127.0", - "batteries/0.71.1", + "abseil/20260107.1", + "batteries/0.72.0", "boost/1.88.0", "bzip2/1.0.8", "cli11/2.5.0", @@ -10,16 +10,16 @@ "glog/0.7.1", "gtest/1.17.0", "keyvcr/0.2.2", - "libbacktrace/cci.20240730", + "libbacktrace/cci.20210118", "libfuse/3.16.2", "libpfm4/4.13.0", "libunwind/1.8.1", "liburing/2.11", - "llfs/0.47.1", + "llfs/0.47.2", "openssl/3.6.0", "pcg-cpp/cci.20220409", "protobuf/3.21.12", - "vqf/0.2.5", + "vqf/0.2.6", "xxhash/0.8.3", "yaml-cpp/0.9.0", "zlib/1.3.1" @@ -49,10 +49,7 @@ ], "boost/[>=1.84.0 <2]": [ "boost/1.88.0" - ], - "batteries/[>=0.60.2 <2]": [ - "batteries/0.70.2" ] }, "config_requires": [] -} \ No newline at end of file +} diff --git a/conanfile.py b/conanfile.py index d90205b..aa69a04 100644 --- a/conanfile.py +++ b/conanfile.py @@ -16,7 +16,7 @@ class TurtleKvRecipe(ConanFile): name = "turtle_kv" - python_requires = "cor_recipe_utils/0.19.1" + python_requires = "cor_recipe_utils/0.21.4.dev3+g0d8231b80" python_requires_extend = "cor_recipe_utils.ConanFileBase" settings = "os", "compiler", "build_type", "arch" @@ -88,18 +88,18 @@ def requirements(self): "force": True, } - self.requires("abseil/20250127.0", **VISIBLE, **OVERRIDE) - self.requires("batteries/[>=0.71.1 <1]", **VISIBLE, **OVERRIDE) - self.requires("boost/1.88.0", **VISIBLE, **OVERRIDE) - self.requires("glog/0.7.1", **VISIBLE) - self.requires("llfs/[>=0.47.0 <1]", **VISIBLE) - self.requires("pcg-cpp/cci.20220409", **VISIBLE) + self.requires("abseil/[>=20260107.1]", **VISIBLE, **OVERRIDE) + self.requires("batteries/[>=0.72.0 <1]", **VISIBLE, **OVERRIDE) + self.requires("boost/[>=1.88.0 <2]", **VISIBLE, **OVERRIDE) + self.requires("glog/[>=0.7.1 <1]", **VISIBLE) + self.requires("llfs/[>=0.47.2 <1]", **VISIBLE) + self.requires("pcg-cpp/[>=cci.20220409]", **VISIBLE) self.requires("yaml-cpp/[>=0.9.0 <1]") self.requires("zlib/1.3.1", **OVERRIDE) # boost/1.88.0 and ninja/1.13.2 depend (exactly) on libbacktrace/cci.20210118 # - self.requires("libbacktrace/[>=cci.20240730]", **OVERRIDE) + self.requires("libbacktrace/[>=cci.20210118]") if platform.system() == "Linux": if self.options.with_keyvcr: diff --git a/cor.yml b/cor.yml index 2a24d1b..0d48710 100644 --- a/cor.yml +++ b/cor.yml @@ -1,3 +1,3 @@ cor: cli: - version: 0.19.1 + version: 0.21.4.dev3+g0d8231b80 diff --git a/src/turtle_kv/core/edit_view.hpp b/src/turtle_kv/core/edit_view.hpp index b5e6f62..9fc9c20 100644 --- a/src/turtle_kv/core/edit_view.hpp +++ b/src/turtle_kv/core/edit_view.hpp @@ -1,3 +1,11 @@ +//=##=##=#==#=#==#===#+==#+==========+==+=+=+=+=+=++=+++=+++++=-++++=-+++++++++++ +// +// Part of the TurtleKV Project, under Apache License v2.0. +// See https://www.apache.org/licenses/LICENSE-2.0 for license information. +// SPDX short identifier: Apache-2.0 +// +//+++++++++++-+-+--+----- --- -- - - - - + #pragma once #include diff --git a/src/turtle_kv/import/buffer.hpp b/src/turtle_kv/import/buffer.hpp index 8640034..0d712e7 100644 --- a/src/turtle_kv/import/buffer.hpp +++ b/src/turtle_kv/import/buffer.hpp @@ -6,14 +6,13 @@ namespace turtle_kv { +using batt::advance_pointer; using batt::buffer_from_struct; +using batt::byte_distance; using batt::ConstBuffer; using batt::make_buffer; using batt::mutable_buffer_from_struct; using batt::MutableBuffer; using batt::resize_buffer; -using llfs::advance_pointer; -using llfs::byte_distance; - } // namespace turtle_kv diff --git a/src/turtle_kv/import/interval.hpp b/src/turtle_kv/import/interval.hpp index 53167ea..33cfe08 100644 --- a/src/turtle_kv/import/interval.hpp +++ b/src/turtle_kv/import/interval.hpp @@ -2,9 +2,6 @@ #include -#include -#include - namespace turtle_kv { using batt::BasicInterval; diff --git a/src/turtle_kv/import/slice.hpp b/src/turtle_kv/import/slice.hpp index 00a8b22..abf769a 100644 --- a/src/turtle_kv/import/slice.hpp +++ b/src/turtle_kv/import/slice.hpp @@ -6,7 +6,6 @@ namespace turtle_kv { using batt::as_const_slice; using batt::as_range; -using batt::as_seq; using batt::as_slice; using batt::Slice; diff --git a/src/turtle_kv/mem_table/mem_table.hpp b/src/turtle_kv/mem_table/mem_table.hpp index c135c2b..ccf03bb 100644 --- a/src/turtle_kv/mem_table/mem_table.hpp +++ b/src/turtle_kv/mem_table/mem_table.hpp @@ -490,7 +490,7 @@ class BasicMemTable::PerOpStorageContext // One thread will acquire a lock, others will block at this point. // - absl::MutexLock lock{&this->mem_table_.block_list_mutex_}; + absl::MutexLock lock{this->mem_table_.block_list_mutex_}; // If there are no block buffers attached to the MemTable, then we may just have to wait until // the checkpoint update pipeline catches up. If there are block buffers attached, then its diff --git a/src/turtle_kv/mem_table/mem_table.ipp b/src/turtle_kv/mem_table/mem_table.ipp index 0ec2d8e..233ce47 100644 --- a/src/turtle_kv/mem_table/mem_table.ipp +++ b/src/turtle_kv/mem_table/mem_table.ipp @@ -395,7 +395,7 @@ void BasicMemTable::handle_external_cache_alloc(i6 this->allocation_tracker_.allocate_external(cache_alloc_delta, overcommit); { - absl::MutexLock lock{&this->block_list_mutex_}; + absl::MutexLock lock{this->block_list_mutex_}; BATT_CHECK(this->cache_alloc_in_progress_); this->total_cache_alloc_.subsume(std::move(alloc)); this->cache_alloc_in_progress_ = false; @@ -417,7 +417,7 @@ void BasicMemTable::handle_external_cache_alloc(i6 } else if (cache_alloc_delta < 0) { StatusOr alloc_to_release; { - absl::MutexLock lock{&this->block_list_mutex_}; + absl::MutexLock lock{this->block_list_mutex_}; BATT_CHECK(this->cache_alloc_in_progress_); alloc_to_release = this->total_cache_alloc_.split(-cache_alloc_delta); this->cache_alloc_in_progress_ = false; diff --git a/src/turtle_kv/mem_table/mem_table.test.cpp b/src/turtle_kv/mem_table/mem_table.test.cpp index 68a684b..a894348 100644 --- a/src/turtle_kv/mem_table/mem_table.test.cpp +++ b/src/turtle_kv/mem_table/mem_table.test.cpp @@ -378,9 +378,9 @@ TEST_F(MemTableTest, PutGet) // TEST_F(MemTableTest, PutUntilFull) { - usize total_key_bytes = 0; - usize total_value_bytes = 0; - usize put_count = 0; + [[maybe_unused]] usize total_key_bytes = 0; + [[maybe_unused]] usize total_value_bytes = 0; + [[maybe_unused]] usize put_count = 0; for (;;) { KeyView key = this->make_random_key(); diff --git a/src/turtle_kv/script/uniform_key_distribution.hpp b/src/turtle_kv/script/uniform_key_distribution.hpp index 67fd211..700f1d4 100644 --- a/src/turtle_kv/script/uniform_key_distribution.hpp +++ b/src/turtle_kv/script/uniform_key_distribution.hpp @@ -37,7 +37,9 @@ inline constexpr std::array kHashSeeds = { class UniformInsertKeyDistribution : public KeyDistribution { public: - explicit UniformInsertKeyDistribution(usize key_size) noexcept : key_buffer_(key_size) + explicit UniformInsertKeyDistribution(usize key_size, usize seed = 0) noexcept + : next_ordinal_{seed} + , key_buffer_(key_size) { } @@ -48,7 +50,7 @@ class UniformInsertKeyDistribution : public KeyDistribution std::pair get_next(KeySet& inserted_keys) override { - return inserted_keys.create_key(this->format_key(this->count_.fetch_add(1))); + return inserted_keys.create_key(this->format_key(this->next_ordinal_.fetch_add(1))); } //+++++++++++-+-+--+----- --- -- - - - - @@ -77,7 +79,7 @@ class UniformInsertKeyDistribution : public KeyDistribution //+++++++++++-+-+--+----- --- -- - - - - - std::atomic count_{0}; + std::atomic next_ordinal_{0}; SmallVec key_buffer_; }; diff --git a/src/turtle_kv/tree/in_memory_node.cpp b/src/turtle_kv/tree/in_memory_node.cpp index 536e0c3..a3a712b 100644 --- a/src/turtle_kv/tree/in_memory_node.cpp +++ b/src/turtle_kv/tree/in_memory_node.cpp @@ -15,6 +15,8 @@ #include #include +#include + #include #include @@ -116,8 +118,8 @@ using PackedSegment = PackedUpdateBuffer::Segment; segment.page_id_slot = llfs::PageIdSlot::from_page_id(packed_segment.leaf_page_id.unpack()); segment.active_pivots = packed_segment.active_pivots.unpack(); - BATT_ASSIGN_OK_RESULT(segment.filter, - packed_node.create_piecewise_filter(level_i, segment_i)); + segment.filter = + PiecewiseFilter{packed_node.get_packed_filter(level_i, segment_i)}; segment.check_invariants(__FILE__, __LINE__); } diff --git a/src/turtle_kv/tree/in_memory_node.test.cpp b/src/turtle_kv/tree/in_memory_node.test.cpp index 22b3d56..52a5dd9 100644 --- a/src/turtle_kv/tree/in_memory_node.test.cpp +++ b/src/turtle_kv/tree/in_memory_node.test.cpp @@ -1,3 +1,11 @@ +//=##=##=#==#=#==#===#+==#+==========+==+=+=+=+=+=++=+++=+++++=-++++=-+++++++++++ +// +// Part of the TurtleKV Project, under Apache License v2.0. +// See https://www.apache.org/licenses/LICENSE-2.0 for license information. +// SPDX short identifier: Apache-2.0 +// +//+++++++++++-+-+--+----- --- -- - - - - + #include // #include @@ -12,9 +20,10 @@ #include #include +#include #include -#include +#include #include #include diff --git a/src/turtle_kv/tree/in_memory_node_merged_level.cpp b/src/turtle_kv/tree/in_memory_node_merged_level.cpp index 7a67284..a28be9f 100644 --- a/src/turtle_kv/tree/in_memory_node_merged_level.cpp +++ b/src/turtle_kv/tree/in_memory_node_merged_level.cpp @@ -16,6 +16,8 @@ #include +#include + #include namespace turtle_kv { diff --git a/src/turtle_kv/tree/in_memory_node_segmented_level.cpp b/src/turtle_kv/tree/in_memory_node_segmented_level.cpp index bba1672..19edf8d 100644 --- a/src/turtle_kv/tree/in_memory_node_segmented_level.cpp +++ b/src/turtle_kv/tree/in_memory_node_segmented_level.cpp @@ -10,6 +10,8 @@ #include #include +#include + #include #include diff --git a/src/turtle_kv/tree/packed_node_page.cpp b/src/turtle_kv/tree/packed_node_page.cpp index bff419e..c469d6f 100644 --- a/src/turtle_kv/tree/packed_node_page.cpp +++ b/src/turtle_kv/tree/packed_node_page.cpp @@ -1,3 +1,11 @@ +//=##=##=#==#=#==#===#+==#+==========+==+=+=+=+=+=++=+++=+++++=-++++=-+++++++++++ +// +// Part of the TurtleKV Project, under Apache License v2.0. +// See https://www.apache.org/licenses/LICENSE-2.0 for license information. +// SPDX short identifier: Apache-2.0 +// +//+++++++++++-+-+--+----- --- -- - - - - + #include // @@ -8,6 +16,7 @@ #include #include +#include #include @@ -213,9 +222,7 @@ StatusOr PackedNodePage::find_key(KeyQuery& query) const //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - // -PackedNodePage::UpdateBuffer::SegmentFilterData PackedNodePage::get_segment_filter_values( - usize level_i, - usize segment_i) const +PackedPiecewiseFilter PackedNodePage::get_packed_filter(usize level_i, usize segment_i) const { const usize i = [&]() -> usize { if (this->is_size_tiered()) { @@ -247,52 +254,9 @@ PackedNodePage::UpdateBuffer::SegmentFilterData PackedNodePage::get_segment_filt bool start_live = (segment.filter_start.value() & PackedNodePage::kSegmentStartsLive) != 0; - return PackedNodePage::UpdateBuffer::SegmentFilterData{ + return PackedPiecewiseFilter{PackedPiecewiseFilterStorage{ as_const_slice(packed_filters.data() + filter_start_i, packed_filters.data() + filter_end_i), - start_live}; -} - -//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - -// -StatusOr> PackedNodePage::create_piecewise_filter(usize level_i, - usize segment_i) const -{ - PackedNodePage::UpdateBuffer::SegmentFilterData filter_data = - this->get_segment_filter_values(level_i, segment_i); - - SmallVec, 64> live_ranges; - u32 i = 0; - - // If the first item at index 0 is live, add the corresponding interval first since the - // serialized version of the filter doesn't store index 0. - // - if (filter_data.start_is_live) { - if (filter_data.values.empty()) { - // Entire segment is live. - // - live_ranges.emplace_back( - Interval{PiecewiseFilter::kMinLowerBound, PiecewiseFilter::kMaxUpperBound}); - } else { - live_ranges.emplace_back( - Interval{PiecewiseFilter::kMinLowerBound, filter_data.values[i].value()}); - i++; - } - } - - for (; i + 1 < filter_data.values.size(); i += 2) { - live_ranges.emplace_back( - Interval{filter_data.values[i].value(), filter_data.values[i + 1].value()}); - } - - // If there's one unpaired value left, it's a lower_bound whose upper_bound (kMaxUpperBound) - // was omitted. - // - if (i < filter_data.values.size()) { - live_ranges.emplace_back( - Interval{filter_data.values[i].value(), PiecewiseFilter::kMaxUpperBound}); - } - - return PiecewiseFilter::from_live(as_slice(live_ranges)); + start_live}}; } //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - @@ -330,7 +294,11 @@ StatusOr PackedNodePage::UpdateBuffer::Segment::load_leaf_page bool PackedNodePage::UpdateBuffer::Segment::is_index_filtered(const SegmentedLevel& level, u32 index) const { - return !(this->live_lower_bound(level, index) == index); + const usize segment_i = std::distance(level.segments_slice.begin(), this); + + PackedPiecewiseFilter filter = level.packed_node_->get_packed_filter(level.level_i_, segment_i); + + return !filter.live_at_index(index); } //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - @@ -339,34 +307,10 @@ u32 PackedNodePage::UpdateBuffer::Segment::live_lower_bound(const SegmentedLevel u32 item_i) const { const usize segment_i = std::distance(level.segments_slice.begin(), this); - PackedNodePage::UpdateBuffer::SegmentFilterData filter_data = - level.packed_node_->get_segment_filter_values(level.level_i_, segment_i); - - const Slice filter_values = filter_data.values; - - if (filter_data.values.empty()) { - BATT_CHECK(filter_data.start_is_live); - return item_i; - } - - auto iter = std::upper_bound(filter_values.begin(), filter_values.end(), item_i); - - usize previous_cut_points = std::distance(filter_data.values.begin(), iter); - - bool is_live = (previous_cut_points % 2 == 0) == filter_data.start_is_live; - - // If we're already in an unfiltered region, just return the index. Otherwise, our upper bound - // is the next unfiltered index. - // - if (is_live) { - return item_i; - } - - if (iter != filter_values.end()) { - return iter->value(); - } - - return PiecewiseFilter::kMaxUpperBound; + + PackedPiecewiseFilter filter = level.packed_node_->get_packed_filter(level.level_i_, segment_i); + + return filter.live_lower_bound(item_i); } //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - @@ -375,48 +319,11 @@ Interval PackedNodePage::UpdateBuffer::Segment::get_live_item_range( const SegmentedLevel& level, Interval i) const { - u32 start_i = i.lower_bound; - u32 end_i = i.upper_bound; - - BATT_CHECK_LT(start_i, end_i); - const usize segment_i = std::distance(level.segments_slice.begin(), this); - PackedNodePage::UpdateBuffer::SegmentFilterData filter_data = - level.packed_node_->get_segment_filter_values(level.level_i_, segment_i); - - const Slice filter_values = filter_data.values; - - if (filter_data.values.empty()) { - BATT_CHECK(filter_data.start_is_live); - return i; - } - - auto iter = std::upper_bound(filter_values.begin(), filter_values.end(), start_i); - - usize previous_cut_points = std::distance(filter_data.values.begin(), iter); - - bool is_live = (previous_cut_points % 2 == 0) == filter_data.start_is_live; - - if (!is_live) { - if (iter == filter_values.end()) { - return Interval{end_i, end_i}; - } - - start_i = iter->value(); - if (start_i >= end_i) { - return Interval{end_i, end_i}; - } - - ++iter; - } - - if (iter != filter_values.end()) { - end_i = std::min(end_i, iter->value()); - } - - BATT_CHECK_LT(start_i, end_i) << BATT_INSPECT(i); - - return Interval{start_i, end_i}; + + PackedPiecewiseFilter filter = level.packed_node_->get_packed_filter(level.level_i_, segment_i); + + return filter.find_live_range(i); } //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - // @@ -496,4 +403,4 @@ std::function PackedNodePage::dump() const }; } -} // namespace turtle_kv \ No newline at end of file +} // namespace turtle_kv diff --git a/src/turtle_kv/tree/packed_node_page.hpp b/src/turtle_kv/tree/packed_node_page.hpp index 1e7cc66..5f4e92e 100644 --- a/src/turtle_kv/tree/packed_node_page.hpp +++ b/src/turtle_kv/tree/packed_node_page.hpp @@ -121,11 +121,6 @@ struct PackedNodePage { struct UpdateBuffer { struct SegmentedLevel; - struct SegmentFilterData { - Slice values; - bool start_is_live; - }; - struct Segment { llfs::PackedPageId leaf_page_id; // +8 -> 8 PackedActivePivotsSet64 active_pivots; // +8 -> 16 @@ -387,9 +382,7 @@ struct PackedNodePage { StatusOr find_key_in_level(usize level_i, KeyQuery& query, i32 key_pivot_i) const; - UpdateBuffer::SegmentFilterData get_segment_filter_values(usize level_i, usize segment_i) const; - - StatusOr> create_piecewise_filter(usize level_i, usize segment_i) const; + PackedPiecewiseFilter get_packed_filter(usize level_i, usize segment_i) const; //----- --- -- - - - - diff --git a/src/turtle_kv/tree/random_str.hpp b/src/turtle_kv/tree/random_str.hpp new file mode 100644 index 0000000..5145d56 --- /dev/null +++ b/src/turtle_kv/tree/random_str.hpp @@ -0,0 +1,48 @@ +//=##=##=#==#=#==#===#+==#+==========+==+=+=+=+=+=++=+++=+++++=-++++=-+++++++++++ +// +// Part of the TurtleKV Project, under Apache License v2.0. +// See https://www.apache.org/licenses/LICENSE-2.0 for license information. +// SPDX short identifier: Apache-2.0 +// +//+++++++++++-+-+--+----- --- -- - - - - + +#include + +#include + +#include +#include +#include +#include + +namespace turtle_kv { + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +template +std::string_view random_str(std::default_random_engine& rng, + SizeDistribution&& pick_size, + usize min_size, + usize max_size, + batt::StableStringStore& strings, + std::string_view prefix = "") noexcept +{ + std::uniform_int_distribution pick_char{'a', 'z'}; + + const usize n = min_size + std::min(pick_size(rng), max_size - min_size); + batt::MutableBuffer buf = strings.allocate(prefix.size() + n); + char* chars = static_cast(buf.data()); + + if (!prefix.empty()) { + std::memcpy(chars, prefix.data(), prefix.size()); + chars += prefix.size(); + } + + for (usize i = 0; i < n; ++i, ++chars) { + *chars = pick_char(rng); + } + + return std::string_view{static_cast(buf.data()), buf.size()}; +} + +} // namespace turtle_kv diff --git a/src/turtle_kv/tree/testing/fake_segment.hpp b/src/turtle_kv/tree/testing/fake_segment.hpp index 8297a28..860f722 100644 --- a/src/turtle_kv/tree/testing/fake_segment.hpp +++ b/src/turtle_kv/tree/testing/fake_segment.hpp @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -102,11 +103,11 @@ struct FakeSegment { { const bool inactive = this->active_pivots_.is_empty(); if (inactive) { - Slice> live_ranges = this->filter_.live(); - BATT_CHECK_EQ(live_ranges.size(), 1) << BATT_INSPECT(live_ranges); - BATT_CHECK_EQ(live_ranges[0].upper_bound, PiecewiseFilter::kMaxUpperBound) - << BATT_INSPECT(live_ranges); - } + Slice> live_ranges = this->filter_.live(); + BATT_CHECK_EQ(live_ranges.size(), 1) << BATT_INSPECT(live_ranges); + BATT_CHECK_EQ(live_ranges[0].upper_bound, PiecewiseFilter::kMaxUpperBound) + << BATT_INSPECT(live_ranges); + } return inactive; } diff --git a/src/turtle_kv/util/art.hpp b/src/turtle_kv/util/art.hpp index b9903cd..d4c050b 100644 --- a/src/turtle_kv/util/art.hpp +++ b/src/turtle_kv/util/art.hpp @@ -948,7 +948,7 @@ class ARTBase ~MemoryContext() noexcept { if (this->art_) { - absl::MutexLock lock{&this->art_->mutex_}; + absl::MutexLock lock{this->art_->mutex_}; for (auto& p_ex : this->thread_extents_) { this->art_->extents_.emplace_back(std::move(p_ex)); } diff --git a/src/turtle_kv/util/packed_piecewise_filter_view.hpp b/src/turtle_kv/util/packed_piecewise_filter_view.hpp new file mode 100644 index 0000000..990088f --- /dev/null +++ b/src/turtle_kv/util/packed_piecewise_filter_view.hpp @@ -0,0 +1,306 @@ +//=##=##=#==#=#==#===#+==#+==========+==+=+=+=+=+=++=+++=+++++=-++++=-+++++++++++ +// +// Part of the TurtleKV Project, under Apache License v2.0. +// See https://www.apache.org/licenses/LICENSE-2.0 for license information. +// SPDX short identifier: Apache-2.0 +// +//+++++++++++-+-+--+----- --- -- - - - - + +#pragma once +#define TURTLE_KV_UTIL_PACKED_PIECEWISE_FILTER_VIEW_HPP + +#include "piecewise_filter_storage_model.concept.hpp" + +#include +#include +#include + +#include +#include + +#include + +#include + +namespace turtle_kv { + +//=#=#==#==#===============+=+=+=+=++=++++++++++++++-++-+--+-+----+--------------- +// +/** \brief Read-only model of PiecewiseFilterStorageModel for packed filters. + * + * Packed piecewise filters are represented as an array of integers, which are the boundaries + * between live and dropped intervals, plus an additional boolean/bit denoting whether the interval + * from the global minimum to the first stored boundary is live or dropped (`start_is_live`). + * + * The global minimum and maximum bounds are never stored in the packed representation. Instead, + * the minimum is implied via the `start_is_live` bit, and the maximum by whether the number of + * stored bounds (plus the implicit first bound, if start_is_live == true) is even or odd. If it is + * odd, then it is implied that there is a final bound equal to the global maximum. + * + * Examples: + * + * Live Intervals: {[0, 10), [20, 30), [40, 50)} + * Packed: start_is_live=1, {10, 20, 30, 40, 50} + * + * Live Intervals: {[0, 10), [20, 30), [40, +inf)} + * Packed: start_is_live=1, {10, 20, 30, 40} + * + * Live Intervals: {[10, 20), [30, 40), [50, 60)} + * Packed: start_is_live=0, {10, 20, 30, 40, 50, 60} + * + * Live Intervals: {[10, 20), [30, 40), [50, +inf)} + * Packed: start_is_live=0, {10, 20, 30, 40, 50} + */ +class PackedPiecewiseFilterStorage +{ + public: + //----- --- -- - - - - + + // Forward-declaration; the type returned by this->begin(), this->end() + // + class const_iterator; + + /** \brief The boundary integer type. Must be unsigned. + */ + using OffsetT = const little_u32; + + /** \brief The live range type; what `iterator` iterates over. + */ + using value_type = Interval; + + /** \brief Non-const iterator aliases const_iterator, since this storage model is read-only. + */ + using iterator = const_iterator; + + // Forward-declaration; defined below. + // + friend const Slice& as_const_slice(const PackedPiecewiseFilterStorage& view); + + //----- --- -- - - - - + + /** \brief Constructs an PackedPiecewiseFilterStorage representing the live interval [0, +inf). + */ + PackedPiecewiseFilterStorage() = default; + + /** \brief Destructs the PackedPiecewiseFilterStorage. + */ + ~PackedPiecewiseFilterStorage() = default; + + /** \brief PackedPiecewiseFilterStorage is copy constructible. + */ + PackedPiecewiseFilterStorage(const PackedPiecewiseFilterStorage&) = default; + + /** \brief PackedPiecewiseFilterStorage is copy assignable. + */ + PackedPiecewiseFilterStorage& operator=(const PackedPiecewiseFilterStorage&) = default; + + /** \brief Constructs PackedPiecewiseFilterStorage from the packed data in the arguments. + * + * See the class-level description for details on what `values` and `start_is_live` represent. + */ + explicit PackedPiecewiseFilterStorage(const Slice& values, + bool start_is_live) noexcept + : values_{values} + , implicit_first_{start_is_live ? 1 : 0} + , size_{(this->implicit_first_ + BATT_CHECKED_CAST(i32, this->values_.size()) + 1) / 2} + { + } + + //----- --- -- - - - - + + /** \brief Returns an iterator to the first live interval in this filter. + */ + const_iterator begin() const noexcept; + + /** \brief Returns an iterator to one past the last live interval in this filter. + */ + const_iterator end() const noexcept; + + /** \brief Returns the number of live intervals in the filter; same as + * `std::distance(this->begin(), this->end())`. + */ + usize size() const noexcept; + + /** \brief Returns true iff `this->size() == 0`. + */ + bool empty() const noexcept; + + /** \brief Returns the `i`-th live interval in the filter. Behavior is undefined if `i` is not + * less than `this->size()`. + */ + Interval operator[](isize i) const noexcept; + + //----- --- -- - - - - + private: + /** \brief Points at the stored boundaries, as described in the class-level doc. + */ + Slice values_; + + /** \brief Set to 1 if `start_is_live`, else 0. + */ + i32 implicit_first_ = 1; + + /** \brief The number of live intervals in the filter. + */ + i32 size_ = 1; +}; + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +/** \brief Returns a const reference to the stored values referenced by `view`. + */ +inline const Slice& as_const_slice(const PackedPiecewiseFilterStorage& view) +{ + return view.values_; +} + +//=#=#==#==#===============+=+=+=+=++=++++++++++++++-++-+--+-+----+--------------- +// +/** \brief Read-only, random access iterator over the live intervals of a packed piecewise filter. + */ +class PackedPiecewiseFilterStorage::const_iterator + : public boost::iterator_facade< // + PackedPiecewiseFilterStorage::const_iterator, // <- Derived + Interval, // <- Value + std::random_access_iterator_tag, // <- CategoryOrTraversal + Interval, // <- Reference + isize // <- Difference + > +{ + public: + using Self = const_iterator; + using iterator_category = std::random_access_iterator_tag; + using value_type = Interval; + using reference = Interval; + + //+++++++++++-+-+--+----- --- -- - - - - + + /** \brief Constructs an invalid iterator. + */ + const_iterator() noexcept : view_{nullptr}, pos_{0} + { + } + + /** \brief Constructs an iterator to the `pos`-th live interval of `view`. + * + * `view` must remain in-scope while this object exists. + */ + const_iterator(const PackedPiecewiseFilterStorage* view, isize pos) noexcept : view_{view}, pos_{pos} + { + } + + //+++++++++++-+-+--+----- --- -- - - - - + + /** \brief Returns the live interval at the current position. + */ + reference dereference() const + { + return (*this->view_)[this->pos_]; + } + + /** \brief Returns true iff this iterator points to the same live interval of the same filter as + * `other`. + */ + bool equal(const Self& other) const + { + return this->view_ == other.view_ && this->pos_ == other.pos_; + } + + /** \brief Moves this iterator forward by one. + */ + void increment() + { + ++this->pos_; + } + + /** \brief Moves this iterator backward by one. + */ + void decrement() + { + --this->pos_; + } + + /** \brief Moves this iterator by `delta`. + */ + void advance(isize delta) + { + this->pos_ += delta; + } + + /** \brief Returns the number of steps required to advance this iterator so it is equivalent to + * `other`. Will panic if this and other do not point at the same filter view. + */ + isize distance_to(const Self& other) const + { + BATT_CHECK_EQ(this->view_, other.view_); + return other.pos_ - this->pos_; + } + + //+++++++++++-+-+--+----- --- -- - - - - + private: + /** \brief Pointer to the filter view over which we are iterating. + */ + const PackedPiecewiseFilterStorage* view_; + + /** \brief The (logical) position of this iterator within `view_`. + */ + isize pos_; +}; + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +inline auto PackedPiecewiseFilterStorage::begin() const noexcept -> const_iterator +{ + return const_iterator{this, 0}; +} + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +inline auto PackedPiecewiseFilterStorage::end() const noexcept -> const_iterator +{ + return const_iterator{this, static_cast(this->size())}; +} + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +inline usize PackedPiecewiseFilterStorage::size() const noexcept +{ + return this->size_; +} + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +inline bool PackedPiecewiseFilterStorage::empty() const noexcept +{ + return this->size_ == 0; +} + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +inline Interval PackedPiecewiseFilterStorage::operator[](isize i) const noexcept +{ + // Cached for brevity below. + // + const isize n = this->values_.size(); + + // The index within this->values_ of the i-th live interval's lower bound. + // May be negative if the first boundary is implicit (0) + // + const isize j0 = i * 2 - this->implicit_first_; + + // The index within this->values_ of the i-th live interval's upper bound. + // May be past the end of this->values_ if the last boundary is implicit (+inf) + // + const isize j1 = j0 + 1; + + const u32 lower_bound = (j0 < 0) ? std::numeric_limits::min() : this->values_[j0].value(); + const u32 upper_bound = (j1 < n) ? this->values_[j1].value() : std::numeric_limits::max(); + + return Interval{lower_bound, upper_bound}; +} + +//=#=#==#==#===============+=+=+=+=++=++++++++++++++-++-+--+-+----+--------------- + +static_assert(PiecewiseFilterStorageModel); + +} // namespace turtle_kv diff --git a/src/turtle_kv/util/piecewise_filter.hpp b/src/turtle_kv/util/piecewise_filter.hpp index f524319..9f22ceb 100644 --- a/src/turtle_kv/util/piecewise_filter.hpp +++ b/src/turtle_kv/util/piecewise_filter.hpp @@ -9,6 +9,9 @@ #pragma once #define TURTLE_KV_UTIL_PIECEWISE_FILTER_HPP +#include "packed_piecewise_filter_view.hpp" +#include "piecewise_filter_storage_model.concept.hpp" + #include #include #include @@ -17,6 +20,8 @@ #include #include +#include + #include #include @@ -26,11 +31,15 @@ namespace turtle_kv { /** \brief A representation of a filtered range of items. */ -template -class PiecewiseFilter +template ModelT> +class BasicPiecewiseFilter : private ModelT { public: - using Self = PiecewiseFilter; + using Self = BasicPiecewiseFilter; + + using ConstIterator = typename ModelT::const_iterator; + + class LiveSubranges; static_assert(std::is_integral::value && std::is_unsigned::value, "Offset must be an unsigned integer type!"); @@ -46,14 +55,27 @@ class PiecewiseFilter /** \brief Creates and returns a PiecewiseFilter instance from a range of intervals that contain * the live item indexes. */ - static StatusOr from_live(const Slice>& live); + static StatusOr from_live(const Slice>& live) + requires PiecewiseFilterMutableStorageModel; //+++++++++++-+-+--+----- --- -- - - - - /** \brief Constructs a default instance of a PiecewiseFilter object, initialized with no item * range and filtered items. */ - PiecewiseFilter() noexcept; + BasicPiecewiseFilter() noexcept + requires PiecewiseFilterMutableStorageModel; + + /** \brief Constructs a BasicPiecewiseFilter directly from a storage model instance. + */ + explicit BasicPiecewiseFilter(const ModelT& model) noexcept; + + /** \brief Constructs a BasicPiecewiseFilter by copying live intervals from a filter with a + * different storage model. + */ + template OtherModelT> + explicit BasicPiecewiseFilter(const BasicPiecewiseFilter& other) + requires PiecewiseFilterMutableStorageModel; //+++++++++++-+-+--+----- --- -- - - - - @@ -63,7 +85,8 @@ class PiecewiseFilter * * \return The new dropped interval that coincides with `i`. */ - Interval drop_index_range(Interval i); + Interval drop_index_range(Interval i) + requires PiecewiseFilterMutableStorageModel; /** \brief Returns whether or not the item at index `i` has been filtered out. * @@ -95,11 +118,34 @@ class PiecewiseFilter /** \brief Returns a view of the live item intervals. */ - Slice> live() const; + Slice> live() const + requires PiecewiseFilterMutableStorageModel; /** \brief Merges two filters in place, taking the union of the live intervals. */ - void merge(const PiecewiseFilter& other); + void merge(const Self& other) + requires PiecewiseFilterMutableStorageModel; + + /** \brief Returns a seq of Interval that is the intersection of `i` and the live ranges + * of this filter. + */ + LiveSubranges live_subranges_of(Interval i) const; + + /** \brief Returns an iterator to the first live interval. + */ + ConstIterator begin() const; + + /** \brief Returns an iterator past the last live interval. + */ + ConstIterator end() const; + + /** \brief Returns the number of live intervals. + */ + usize size() const; + + /** \brief Returns true iff there are no live intervals. + */ + bool empty() const; /** \brief Validate the state of the live intervals. */ @@ -109,11 +155,24 @@ class PiecewiseFilter //+++++++++++-+-+--+----- --- -- - - - - private: - /** \brief The range of filtered out item indexes. - */ - SmallVec, 64> live_; + BATT_ALWAYS_INLINE ModelT& live_() noexcept + { + return *this; + } + + BATT_ALWAYS_INLINE const ModelT& live_() const noexcept + { + return *this; + } }; +template +using PiecewiseFilter = BasicPiecewiseFilter, 64>>; + +using PackedPiecewiseFilter = BasicPiecewiseFilter; + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// template inline Interval drop_item_range(PiecewiseFilter& filter, const Slice& items, @@ -127,6 +186,5 @@ inline Interval drop_item_range(PiecewiseFilter& filter, return filter.drop_index_range(Interval{start_i, end_i}); } -} // namespace turtle_kv -#include +} // namespace turtle_kv diff --git a/src/turtle_kv/util/piecewise_filter.ipp b/src/turtle_kv/util/piecewise_filter.ipp index 3456043..3848365 100644 --- a/src/turtle_kv/util/piecewise_filter.ipp +++ b/src/turtle_kv/util/piecewise_filter.ipp @@ -11,20 +11,19 @@ #include "piecewise_filter.hpp" -#include - namespace turtle_kv { //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - // -template -/*static*/ StatusOr> PiecewiseFilter::from_live( - const Slice>& live) +template ModelT> +/*static*/ StatusOr> +BasicPiecewiseFilter::from_live(const Slice>& live) + requires PiecewiseFilterMutableStorageModel { - PiecewiseFilter filter; - filter.live_.clear(); + Self filter; - filter.live_.insert(filter.live_.end(), live.begin(), live.end()); + filter.live_().clear(); + filter.live_().insert(filter.live_().end(), live.begin(), live.end()); if (!filter.check_invariants()) { return Status{::batt::StatusCode::kInvalidArgument}; @@ -35,16 +34,41 @@ template //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - // -template -PiecewiseFilter::PiecewiseFilter() noexcept - : live_{{Interval{Self::kMinLowerBound, Self::kMaxUpperBound}}} +template ModelT> +BasicPiecewiseFilter::BasicPiecewiseFilter() noexcept + requires PiecewiseFilterMutableStorageModel + : ModelT{{Interval{Self::kMinLowerBound, Self::kMaxUpperBound}}} +{ +} + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +template ModelT> +BasicPiecewiseFilter::BasicPiecewiseFilter(const ModelT& model) noexcept + : ModelT{model} +{ +} + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +template ModelT> +template OtherModelT> +BasicPiecewiseFilter::BasicPiecewiseFilter( + const BasicPiecewiseFilter& other) + requires PiecewiseFilterMutableStorageModel + : ModelT{} { + this->live_().clear(); + + for (auto iter = other.begin(); iter != other.end(); ++iter) { + this->live_().insert(this->live_().end(), *iter); + } } //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - // -template -bool PiecewiseFilter::check_invariants() const +template ModelT> +bool BasicPiecewiseFilter::check_invariants() const { Optional prev_upper_bound = None; @@ -52,7 +76,7 @@ bool PiecewiseFilter::check_invariants() const // - all intervals are in non-decreasing order // - no intervals overlap or are adjacent (i.e., prev.upper_bound == next.lower_bound) // - for (const Interval& range : this->live_) { + for (const Interval& range : this->live_()) { // If a range has the minimum lower bound, it must be the first. // if (range.lower_bound == Self::kMinLowerBound && prev_upper_bound) { @@ -80,15 +104,16 @@ bool PiecewiseFilter::check_invariants() const //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - // -template -Interval PiecewiseFilter::drop_index_range(Interval to_drop) +template ModelT> +Interval BasicPiecewiseFilter::drop_index_range(Interval to_drop) + requires PiecewiseFilterMutableStorageModel { if (to_drop.empty()) { return to_drop; } - auto [first, last] = std::equal_range(this->live_.begin(), - this->live_.end(), + auto [first, last] = std::equal_range(this->live_().begin(), + this->live_().end(), to_drop, typename Interval::LinearOrder{}); @@ -98,7 +123,7 @@ Interval PiecewiseFilter::drop_index_range(Interval t // the return value bounds correctly. // if (first == last || to_drop.lower_bound < first->lower_bound) { - if (first != this->live_.begin()) { + if (first != this->live_().begin()) { // We are starting in a live interval gap (dropped region), so we extend to the previous live // interval's upper bound. // @@ -109,7 +134,7 @@ Interval PiecewiseFilter::drop_index_range(Interval t } if (first == last || to_drop.upper_bound >= std::prev(last)->upper_bound) { - if (last != this->live_.end()) { + if (last != this->live_().end()) { // We are ending in a live interval gap, so we extend to the next live interval's start. // dropped.upper_bound = last->lower_bound; @@ -164,7 +189,7 @@ Interval PiecewiseFilter::drop_index_range(Interval t // Process all overlapping intervals with `to_drop`. // - while (first != this->live_.end()) { + while (first != this->live_().end()) { if (first->lower_bound >= to_drop.upper_bound) { // Interval is entirely after `to_drop`, so there is nothing left to process. // @@ -177,7 +202,7 @@ Interval PiecewiseFilter::drop_index_range(Interval t // Interval right_half{to_drop.upper_bound, first->upper_bound}; first->upper_bound = to_drop.lower_bound; - this->live_.insert(std::next(first), right_half); + this->live_().insert(std::next(first), right_half); return dropped; } else { // Cases 2b and 4. @@ -193,7 +218,7 @@ Interval PiecewiseFilter::drop_index_range(Interval t } else { // Case 1. // - first = this->live_.erase(first); + first = this->live_().erase(first); } } @@ -202,41 +227,74 @@ Interval PiecewiseFilter::drop_index_range(Interval t //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - // -template -Slice> PiecewiseFilter::live() const +template ModelT> +Slice> BasicPiecewiseFilter::live() const + requires PiecewiseFilterMutableStorageModel +{ + return as_const_slice(this->live_()); +} + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +template ModelT> +auto BasicPiecewiseFilter::begin() const -> ConstIterator +{ + return this->live_().begin(); +} + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +template ModelT> +auto BasicPiecewiseFilter::end() const -> ConstIterator +{ + return this->live_().end(); +} + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +template ModelT> +usize BasicPiecewiseFilter::size() const +{ + return this->live_().size(); +} + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +template ModelT> +bool BasicPiecewiseFilter::empty() const { - return as_const_slice(this->live_); + return this->live_().empty(); } //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - // -template -bool PiecewiseFilter::live_at_index(OffsetT i) const +template ModelT> +bool BasicPiecewiseFilter::live_at_index(OffsetT i) const { return this->live_lower_bound(i) == i; } //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - // -template -OffsetT PiecewiseFilter::live_lower_bound(OffsetT i) const +template ModelT> +OffsetT BasicPiecewiseFilter::live_lower_bound(OffsetT i) const { // Compute the live interval which could contain `i`. // - auto iter = std::lower_bound(this->live_.begin(), - this->live_.end(), + auto iter = std::lower_bound(this->live_().begin(), + this->live_().end(), i, typename Interval::LinearOrder{}); // Check if current interval contains `i`. // - if (iter != this->live_.end() && iter->contains(i)) { + if (iter != this->live_().end() && iter->contains(i)) { return i; } // `i` is in a dropped range, so we return the start of the next live interval. // - if (iter != this->live_.end()) { + if (iter != this->live_().end()) { return iter->lower_bound; } @@ -247,22 +305,22 @@ OffsetT PiecewiseFilter::live_lower_bound(OffsetT i) const //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - // -template -Interval PiecewiseFilter::find_live_range(Interval i) const +template ModelT> +Interval BasicPiecewiseFilter::find_live_range(Interval i) const { OffsetT start_i = i.lower_bound; OffsetT end_i = i.upper_bound; BATT_CHECK_LE(start_i, end_i); - auto iter = std::lower_bound(this->live_.begin(), - this->live_.end(), + auto iter = std::lower_bound(this->live_().begin(), + this->live_().end(), start_i, typename Interval::LinearOrder{}); // Check if current interval contains or starts at `start_i`. // - if (iter != this->live_.end()) { + if (iter != this->live_().end()) { if (iter->contains(start_i)) { OffsetT live_end = std::min(end_i, iter->upper_bound); return Interval{start_i, live_end}; @@ -283,25 +341,26 @@ Interval PiecewiseFilter::find_live_range(Interval i) //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - // -template -void PiecewiseFilter::merge(const PiecewiseFilter& other) +template ModelT> +void BasicPiecewiseFilter::merge(const Self& other) + requires PiecewiseFilterMutableStorageModel { // If other has no live intervals, we are done. // - if (other.live_.empty()) { + if (other.live_().empty()) { return; } // If this has no live intervals, copy from other. // - if (this->live_.empty()) { - this->live_.insert(this->live_.end(), other.live_.begin(), other.live_.end()); + if (this->live_().empty()) { + this->live_().insert(this->live_().end(), other.live_().begin(), other.live_().end()); BATT_CHECK(this->check_invariants()); return; } SmallVec, 64> merged_intervals; - merged_intervals.reserve(this->live_.size() + other.live_.size()); + merged_intervals.reserve(this->live_().size() + other.live_().size()); usize i = 0; usize j = 0; @@ -329,42 +388,57 @@ void PiecewiseFilter::merge(const PiecewiseFilter& other) // Merge the live intervals arrays. // - while (i < this->live_.size() && j < other.live_.size()) { - if (this->live_[i].lower_bound <= other.live_[j].lower_bound) { - add_interval(this->live_[i]); + while (i < this->live_().size() && j < other.live_().size()) { + if (this->live_()[i].lower_bound <= other.live_()[j].lower_bound) { + add_interval(this->live_()[i]); ++i; } else { - add_interval(other.live_[j]); + add_interval(other.live_()[j]); ++j; } } - // Add remaining intervals from this->live_. + // Add remaining intervals.. // - while (i < this->live_.size()) { - add_interval(this->live_[i]); + while (i < this->live_().size()) { + add_interval(this->live_()[i]); ++i; } // Add remaining intervals from other.live_. // - while (j < other.live_.size()) { - add_interval(other.live_[j]); + while (j < other.live_().size()) { + add_interval(other.live_()[j]); ++j; } - this->live_ = std::move(merged_intervals); + this->live_() = std::move(merged_intervals); BATT_CHECK(this->check_invariants()); } //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - // -template -SmallFn PiecewiseFilter::dump() const +template ModelT> +SmallFn BasicPiecewiseFilter::dump() const { return [this](std::ostream& out) { - out << batt::dump_range(this->live_); + out << batt::dump_range(this->live_()); }; } + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +template ModelT> +auto BasicPiecewiseFilter::live_subranges_of(Interval query_range) const + -> LiveSubranges +{ + const auto [first, last] = std::equal_range(this->live_().begin(), + this->live_().end(), + query_range, + typename Interval::LinearOrder{}); + + return LiveSubranges{query_range, std::ranges::subrange(first, last)}; +} + } // namespace turtle_kv diff --git a/src/turtle_kv/util/piecewise_filter.live_subranges.hpp b/src/turtle_kv/util/piecewise_filter.live_subranges.hpp new file mode 100644 index 0000000..a22a0b1 --- /dev/null +++ b/src/turtle_kv/util/piecewise_filter.live_subranges.hpp @@ -0,0 +1,88 @@ +//=##=##=#==#=#==#===#+==#+==========+==+=+=+=+=+=++=+++=+++++=-++++=-+++++++++++ +// +// Part of the TurtleKV Project, under Apache License v2.0. +// See https://www.apache.org/licenses/LICENSE-2.0 for license information. +// SPDX short identifier: Apache-2.0 +// +//+++++++++++-+-+--+----- --- -- - - - - + +#pragma once +#define TURTLE_KV_UTIL_PIECEWISE_FILTER_LIVE_SUBRANGES_HPP + +#include "piecewise_filter.hpp" + +namespace turtle_kv { + +//=#=#==#==#===============+=+=+=+=++=++++++++++++++-++-+--+-+----+--------------- +// +/** \brief A (batt) Seq over the sub-ranges of a PiecewiseFilter which match some interval (the + * `query_range`). + */ +template ModelT> +class BasicPiecewiseFilter::LiveSubranges +{ + public: + using Iterator = typename BasicPiecewiseFilter::ConstIterator; + using Item = Interval; + + //+++++++++++-+-+--+----- --- -- - - - - + + /** \brief Constructs a LiveSubranges seq containing the passed range of live intervals (`match`), + * with the first and last element clamped to the `query_range`. + * + * `match` *must* not extend more than one live interval past `query_range` at the front or back. + */ + explicit LiveSubranges(Interval query_range, + std::ranges::subrange match) noexcept + : clamp_lower_{query_range.lower_bound} + , clamp_upper_{query_range.upper_bound} + , match_{match} + { + } + + /** \brief Returns the current live subrange, or None if the seq has been fully consumed. + */ + Optional peek() + { + if (this->match_.empty()) { + return None; + } + Interval item = this->match_.front(); + if (this->clamp_lower_) { + item.lower_bound = std::max(item.lower_bound, *this->clamp_lower_); + } + if (this->match_.size() == 1 && this->clamp_upper_) { + item.upper_bound = std::min(item.upper_bound, *this->clamp_upper_); + } + return item; + } + + /** \brief Returns the current live subrange, or None if the seq has been fully consumed, + * consuming the returned item. + */ + Optional next() + { + Optional item = this->peek(); + if (item) { + this->clamp_lower_ = None; + this->match_.advance(1); + } + return item; + } + + //+++++++++++-+-+--+----- --- -- - - - - + private: + /** \brief The lower bound to which to clamp the first interval of `this->match_`. + */ + Optional clamp_lower_; + + /** \brief The upper bound to which to clamp the last interval of `this->match_`. + */ + Optional clamp_upper_; + + /** \brief The current subrange of the live intervals in the filter. + */ + std::ranges::subrange match_; +}; + +} // namespace turtle_kv diff --git a/src/turtle_kv/util/piecewise_filter.test.cpp b/src/turtle_kv/util/piecewise_filter.test.cpp index d4c6ee4..7aa134e 100644 --- a/src/turtle_kv/util/piecewise_filter.test.cpp +++ b/src/turtle_kv/util/piecewise_filter.test.cpp @@ -13,8 +13,18 @@ #include #include +#include +#include +#include +#include + #include +#include +#include +#include +#include + #include #include #include @@ -33,14 +43,26 @@ using turtle_kv::PiecewiseFilter; using turtle_kv::Slice; using turtle_kv::Status; using turtle_kv::StatusOr; +using turtle_kv::testing::build_filter_with_random_drops; +using turtle_kv::testing::drop_n_disjoint_intervals_from; +using turtle_kv::testing::get_packed_filter_from_data; +using turtle_kv::testing::PackedFilterData; +using turtle_kv::testing::pack_in_memory_filter; +using turtle_kv::testing::RandomDropResult; using turtle_kv::testing::RandomStringGenerator; +using turtle_kv::testing::verify_filter_queries; using turtle_kv::drop_item_range; using llfs::KeyRangeOrder; +using batt::mask_from_interval; using batt::StableStringStore; +using turtle_kv::PackedPiecewiseFilter; +using turtle_kv::PackedPiecewiseFilterStorage; + + //==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - // TEST(PiecewiseFilterTest, InvalidFilterTest) @@ -73,89 +95,68 @@ TEST(PiecewiseFilterTest, InvalidFilterTest) // TEST(PiecewiseFilterTest, QueryTest) { - const usize num_items = 10000; + const u32 num_items = 10000; - for (usize seed = 0; seed < 100; ++seed) { + for (u32 seed = 0; seed < 100; ++seed) { std::default_random_engine rng{seed}; - PiecewiseFilter filter; - EXPECT_TRUE(filter.check_invariants()); + auto [filter, live_items] = build_filter_with_random_drops(num_items, rng); - // All items start live. - // - std::set live_items; - for (usize i = 0; i < num_items; ++i) { - live_items.insert(i); - } + EXPECT_TRUE(filter.check_invariants()); - // Drop random intervals. - // - std::uniform_int_distribution pick_num_dropped{100, num_items / 2}; - usize num_intervals_dropped = pick_num_dropped(rng); - for (usize i = 0; i < num_intervals_dropped; ++i) { - std::uniform_int_distribution pick_interval_start{0, num_items - 1}; - usize start_i = pick_interval_start(rng); + verify_filter_queries(filter, live_items, num_items, seed, rng); + } +} - std::uniform_int_distribution pick_interval_end{start_i, num_items}; - usize end_i = pick_interval_end(rng); +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +TEST(PiecewiseFilterTest, PackedQueryTest) +{ + const u32 num_items = 10000; - for (usize j = start_i; j < end_i; ++j) { - live_items.erase(j); - } + for (u32 seed = 0; seed < 100; ++seed) { + std::default_random_engine rng{seed}; - Interval new_dropped = filter.drop_index_range(Interval{start_i, end_i}); - EXPECT_LE(new_dropped.lower_bound, start_i) << BATT_INSPECT(seed); - EXPECT_GE(new_dropped.upper_bound, end_i) << BATT_INSPECT(seed); - } + auto [filter, live_items] = build_filter_with_random_drops(num_items, rng); EXPECT_TRUE(filter.check_invariants()); - // Test live_at_index + // Pack the filter and construct a PackedPiecewiseFilter. // - for (usize i = 0; i < num_items; ++i) { - bool expected_live = live_items.count(i) > 0; - bool actual_live = filter.live_at_index(i); - EXPECT_EQ(actual_live, expected_live) << BATT_INSPECT(seed) << BATT_INSPECT(i); - } + PackedFilterData packed_data = pack_in_memory_filter(filter); + PackedPiecewiseFilter packed_filter = get_packed_filter_from_data(packed_data); - // Test live_lower_bound + // Verify the packed filter has the same number of live intervals. // - for (usize i = 0; i < num_items; ++i) { - auto iter = live_items.lower_bound(i); - usize expected = (iter != live_items.end()) ? *iter : num_items; - usize actual = filter.live_lower_bound(i); - EXPECT_EQ(actual, expected) << BATT_INSPECT(seed) << BATT_INSPECT(i); - } + EXPECT_EQ(packed_filter.size(), filter.size()) << BATT_INSPECT(seed); - // Test find_live_range + // Verify the packed filter produces identical intervals. // - for (usize i = 0; i < 100; ++i) { - std::uniform_int_distribution pick_interval_start{0, num_items - 1}; - usize start_i = pick_interval_start(rng); - - std::uniform_int_distribution pick_interval_end{start_i, num_items}; - usize end_i = pick_interval_end(rng); - - auto iter = live_items.lower_bound(start_i); - Interval expected_range; - - if (iter == live_items.end() || *iter >= end_i) { - expected_range = Interval{end_i, end_i}; - } else { - usize first = *iter; - usize last = first + 1; - auto next = std::next(iter); - - while (next != live_items.end() && *next < end_i && *next == last) { - ++last; - ++next; - } - - expected_range = Interval{first, last}; + { + auto mutable_iter = filter.begin(); + auto packed_iter = packed_filter.begin(); + while (mutable_iter != filter.end() && packed_iter != packed_filter.end()) { + EXPECT_EQ(*mutable_iter, *packed_iter) << BATT_INSPECT(seed); + ++mutable_iter; + ++packed_iter; } + EXPECT_EQ(mutable_iter, filter.end()) << BATT_INSPECT(seed); + EXPECT_EQ(packed_iter, packed_filter.end()) << BATT_INSPECT(seed); + } - Interval actual_range = filter.find_live_range(Interval{start_i, end_i}); - EXPECT_EQ(actual_range, expected_range) << BATT_INSPECT(seed); + // Verify queries produce the same results as the in-memory filter. + // + verify_filter_queries(packed_filter, live_items, num_items, seed, rng); + + // Converting packed back to in-memory should produce identical filter. + // + PiecewiseFilter converted_filter{packed_filter}; + EXPECT_TRUE(converted_filter.check_invariants()); + Slice> original_live = filter.live(); + Slice> converted_live = converted_filter.live(); + ASSERT_EQ(original_live.size(), converted_live.size()) << BATT_INSPECT(seed); + for (usize i = 0; i < original_live.size(); ++i) { + EXPECT_EQ(original_live[i], converted_live[i]) << BATT_INSPECT(seed) << BATT_INSPECT(i); } } } @@ -284,4 +285,88 @@ TEST(PiecewiseFilterTest, KeyQueryTest) EXPECT_TRUE(filter.check_invariants()); } } -} // namespace \ No newline at end of file + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +TEST(PiecewiseFilterTest, LiveSubranges) +{ + std::uniform_int_distribution pick_bound{0, 64}; + + const auto pick_interval = [&](auto& rng) { + Interval i{pick_bound(rng), pick_bound(rng)}; + if (i.upper_bound < i.lower_bound) { + std::swap(i.lower_bound, i.upper_bound); + } + return i; + }; + + std::array, 1> init_live{{{0, 64}}}; + + const usize n_seeds = 100000; + const usize n_drops = 32; + const usize n_queries = 15; + const usize first_seed = 0; + + PiecewiseFilter filter; + + const auto query_as_bits = [&](Interval query) { + u64 bits = 0; + filter.live_subranges_of(query) | batt::seq::for_each([&bits](const Interval& live) { + bits |= mask_from_interval(live); + }); + return bits; + }; + + for (usize seed_i = first_seed; seed_i < first_seed + n_seeds; ++seed_i) { + std::default_random_engine rng{seed_i}; + + for (usize i = 0; i < n_drops; ++i) { + BATT_DEBUG_INFO(BATT_INSPECT(i) << BATT_INSPECT(seed_i)); + + filter = BATT_OK_RESULT_OR_PANIC(PiecewiseFilter::from_live(batt::as_slice(init_live))); + u64 filter_state = ~u64{0}; + + std::vector> dropped_ranges = + drop_n_disjoint_intervals_from(&filter, i, init_live[0], rng).second; + + for (const Interval& drop_interval : dropped_ranges) { + const u64 drop_mask = mask_from_interval(drop_interval); + filter_state &= ~drop_mask; + } + + // Also test via PackedPiecewiseFilter. + // + PackedFilterData packed_data = pack_in_memory_filter(filter); + PackedPiecewiseFilter packed_filter = get_packed_filter_from_data(packed_data); + + const auto packed_query_as_bits = [&](Interval query) { + u64 bits = 0; + packed_filter.live_subranges_of(query) | + batt::seq::for_each([&bits](const Interval& live) { + bits |= mask_from_interval(live); + }); + return bits; + }; + + for (usize j = 0; j < n_queries; ++j) { + const Interval query_interval = pick_interval(rng); + const u64 query_mask = mask_from_interval(query_interval); + const u64 expected_bits = query_mask & filter_state; + const u64 actual_bits = query_as_bits(query_interval); + + ASSERT_EQ(std::bitset<64>{expected_bits}, std::bitset<64>{actual_bits}) + << BATT_INSPECT(seed_i) << BATT_INSPECT(query_interval) + << BATT_INSPECT(query_interval.size()) << BATT_INSPECT(std::bitset<64>{query_mask}); + + const u64 packed_actual_bits = packed_query_as_bits(query_interval); + + ASSERT_EQ(std::bitset<64>{expected_bits}, std::bitset<64>{packed_actual_bits}) + << "PackedPiecewiseFilter mismatch: " << BATT_INSPECT(seed_i) + << BATT_INSPECT(query_interval) << BATT_INSPECT(query_interval.size()) + << BATT_INSPECT(std::bitset<64>{query_mask}); + } + } + } +} + +} // namespace diff --git a/src/turtle_kv/util/piecewise_filter.test.hpp b/src/turtle_kv/util/piecewise_filter.test.hpp new file mode 100644 index 0000000..f4058e5 --- /dev/null +++ b/src/turtle_kv/util/piecewise_filter.test.hpp @@ -0,0 +1,247 @@ +//=##=##=#==#=#==#===#+==#+==========+==+=+=+=+=+=++=+++=+++++=-++++=-+++++++++++ +// +// Part of the TurtleKV Project, under Apache License v2.0. +// See https://www.apache.org/licenses/LICENSE-2.0 for license information. +// SPDX short identifier: Apache-2.0 +// +//+++++++++++-+-+--+----- --- -- - - - - + +#pragma once +#define TURTLE_KV_UTIL_PIECEWISE_FILTER_TEST_HPP + +#include "piecewise_filter.hpp" +#include "piecewise_filter_storage_model.concept.hpp" + +#include + +#include +#include + +#include +#include + +namespace turtle_kv { +namespace testing { + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - +// +/** \brief Randomly drops `n` ranges within the specified live range of the passed filter. + * + * Requires that: + * - `drop_within` must be live in `filter` + * - `drop_within.size()` must be large enough to fit `n` disjoint intervals + * + * \return a pair of { total offset size dropped, vector of the dropped intervals } + */ +template ModelT> +inline std::pair>> drop_n_disjoint_intervals_from( + BasicPiecewiseFilter* filter, + usize n, + const Interval& drop_within, + Rng& rng) +{ + constexpr bool debug = false; + + if constexpr (debug) { + std::cerr << BATT_INSPECT(n) << std::endl; + } + + OffsetT dropped_total_size = 0; + std::vector> dropped_ranges; + + if (n == 0) { + return std::make_pair(dropped_total_size, dropped_ranges); + } + + BATT_CHECK_LE(n * 2 - 1, drop_within.size()); + BATT_CHECK_EQ(filter->live().empty(), false); + BATT_CHECK_EQ(filter->find_live_range(drop_within), drop_within); + + usize drops_remaining = n; + OffsetT next_droppable = drop_within.lower_bound; + const OffsetT live_lower_bound = filter->live().front().lower_bound; + const OffsetT live_upper_bound = filter->live().back().upper_bound; + + BATT_DEBUG_INFO(BATT_INSPECT(dropped_total_size) + << BATT_INSPECT_RANGE(dropped_ranges) << BATT_INSPECT(drop_within) + << BATT_INSPECT(n) << BATT_INSPECT(drops_remaining) + << BATT_INSPECT(next_droppable) << BATT_INSPECT(live_lower_bound) + << BATT_INSPECT(live_upper_bound)); + + if constexpr (debug) { + std::cerr << BATT_INSPECT_RANGE(filter->live()) << std::endl; + } + + for (usize drop_i = 0; drop_i < n; ++drop_i) { + BATT_CHECK_GE(next_droppable, 0); + BATT_CHECK_LT(next_droppable, drop_within.upper_bound); + + std::uniform_int_distribution pick_lower_bound{ + next_droppable, + drop_within.upper_bound - (drops_remaining * 2 - 1), + }; + const OffsetT lower_bound_i = pick_lower_bound(rng); + + std::uniform_int_distribution pick_upper_bound{ + lower_bound_i + 1, + drop_within.upper_bound - (drops_remaining * 2 - 2), + }; + const OffsetT upper_bound_i = pick_upper_bound(rng); + + BATT_CHECK_LT(lower_bound_i, upper_bound_i); + BATT_CHECK_GE(lower_bound_i, next_droppable); + + dropped_total_size += upper_bound_i - lower_bound_i; + + const usize live_count_before = filter->live().size(); + //----- --- -- - - - - + dropped_ranges.push_back(Interval{lower_bound_i, upper_bound_i}); + if constexpr (debug) { + std::cerr << " dropping: " << lower_bound_i << ".." << upper_bound_i << std::endl; + } + filter->drop_index_range(Interval{lower_bound_i, upper_bound_i}); + //----- --- -- - - - - + const usize live_count_after = filter->live().size(); + + if constexpr (debug) { + std::cerr << BATT_INSPECT_RANGE(filter->live()) << std::endl; + } + + if (lower_bound_i == live_lower_bound && upper_bound_i == live_upper_bound) { + BATT_CHECK_EQ(live_count_after + 1, live_count_before); + + } else if ((lower_bound_i == live_lower_bound && upper_bound_i != live_upper_bound) || + (upper_bound_i == live_upper_bound && lower_bound_i != live_lower_bound)) { + BATT_CHECK_EQ(live_count_after, live_count_before); + + } else { + BATT_CHECK_EQ(live_count_after, live_count_before + 1); + } + + --drops_remaining; + next_droppable = upper_bound_i + 1; + } + + return std::make_pair(dropped_total_size, dropped_ranges); +} + +struct PackedFilterData { + std::vector values; + bool start_is_live; +}; + +inline PackedFilterData pack_in_memory_filter(const PiecewiseFilter& filter) +{ + PackedFilterData data; + data.start_is_live = false; + Slice> live = filter.live(); + + if (live.empty()) { + return data; + } + + data.start_is_live = (live[0].lower_bound == PiecewiseFilter::kMinLowerBound); + + for (const Interval& range : live) { + if (range.lower_bound != PiecewiseFilter::kMinLowerBound) { + data.values.push_back(range.lower_bound); + } + if (range.upper_bound != PiecewiseFilter::kMaxUpperBound) { + data.values.push_back(range.upper_bound); + } + } + + return data; +} + +inline PackedPiecewiseFilter get_packed_filter_from_data(const PackedFilterData& data) +{ + return PackedPiecewiseFilter{PackedPiecewiseFilterStorage{ + batt::as_const_slice(data.values), data.start_is_live}}; +} + +//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - - + +struct RandomDropResult { + PiecewiseFilter filter; + std::set live_items; +}; + +inline RandomDropResult build_filter_with_random_drops(u32 num_items, std::default_random_engine& rng) +{ + RandomDropResult result; + for (u32 i = 0; i < num_items; ++i) { + result.live_items.insert(i); + } + + std::uniform_int_distribution pick_num_dropped{100, num_items / 2}; + u32 num_intervals_dropped = pick_num_dropped(rng); + for (u32 i = 0; i < num_intervals_dropped; ++i) { + std::uniform_int_distribution pick_interval_start{0, num_items - 1}; + u32 start_i = pick_interval_start(rng); + + std::uniform_int_distribution pick_interval_end{start_i, num_items}; + u32 end_i = pick_interval_end(rng); + + for (u32 j = start_i; j < end_i; ++j) { + result.live_items.erase(j); + } + + result.filter.drop_index_range(Interval{start_i, end_i}); + } + + return result; +} + +template +inline void verify_filter_queries(const FilterT& filter, + const std::set& live_items, + u32 num_items, + u32 seed, + std::default_random_engine& rng) +{ + for (u32 i = 0; i < num_items; ++i) { + bool expected_live = live_items.count(i) > 0; + bool actual_live = filter.live_at_index(i); + EXPECT_EQ(actual_live, expected_live) << BATT_INSPECT(seed) << BATT_INSPECT(i); + } + + for (u32 i = 0; i < num_items; ++i) { + auto iter = live_items.lower_bound(i); + u32 expected = (iter != live_items.end()) ? *iter : num_items; + u32 actual = filter.live_lower_bound(i); + EXPECT_EQ(actual, expected) << BATT_INSPECT(seed) << BATT_INSPECT(i); + } + + for (u32 i = 0; i < 100; ++i) { + std::uniform_int_distribution pick_interval_start{0, num_items - 1}; + u32 start_i = pick_interval_start(rng); + + std::uniform_int_distribution pick_interval_end{start_i, num_items}; + u32 end_i = pick_interval_end(rng); + + auto iter = live_items.lower_bound(start_i); + Interval expected_range; + + if (iter == live_items.end() || *iter >= end_i) { + expected_range = Interval{end_i, end_i}; + } else { + u32 first = *iter; + u32 last = first + 1; + auto next = std::next(iter); + + while (next != live_items.end() && *next < end_i && *next == last) { + ++last; + ++next; + } + + expected_range = Interval{first, last}; + } + + Interval actual_range = filter.find_live_range(Interval{start_i, end_i}); + EXPECT_EQ(actual_range, expected_range) << BATT_INSPECT(seed) << BATT_INSPECT(i); + } +} + +} // namespace testing +} // namespace turtle_kv diff --git a/src/turtle_kv/util/piecewise_filter_storage_model.concept.hpp b/src/turtle_kv/util/piecewise_filter_storage_model.concept.hpp new file mode 100644 index 0000000..5a105df --- /dev/null +++ b/src/turtle_kv/util/piecewise_filter_storage_model.concept.hpp @@ -0,0 +1,59 @@ +//=##=##=#==#=#==#===#+==#+==========+==+=+=+=+=+=++=+++=+++++=-++++=-+++++++++++ +// +// Part of the TurtleKV Project, under Apache License v2.0. +// See https://www.apache.org/licenses/LICENSE-2.0 for license information. +// SPDX short identifier: Apache-2.0 +// +//+++++++++++-+-+--+----- --- -- - - - - + +#pragma once +#define TURTLE_KV_UTIL_PIECEWISE_FILTER_STORAGE_MODEL_CONCEPT_HPP + +#include +#include + +#include + +#include +#include +#include +#include + +namespace turtle_kv { + +template +concept PiecewiseFilterStorageModel = requires(const T& model, + T& src, + T& dst, + Interval interval, + usize i, + std::ostream& out) { + typename T::value_type; + typename T::iterator; + typename T::const_iterator; + + { model.begin() }; + { model.end() }; + { std::begin(model) } -> std::same_as; + { std::end(model) } -> std::same_as; + { as_const_slice(model) }; + { model.empty() } -> std::convertible_to; + { model.size() } -> std::convertible_to; + { *model.begin() } -> std::convertible_to&>; + { model[i] } -> std::convertible_to&>; + { dst = std::move(src) }; + { out << batt::dump_range(model) } -> std::same_as; +}; + +template +concept PiecewiseFilterMutableStorageModel = + PiecewiseFilterStorageModel && + requires(T model, T other, Interval interval, usize i, std::ostream& out) { + { model.clear() }; + { model.erase(model.end()) }; + { model.insert(model.end(), interval) }; + { model.insert(model.end(), model.begin(), model.end()) }; + { model[i] = interval }; + }; + +} // namespace turtle_kv