From e00d76ce86389bbe46cc24b2fe70c21fb309eb9a Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Mon, 19 Oct 2026 03:11:17 -0400 Subject: [PATCH 1/5] Make head sharing a per-instance construction option. --- include/bitcoin/database/impl/memory/mmap.ipp | 6 ++- .../database/impl/memory/mmap_private.ipp | 2 +- .../database/impl/memory/mmap_staging.ipp | 11 ++-- include/bitcoin/database/memory/mmap.hpp | 19 ++++--- test/memory/mmap.cpp | 51 +++++++++++++++++++ test/mocks/chunk_storage.hpp | 2 +- 6 files changed, 72 insertions(+), 19 deletions(-) diff --git a/include/bitcoin/database/impl/memory/mmap.ipp b/include/bitcoin/database/impl/memory/mmap.ipp index a22ed8186..a5c09eaa4 100644 --- a/include/bitcoin/database/impl/memory/mmap.ipp +++ b/include/bitcoin/database/impl/memory/mmap.ipp @@ -36,7 +36,7 @@ namespace database { // Unstaged aggregates transfer in full, and shared heads map their files. TEMPLATE CLASS::mmap(const path& filename, const storage_settings& settings, - bool random, bool staged) NOEXCEPT + bool random, bool staged, bool shared) NOEXCEPT requires (is_one(columns)) : filenames_{ filename }, minimum_(std::max(one, to_rows(settings.size))), @@ -46,7 +46,8 @@ CLASS::mmap(const path& filename, const storage_settings& settings, zeroed_(settings.zeroed), random_(random), staged_(staged), - managed_(!staged && !head_shared), + head_shared_(!staged && shared), + managed_(!staged && !shared), opened_{ file::invalid } { } @@ -63,6 +64,7 @@ CLASS::mmap(const paths& filenames, const storage_settings& settings, zeroed_(settings.zeroed), random_(random), staged_(staged), + head_shared_(false), managed_(false), opened_{} { diff --git a/include/bitcoin/database/impl/memory/mmap_private.ipp b/include/bitcoin/database/impl/memory/mmap_private.ipp index ea6781b5b..1f3e7f2ae 100644 --- a/include/bitcoin/database/impl/memory/mmap_private.ipp +++ b/include/bitcoin/database/impl/memory/mmap_private.ipp @@ -295,7 +295,7 @@ void CLASS::worker_start_() NOEXCEPT { #if defined(MANAGE_STAGING) // A shared head has no lazy writer (the kernel writes its mapping back). - if (!staged_ && head_shared) + if (head_shared_) return; limit_ = system_memory() / throttle_factor; diff --git a/include/bitcoin/database/impl/memory/mmap_staging.ipp b/include/bitcoin/database/impl/memory/mmap_staging.ipp index db4b5f5b6..82475bb27 100644 --- a/include/bitcoin/database/impl/memory/mmap_staging.ipp +++ b/include/bitcoin/database/impl/memory/mmap_staging.ipp @@ -58,8 +58,9 @@ bool CLASS::persist_(size_t to) NOEXCEPT const auto from = to_width(settled_.load()); return staged_ ? ((from >= to) || pwrite_all(opened_[Column], std::next(memory_map_[Column], from), to - from, from)) : - (head_shared || shared_.load()) ? - (::msync(memory_map_[Column], to, MS_SYNC) != fail) : + (head_shared_ || shared_.load()) ? + (is_zero(to) || + (::msync(memory_map_[Column], to, MS_SYNC) != fail)) : transfer_(to); } @@ -194,7 +195,7 @@ bool CLASS::stage_() NOEXCEPT // A shared head maps its file in place of commitment (content is the // mapping, so there is nothing to populate). - if (!staged_ && head_shared) + if (head_shared_) { if (mmap_share(memory_map_[Column], target, opened_[Column], zero) == fail) @@ -281,7 +282,7 @@ bool CLASS::commit_(size_t size, bool final) NOEXCEPT { // A shared head extends its file mapping over the growth (the file is // provisioned to capacity, so the extension is already allocated). - if (!staged_ && (head_shared || shared_.load())) + if (head_shared_ || (!staged_ && shared_.load())) { const auto grown = page_floor(to_width(capacity_.load())); @@ -340,7 +341,7 @@ bool CLASS::commit_(size_t size, bool final) NOEXCEPT // A shared head remaps its file onto the replacement (the file is the // content, so migration copies nothing). - if (!staged_ && (head_shared || shared_.load())) + if (head_shared_ || (!staged_ && shared_.load())) { if (mmap_share(replace, target, opened_[Column], zero) == fail) { diff --git a/include/bitcoin/database/memory/mmap.hpp b/include/bitcoin/database/memory/mmap.hpp index 901e24d2f..9a97b496a 100644 --- a/include/bitcoin/database/memory/mmap.hpp +++ b/include/bitcoin/database/memory/mmap.hpp @@ -76,7 +76,7 @@ class mmap /// Scalar construction (columns == 1). mmap(const path& filename, const storage_settings& settings={}, - bool random=true, bool staged=false) NOEXCEPT + bool random=true, bool staged=false, bool shared=false) NOEXCEPT requires (is_one(columns)); /// Aggregate construction (columns > 1): one file per column, shared guards. @@ -294,15 +294,6 @@ class mmap static constexpr bool head_release = false; #endif - // Map heads writable-shared from their files (the native windows model) - // instead of anonymously with a dirty-page writer. Head reclaim is then - // kernel writeback of a bounded rewrite-in-place mapping (clean pages - // drop) rather than swap, which anonymous pages alone require. Bodies - // remain staged, so the unbounded append writeback that motivates dirty - // ratio tuning does not return with it. Excludes head_release (nothing - // to release) and the dirty bitmap (nothing to transfer). - static constexpr bool head_shared = false; - // column dispatch, not thread safe. template bool flush_all_(size_t rows, std::index_sequence) NOEXCEPT; @@ -433,6 +424,14 @@ class mmap const bool zeroed_; const bool random_; const bool staged_; + + // Map the head writable-shared from its file (the native windows model) + // instead of anonymously with a dirty-page writer. Head reclaim is then + // kernel writeback of a bounded rewrite-in-place mapping (clean pages + // drop) rather than swap, which anonymous pages alone require. Excludes + // head_release (nothing to release) and the dirty bitmap (nothing to + // transfer). + const bool head_shared_; const bool managed_; // These are thread safe (atomic). diff --git a/test/memory/mmap.cpp b/test/memory/mmap.cpp index 28edec77e..ae9680114 100644 --- a/test/memory/mmap.cpp +++ b/test/memory/mmap.cpp @@ -1313,6 +1313,57 @@ BOOST_AUTO_TEST_CASE(mmap__unstaged__rewrite_below_flush__expected) BOOST_REQUIRE(!reopened.get_fault()); } +BOOST_AUTO_TEST_CASE(mmap__shared__rewrite_below_flush__expected) +{ + constexpr size_t size = 10'000; + constexpr size_t rewrite = 100; + const std::string file = TEST_PATH; + BOOST_REQUIRE(test::create(file)); + + // Shared head writes through its file mapping (no mark or transfer). + map instance(file, { 1, 50 }, true, false, true); + BOOST_REQUIRE(!instance.open()); + BOOST_REQUIRE(!instance.load()); + + const auto original = stage_vector(stage_pattern, size); + const auto replaced = stage_vector(stage_repattern, rewrite); + + auto expected = stage_vector(stage_repattern, rewrite); + const auto retained = stage_vector(stage_pattern, size - rewrite, rewrite); + expected.insert(expected.end(), retained.begin(), retained.end()); + + auto memory = instance.get(instance.allocate(size)); + BOOST_REQUIRE(memory); + std::copy_n(original.begin(), original.size(), memory.begin()); + memory.reset(); + BOOST_REQUIRE(!instance.flush()); + + memory = instance.get(); + BOOST_REQUIRE(memory); + std::copy_n(replaced.begin(), replaced.size(), memory.begin()); + memory.reset(); + BOOST_REQUIRE(!instance.flush()); + BOOST_REQUIRE(!instance.unload()); + BOOST_REQUIRE(!instance.close()); + BOOST_REQUIRE(!instance.get_fault()); + + map reopened(file, { 1, 50 }, true, false, true); + BOOST_REQUIRE(!reopened.open()); + BOOST_REQUIRE(!reopened.load()); + BOOST_REQUIRE_EQUAL(reopened.size(), size); + + memory = reopened.get(); + BOOST_REQUIRE(memory); + + const auto end = std::next(memory.begin(), expected.size()); + BOOST_REQUIRE_EQUAL_COLLECTIONS(memory.begin(), end, expected.begin(), expected.end()); + + memory.reset(); + BOOST_REQUIRE(!reopened.unload()); + BOOST_REQUIRE(!reopened.close()); + BOOST_REQUIRE(!reopened.get_fault()); +} + BOOST_AUTO_TEST_CASE(mmap__unload__shared__unload_locked) { const std::string file = TEST_PATH; diff --git a/test/mocks/chunk_storage.hpp b/test/mocks/chunk_storage.hpp index 67b2b446d..1d323f8f8 100644 --- a/test/mocks/chunk_storage.hpp +++ b/test/mocks/chunk_storage.hpp @@ -72,7 +72,7 @@ class chunk_storages chunk_storages(const path& filename, const database::storage_settings& ={}, bool=true, - bool=false) NOEXCEPT requires (is_one(columns)) + bool=false, bool=false) NOEXCEPT requires (is_one(columns)) : alias_{}, paths_{ filename }, logical_{} { } From e23dd83ff14309ca329865bf6cbf03a875cb963e Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Mon, 19 Oct 2026 03:11:17 -0400 Subject: [PATCH 2/5] Map the strong head shared from its file. --- include/bitcoin/database/impl/store/store.ipp | 2 +- include/bitcoin/database/store.hpp | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/include/bitcoin/database/impl/store/store.ipp b/include/bitcoin/database/impl/store/store.ipp index 9fddf8aee..8e1c38fd1 100644 --- a/include/bitcoin/database/impl/store/store.ipp +++ b/include/bitcoin/database/impl/store/store.ipp @@ -73,7 +73,7 @@ CLASS::store(const settings& config) NOEXCEPT candidate_head_(head(config.path / schema::dir::heads, schema::indexes::candidate), head_settings(config.candidate), sequential), confirmed_head_(head(config.path / schema::dir::heads, schema::indexes::confirmed), head_settings(config.confirmed), sequential), - strong_head_(head(config.path / schema::dir::heads, schema::indexes::strong), head_settings(config.strong), random), + strong_head_(head(config.path / schema::dir::heads, schema::indexes::strong), head_settings(config.strong), random, !staged, shared), scan_head_(head(config.path / schema::dir::heads, schema::indexes::scan), head_settings(config.scan), random), scan_body_(body(config.path, schema::indexes::scan), config.scan, sequential, staged), scan_prefix_head_(head(config.path / schema::dir::heads, schema::indexes::scan_prefix), head_settings(config.scan), sequential), diff --git a/include/bitcoin/database/store.hpp b/include/bitcoin/database/store.hpp index 14fc984d6..b3fda43bc 100644 --- a/include/bitcoin/database/store.hpp +++ b/include/bitcoin/database/store.hpp @@ -337,6 +337,10 @@ class store // staging backend is built (heads update in place and remain resident). static constexpr bool staged = true; + // A head appended in link order goes cold behind its frontier, so it + // maps its file (droppable pages) rather than holding its content in RAM. + static constexpr bool shared = true; + static inline path head(const path& folder, const std::string& name) NOEXCEPT { return folder / (name + schema::ext::head); From 4f18725066b07a1dfee741d7ff1f197b24133d31 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Mon, 19 Oct 2026 13:12:30 -0400 Subject: [PATCH 3/5] Contest memory at 8GB on all platforms. --- include/bitcoin/database/impl/primitives/hashhead.ipp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/include/bitcoin/database/impl/primitives/hashhead.ipp b/include/bitcoin/database/impl/primitives/hashhead.ipp index 6b5f012c0..b8042e762 100644 --- a/include/bitcoin/database/impl/primitives/hashhead.ipp +++ b/include/bitcoin/database/impl/primitives/hashhead.ipp @@ -139,11 +139,7 @@ uint32_t CLASS::derive_buckets(uint64_t expected, uint32_t low, constexpr auto gigabyte = power2(30u); constexpr auto megabyte = power2(20u); constexpr auto uncontested = power2(35u); -#if defined(HAVE_APPLE) - constexpr auto contested = 10u * gigabyte; -#else constexpr auto contested = 8u * gigabyte; -#endif if (is_zero(expected) || is_zero(low) || is_zero(high)) return {}; From 8fdda49d0af492e133126b66b8586541d123144b Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Mon, 19 Oct 2026 13:40:11 -0400 Subject: [PATCH 4/5] Advise the strong head sequential. --- include/bitcoin/database/impl/store/store.ipp | 2 +- include/bitcoin/database/store.hpp | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/include/bitcoin/database/impl/store/store.ipp b/include/bitcoin/database/impl/store/store.ipp index 8e1c38fd1..7c5af774b 100644 --- a/include/bitcoin/database/impl/store/store.ipp +++ b/include/bitcoin/database/impl/store/store.ipp @@ -73,7 +73,7 @@ CLASS::store(const settings& config) NOEXCEPT candidate_head_(head(config.path / schema::dir::heads, schema::indexes::candidate), head_settings(config.candidate), sequential), confirmed_head_(head(config.path / schema::dir::heads, schema::indexes::confirmed), head_settings(config.confirmed), sequential), - strong_head_(head(config.path / schema::dir::heads, schema::indexes::strong), head_settings(config.strong), random, !staged, shared), + strong_head_(head(config.path / schema::dir::heads, schema::indexes::strong), head_settings(config.strong, advice::sequential), sequential, !staged, shared), scan_head_(head(config.path / schema::dir::heads, schema::indexes::scan), head_settings(config.scan), random), scan_body_(body(config.path, schema::indexes::scan), config.scan, sequential, staged), scan_prefix_head_(head(config.path / schema::dir::heads, schema::indexes::scan_prefix), head_settings(config.scan), sequential), diff --git a/include/bitcoin/database/store.hpp b/include/bitcoin/database/store.hpp index b3fda43bc..f132892aa 100644 --- a/include/bitcoin/database/store.hpp +++ b/include/bitcoin/database/store.hpp @@ -314,23 +314,25 @@ class store // Heads share the table growth rate. static constexpr storage_settings head_settings( - const settings::simple_table& table, uint64_t minimum=one) NOEXCEPT + const settings::simple_table& table, uint64_t minimum=one, + advice access=advice::random) NOEXCEPT { return { .size = std::max(one, minimum), .rate = table.rate, - .access = advice::random, + .access = access, .zeroed = false }; } template static constexpr storage_settings head_settings( - const settings::bucket_table& table) NOEXCEPT + const settings::bucket_table& table, + advice access=advice::random) NOEXCEPT { return head_settings(table, - system::ceilinged_multiply(table.buckets, Cell)); + system::ceilinged_multiply(table.buckets, Cell), access); } // Bodies are append-only, so stage writes in anonymous memory where the From 1f14c008033fe0d531e720700484426979d692a7 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Mon, 19 Oct 2026 14:14:35 -0400 Subject: [PATCH 5/5] Drop narrative comment from head sharing member. --- include/bitcoin/database/memory/mmap.hpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/include/bitcoin/database/memory/mmap.hpp b/include/bitcoin/database/memory/mmap.hpp index 9a97b496a..345bf961e 100644 --- a/include/bitcoin/database/memory/mmap.hpp +++ b/include/bitcoin/database/memory/mmap.hpp @@ -424,13 +424,6 @@ class mmap const bool zeroed_; const bool random_; const bool staged_; - - // Map the head writable-shared from its file (the native windows model) - // instead of anonymously with a dirty-page writer. Head reclaim is then - // kernel writeback of a bounded rewrite-in-place mapping (clean pages - // drop) rather than swap, which anonymous pages alone require. Excludes - // head_release (nothing to release) and the dirty bitmap (nothing to - // transfer). const bool head_shared_; const bool managed_;