From dc042f6b6f7295df8691fdcba0477d1e183197bf Mon Sep 17 00:00:00 2001 From: wvpm <24685035+wvpm@users.noreply.github.com> Date: Sun, 17 May 2026 15:09:55 +0200 Subject: [PATCH] Minimise allocations for ModifierSum --- .../core/BulkInsertWrapper.hpp | 201 ++++++++++++++++++ .../country/CountryInstance.cpp | 4 + .../country/CountryInstance.hpp | 1 + src/openvic-simulation/map/MapInstance.cpp | 4 + .../map/ProvinceInstance.cpp | 6 + .../map/ProvinceInstance.hpp | 1 + .../modifier/ModifierSum.cpp | 38 ++-- .../modifier/ModifierSum.hpp | 71 +++++-- tests/src/SpyAllocator.hpp | 61 ++++++ tests/src/core/BulkInsertWrapper.cpp | 162 ++++++++++++++ 10 files changed, 512 insertions(+), 37 deletions(-) create mode 100644 src/openvic-simulation/core/BulkInsertWrapper.hpp create mode 100644 tests/src/SpyAllocator.hpp create mode 100644 tests/src/core/BulkInsertWrapper.cpp diff --git a/src/openvic-simulation/core/BulkInsertWrapper.hpp b/src/openvic-simulation/core/BulkInsertWrapper.hpp new file mode 100644 index 000000000..cd31e8bdf --- /dev/null +++ b/src/openvic-simulation/core/BulkInsertWrapper.hpp @@ -0,0 +1,201 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "openvic-simulation/core/Assert.hpp" + +namespace OpenVic { + // not thread safe + template + struct bulk_insert_wrapper { + public: + // Member types based on std::vector + using container_type = Container; + using value_type = typename container_type::value_type; + using allocator_type = typename container_type::allocator_type; + using size_type = typename container_type::size_type; + using difference_type = typename container_type::difference_type; + using reference = typename container_type::reference; + using const_reference = typename container_type::const_reference; + using pointer = typename container_type::pointer; + using const_pointer = typename container_type::const_pointer; + using iterator = typename container_type::iterator; + using const_iterator = typename container_type::const_iterator; + using reverse_iterator = typename container_type::reverse_iterator; + using const_reverse_iterator = typename container_type::const_reverse_iterator; + + static_assert(std::is_default_constructible_v); + static_assert(std::is_trivially_destructible_v); + + private: + Container container; + std::atomic pending_extra_size {}; + + constexpr void flush_pending_room() { + if (pending_extra_size > size_type{}) { + size_type valid_size { size() }; + container.resize(valid_size + pending_extra_size); + container.resize(valid_size); + pending_extra_size = size_type{}; + } + } + + public: + constexpr allocator_type get_allocator() const { + return container.get_allocator(); + } + + constexpr bulk_insert_wrapper() noexcept {}; + + // Forwarding constructor for custom allocators or initial capacities + template + constexpr explicit bulk_insert_wrapper(Args&&... args) + : container(std::forward(args)...) {} + + // thread safe + constexpr void make_room_for(const size_type count) noexcept { + pending_extra_size += count; + } + + // Element access based on std::vector + constexpr reference operator[](const size_type pos) { + OV_HARDEN_ASSERT_ACCESS(pos, "operator[]"); + return container[pos]; + } + constexpr const_reference operator[](const size_type pos) const { + OV_HARDEN_ASSERT_ACCESS(pos, "operator[]"); + return container[pos]; + } + + constexpr reference front() { + OV_HARDEN_ASSERT_NONEMPTY("front"); + return container[0]; + } + constexpr const_reference front() const { + OV_HARDEN_ASSERT_NONEMPTY("front"); + return container[0]; + } + + constexpr reference back() { + OV_HARDEN_ASSERT_NONEMPTY("back"); + return container[size()-1]; + } + constexpr const_reference back() const { + OV_HARDEN_ASSERT_NONEMPTY("back"); + return container[size()-1]; + } + + constexpr value_type* data() noexcept { return container.data(); } + constexpr value_type const* data() const noexcept { return container.data(); } + + // Iterators based on std::vector + constexpr iterator begin() noexcept { + return container.begin(); + } + constexpr const_iterator begin() const noexcept { + return container.begin(); + } + constexpr const_iterator cbegin() const noexcept { + return container.cbegin(); + } + + constexpr iterator end() noexcept { + return container.end(); + } + constexpr const_iterator end() const noexcept { + return container.end(); + } + constexpr const_iterator cend() const noexcept { + return container.cend(); + } + + constexpr reverse_iterator rbegin() noexcept { + return container.rbegin(); + } + constexpr const_reverse_iterator rbegin() const noexcept { + return container.rbegin(); + } + constexpr const_reverse_iterator crbegin() const noexcept { + return container.crbegin(); + } + + constexpr reverse_iterator rend() noexcept { + return container.rend(); + } + constexpr const_reverse_iterator rend() const noexcept { + return container.rend(); + } + constexpr const_reverse_iterator crend() const noexcept { + return container.crend(); + } + + // Capacity based on std::vector + constexpr bool empty() const noexcept { return size() <= size_type{}; } + constexpr size_type size() const noexcept { return container.size(); } + constexpr size_type max_size() const noexcept { return container.max_size(); } + // reserve() is omitted as we manage that via make_room_for + constexpr size_type capacity() const noexcept { return container.capacity(); } + constexpr void shrink_to_fit() { + pending_extra_size = size_type{}; + container.shrink_to_fit(); + } + + // Modifiers based on std::vector + constexpr void clear() noexcept { + pending_extra_size = size_type{}; + container.clear(); + } + + // the following could be implemented: + // - insert + // - insert_range + // - emplace + // - erase + // - append_range (C++23) + // - pop_back + // - swap + + constexpr void push_back(value_type const& value) { + flush_pending_room(); + container.push_back(value); + + } + constexpr void push_back(value_type&& value) { + flush_pending_room(); + container.push_back(std::move(value)); + } + + template + requires std::is_trivially_destructible_v + constexpr reference emplace_back(Args&&... args) { + return container.emplace_back(std::forward(args)...); + } + + template + constexpr void append_range(OtherContainerT const& other) { + append_range(other.begin(), other.end()); + } + + template + constexpr void append_range(const InputIt first, const InputIt last) { + flush_pending_room(); + + const size_type new_valid_size = size() + std::distance(first, last); + if (new_valid_size > container.capacity()) { + assert(!"append_range called without make_room_for"); + container.reserve(new_valid_size); + } + + std::uninitialized_copy(first, last, end()); + container.resize(new_valid_size); + } + + // resize() is omitted as we manage that via make_room_for + }; +} \ No newline at end of file diff --git a/src/openvic-simulation/country/CountryInstance.cpp b/src/openvic-simulation/country/CountryInstance.cpp index 14bfe7a3d..3978b10f7 100644 --- a/src/openvic-simulation/country/CountryInstance.cpp +++ b/src/openvic-simulation/country/CountryInstance.cpp @@ -1856,6 +1856,10 @@ void CountryInstance::update_modifier_sum(Date today, StaticModifierCache const& // TODO - calculate stats for each unit type (locked and unlocked) } +void CountryInstance::make_room_for_province_modifier_sum(ModifierSum const& province_modifier_sum) { + modifier_sum.make_room_for(province_modifier_sum); +} + void CountryInstance::contribute_province_modifier_sum(ModifierSum const& province_modifier_sum) { modifier_sum.add_modifier_sum(province_modifier_sum); } diff --git a/src/openvic-simulation/country/CountryInstance.hpp b/src/openvic-simulation/country/CountryInstance.hpp index c9404f043..f2c0fc6fe 100644 --- a/src/openvic-simulation/country/CountryInstance.hpp +++ b/src/openvic-simulation/country/CountryInstance.hpp @@ -686,6 +686,7 @@ namespace OpenVic { public: void update_modifier_sum(Date today, StaticModifierCache const& static_modifier_cache); + void make_room_for_province_modifier_sum(ModifierSum const& province_modifier_sum); void contribute_province_modifier_sum(ModifierSum const& province_modifier_sum); fixed_point_t get_modifier_effect_value(ModifierEffect const& effect) const; constexpr void for_each_contributing_modifier( diff --git a/src/openvic-simulation/map/MapInstance.cpp b/src/openvic-simulation/map/MapInstance.cpp index 4b8cc6286..5a9b50270 100644 --- a/src/openvic-simulation/map/MapInstance.cpp +++ b/src/openvic-simulation/map/MapInstance.cpp @@ -149,6 +149,10 @@ void MapInstance::update_modifier_sums(const Date today, StaticModifierCache con for (ProvinceInstance& province : get_province_instances()) { province.update_modifier_sum(today, static_modifier_cache); } + + for (ProvinceInstance& province : get_province_instances()) { + province.update_country_modifier_sum(); + } } void MapInstance::update_gamestate(InstanceManager const& instance_manager) { diff --git a/src/openvic-simulation/map/ProvinceInstance.cpp b/src/openvic-simulation/map/ProvinceInstance.cpp index b10a92072..aa058883b 100644 --- a/src/openvic-simulation/map/ProvinceInstance.cpp +++ b/src/openvic-simulation/map/ProvinceInstance.cpp @@ -283,6 +283,12 @@ void ProvinceInstance::update_modifier_sum(Date today, StaticModifierCache const modifier_sum.add_modifier(*crime); } + if (controller != nullptr) { + controller->make_room_for_province_modifier_sum(modifier_sum); + } +} + +void ProvinceInstance::update_country_modifier_sum() { if (controller != nullptr) { controller->contribute_province_modifier_sum(modifier_sum); } diff --git a/src/openvic-simulation/map/ProvinceInstance.hpp b/src/openvic-simulation/map/ProvinceInstance.hpp index 7e6038a60..e9755d686 100644 --- a/src/openvic-simulation/map/ProvinceInstance.hpp +++ b/src/openvic-simulation/map/ProvinceInstance.hpp @@ -190,6 +190,7 @@ namespace OpenVic { size_t get_pop_count() const; void update_modifier_sum(Date today, StaticModifierCache const& static_modifier_cache); + void update_country_modifier_sum(); fixed_point_t get_modifier_effect_value(ModifierEffect const& effect) const; void for_each_contributing_modifier(ModifierEffect const& effect, ContributingModifierCallback auto callback) const { diff --git a/src/openvic-simulation/modifier/ModifierSum.cpp b/src/openvic-simulation/modifier/ModifierSum.cpp index 1c4daba47..442a4e8e7 100644 --- a/src/openvic-simulation/modifier/ModifierSum.cpp +++ b/src/openvic-simulation/modifier/ModifierSum.cpp @@ -1,17 +1,20 @@ #include "ModifierSum.hpp" -#include "openvic-simulation/modifier/Modifier.hpp" +#include -#include "openvic-simulation/country/CountryInstance.hpp" -#include "openvic-simulation/map/ProvinceInstance.hpp" #include "openvic-simulation/core/template/Concepts.hpp" +#include "openvic-simulation/country/CountryInstance.hpp" // IWYU pragma: keep for modifier_source_t +#include "openvic-simulation/map/ProvinceInstance.hpp" // IWYU pragma: keep for modifier_source_t +#include "openvic-simulation/modifier/Modifier.hpp" using namespace OpenVic; std::string_view modifier_entry_t::source_to_string(modifier_source_t const& source) { return std::visit( [](has_get_identifier auto const* has_identifier) -> std::string_view { - return has_identifier->get_identifier(); + return has_identifier == nullptr + ? "" + : has_identifier->get_identifier(); }, source ); @@ -20,7 +23,9 @@ std::string_view modifier_entry_t::source_to_string(modifier_source_t const& sou memory::string modifier_entry_t::to_string() const { return memory::fmt::format( "[{}, {}, {}, {}]", - modifier, multiplier, source_to_string(source), + ovfmt::validate(modifier), + multiplier, + source_to_string(source), ModifierEffect::target_to_string(excluded_targets) ); } @@ -30,10 +35,6 @@ void ModifierSum::clear() { value_sum.clear(); } -bool ModifierSum::empty() { - return modifiers.empty(); -} - fixed_point_t ModifierSum::get_modifier_effect_value(ModifierEffect const& effect, bool* effect_found) const { return value_sum.get_effect(effect, effect_found); } @@ -55,21 +56,10 @@ void ModifierSum::add_modifier( modifier_entry_t::source_or_null_fallback(source, this_source), excluded_targets | this_excluded_targets ); - value_sum.multiply_add_exclude_targets(new_entry.modifier, new_entry.multiplier, new_entry.excluded_targets); - } -} - -void ModifierSum::add_modifier_sum(ModifierSum const& modifier_sum) { - reserve_more(modifiers, modifier_sum.modifiers.size()); - - // We could test that excluded_targets != ALL_TARGETS, but in practice it's always - // called with an explcit/hardcoded value and so won't ever exclude everything. - for (modifier_entry_t const& modifier_entry : modifier_sum.modifiers) { - add_modifier( - modifier_entry.modifier, - modifier_entry.multiplier, - modifier_entry.source, - modifier_entry.excluded_targets + value_sum.multiply_add_exclude_targets( + *new_entry.modifier, + new_entry.multiplier, + new_entry.excluded_targets ); } } diff --git a/src/openvic-simulation/modifier/ModifierSum.hpp b/src/openvic-simulation/modifier/ModifierSum.hpp index 72b2459c2..4be964469 100644 --- a/src/openvic-simulation/modifier/ModifierSum.hpp +++ b/src/openvic-simulation/modifier/ModifierSum.hpp @@ -1,8 +1,11 @@ #pragma once +#include #include +#include "openvic-simulation/core/BulkInsertWrapper.hpp" #include "openvic-simulation/core/memory/Vector.hpp" +#include "openvic-simulation/core/portable/ForwardableSpan.hpp" #include "openvic-simulation/dataloader/NodeTools.hpp" #include "openvic-simulation/modifier/ModifierValue.hpp" #include "openvic-simulation/modifier/Modifier.hpp" @@ -31,23 +34,40 @@ namespace OpenVic { ); } - Modifier const& modifier; + Modifier const* modifier; fixed_point_t multiplier; modifier_source_t source; ModifierEffect::target_t excluded_targets; + //invalid but required fore resizing to work + constexpr modifier_entry_t() + : modifier {nullptr}, + multiplier {fixed_point_t::_0}, + source {static_cast>(nullptr)}, + excluded_targets {} {} + + // constexpr modifier_entry_t( Modifier const& new_modifier, fixed_point_t new_multiplier, modifier_source_t const& new_source, ModifierEffect::target_t new_excluded_targets - ) : modifier { new_modifier }, + ) : modifier { &new_modifier }, multiplier { new_multiplier }, source { new_source }, excluded_targets { new_excluded_targets } {} + constexpr bool is_valid() const { + return multiplier != fixed_point_t::_0 + && modifier != nullptr + && ( + get_source_country() != nullptr + || get_source_province() != nullptr + ); + } + constexpr bool operator==(modifier_entry_t const& other) const { - return &modifier == &other.modifier + return modifier == other.modifier && multiplier == other.multiplier && source == other.source && excluded_targets == other.excluded_targets; @@ -67,14 +87,18 @@ namespace OpenVic { constexpr fixed_point_t get_modifier_effect_value( ModifierEffect const& effect, bool* effect_found = nullptr ) const { + if (!is_valid()) { + return fixed_point_t::_0; + } + if (ModifierEffect::excludes_targets(effect.targets, excluded_targets)) { - return modifier.get_effect(effect, effect_found) * multiplier; + return modifier->get_effect(effect, effect_found) * multiplier; } if (effect_found != nullptr) { *effect_found = false; } - return 0; + return fixed_point_t::_0; } }; @@ -94,18 +118,22 @@ namespace OpenVic { // Targets to be excluded from all modifiers added to the sum, combined with any explicit exclusions. ModifierEffect::target_t PROPERTY_RW(this_excluded_targets, ModifierEffect::target_t::NO_TARGETS); - memory::vector SPAN_PROPERTY(modifiers); + bulk_insert_wrapper< + memory::vector + > SPAN_PROPERTY(modifiers); ModifierValue PROPERTY(value_sum); public: ModifierSum() {}; - ModifierSum(ModifierSum const&) = default; ModifierSum(ModifierSum&&) = default; - ModifierSum& operator=(ModifierSum const&) = default; - ModifierSum& operator=(ModifierSum&&) = default; + constexpr std::size_t size() const { + return modifiers.size(); + } + constexpr bool empty() const { + return modifiers.empty(); + } void clear(); - bool empty(); fixed_point_t get_modifier_effect_value(ModifierEffect const& effect, bool* effect_found = nullptr) const; bool has_modifier_effect(ModifierEffect const& effect) const; @@ -116,17 +144,34 @@ namespace OpenVic { modifier_entry_t::modifier_source_t const& source = {}, ModifierEffect::target_t excluded_targets = ModifierEffect::target_t::NO_TARGETS ); - // Reserves space for the number of modifier entries in the given sum and adds each of them using add_modifier + + constexpr void make_room_for(ModifierSum const& modifier_sum) { + modifiers.make_room_for(modifier_sum.size()); + } + + // Inserts modifiers directly via std::ranges::copy. Requires resizing beforehand! // with the modifier entries' attributes as arguments. This means non-null sources are preserved (null ones are // replaced with this_source, but in practice the other sum should've set them itself already) and exclusion targets // are combined with this_excluded_targets. - void add_modifier_sum(ModifierSum const& modifier_sum); + constexpr void add_modifier_sum(ModifierSum const& modifier_sum) { + // We could test that excluded_targets != ALL_TARGETS, but in practice it's always + // called with an explcit/hardcoded value and so won't ever exclude everything. + modifiers.append_range(modifier_sum.modifiers); + + for (modifier_entry_t const& m : modifier_sum.get_modifiers()) { + value_sum.multiply_add_exclude_targets( + *m.modifier, + m.multiplier, + m.excluded_targets + ); + } + } // TODO - help calculate value_sum[effect]? Early return if lookup in value_sum fails? constexpr void for_each_contributing_modifier( ModifierEffect const& effect, ContributingModifierCallback auto callback ) const { - for (modifier_entry_t const& modifier_entry : modifiers) { + for (modifier_entry_t const& modifier_entry : get_modifiers()) { const fixed_point_t contribution = modifier_entry.get_modifier_effect_value(effect); if (contribution != 0) { diff --git a/tests/src/SpyAllocator.hpp b/tests/src/SpyAllocator.hpp new file mode 100644 index 000000000..22a1d21e0 --- /dev/null +++ b/tests/src/SpyAllocator.hpp @@ -0,0 +1,61 @@ +#include +#include + +// Telemetry structure remains the same +struct AllocationMetrics { + std::size_t total_allocated_bytes = 0; + std::size_t total_deallocated_bytes = 0; + std::size_t allocation_count = 0; + std::size_t deallocation_count = 0; + + void reset() { + total_allocated_bytes = 0; + total_deallocated_bytes = 0; + allocation_count = 0; + deallocation_count = 0; + } + + std::size_t active_bytes() const { + return total_allocated_bytes - total_deallocated_bytes; + } +}; + +// SpyAllocator now wraps an underlying Allocator type (defaults to std::allocator) +template > +class SpyAllocator { +public: + using value_type = T; + + // Shared telemetry state across allocator copies + std::shared_ptr metrics; + // The actual allocator doing the heavy lifting + Allocator upstream; + + // Default Constructor + SpyAllocator() + : metrics(std::make_shared()), upstream() {} + + // Explicitly pass an existing upstream allocator instance if needed + explicit SpyAllocator(const Allocator& alloc) + : metrics(std::make_shared()), upstream(alloc) {} + + value_type* allocate(const std::size_t n) { + // Delegate allocation to the upstream allocator + value_type* ptr = std::allocator_traits::allocate(upstream, n); + + if (metrics && ptr) { + metrics->total_allocated_bytes += (n * sizeof(value_type)); + metrics->allocation_count++; + } + return ptr; + } + + void deallocate(value_type* const ptr, const std::size_t n) noexcept { + if (metrics) { + metrics->total_deallocated_bytes += (n * sizeof(value_type)); + metrics->deallocation_count++; + } + // Delegate deallocation to the upstream allocator + std::allocator_traits::deallocate(upstream, ptr, n); + } +}; \ No newline at end of file diff --git a/tests/src/core/BulkInsertWrapper.cpp b/tests/src/core/BulkInsertWrapper.cpp new file mode 100644 index 000000000..7b2301ce6 --- /dev/null +++ b/tests/src/core/BulkInsertWrapper.cpp @@ -0,0 +1,162 @@ +#include +#include +#include + +#include "openvic-simulation/core/BulkInsertWrapper.hpp" + +#include + +#include "SpyAllocator.hpp" + +// A simple non-trivially destructible type to test constraint violations if needed, +// and a trivial one to satisfy emplace_back's requires clause. +struct TrivialPoint { + int x = 0; + int y = 0; +}; + +using namespace OpenVic; + +TEST_CASE("bulk_insert_wrapper Constructors", "[bulk_insert_wrapper][bulk_insert_wrapper-constructor]") { + constexpr bulk_insert_wrapper> empty; + CONSTEXPR_CHECK(empty.empty()); + CONSTEXPR_CHECK(empty.size() == 0); + CONSTEXPR_CHECK(empty.capacity() == 0); + CONSTEXPR_CHECK(empty.begin() == empty.end()); + CONSTEXPR_CHECK(empty.cbegin() == empty.cend()); + CONSTEXPR_CHECK(empty.rbegin() == empty.rend()); + + constexpr std::size_t expected_size = 3; + bulk_insert_wrapper> filled { + std::vector { 1, 2, 3 } + }; + CHECK(!filled.empty()); + CHECK(filled.size() == expected_size); + CHECK(filled.capacity() >= expected_size); + CHECK(std::distance(filled.begin(), filled.end()) == expected_size); + CHECK(std::distance(filled.cbegin(), filled.cend()) == expected_size); + CHECK(std::distance(filled.rbegin(), filled.rend()) == expected_size); + CHECK(filled[0] == 1); +} + +TEST_CASE("bulk_insert_wrapper make_room does not allocate", "[bulk_insert_wrapper][bulk_insert_wrapper-make_room]") { + SpyAllocator spy_allocator{}; + bulk_insert_wrapper< + std::vector< + int, + SpyAllocator + > + > empty { spy_allocator }; + empty.make_room_for(10); + + CHECK(empty.empty()); + CHECK(empty.size() == 0); + CHECK(empty.capacity() == 0); + CHECK(empty.begin() == empty.end()); + CHECK(empty.cbegin() == empty.cend()); + CHECK(empty.rbegin() == empty.rend()); + CHECK(spy_allocator.metrics->allocation_count == 0); +} + +// correct usage +TEST_CASE("bulk_insert_wrapper make_room + append_range", "[bulk_insert_wrapper][bulk_insert_wrapper-append_range]") { + SpyAllocator spy_allocator{}; + bulk_insert_wrapper< + std::vector< + int, + SpyAllocator + > + > wrapper { spy_allocator }; + + std::vector a { 1, 2 }; + std::vector b { 3, 4, 5 }; + + wrapper.make_room_for(a.size()); + wrapper.make_room_for(b.size()); + + wrapper.append_range(a); + CHECK(wrapper.size() == a.size()); + CHECK(wrapper.capacity() >= a.size() + b.size()); + + wrapper.append_range(b); + + CHECK(wrapper.size() == a.size() + b.size()); + CHECK(spy_allocator.metrics->allocation_count == 1); +} + +// cassert prevents testing in debug +#ifdef NDEBUG +// incorrect usage may not crash +TEST_CASE("bulk_insert_wrapper append_range without make_room", "[bulk_insert_wrapper][bulk_insert_wrapper-append_range]") { + SpyAllocator spy_allocator{}; + bulk_insert_wrapper< + std::vector< + int, + SpyAllocator + > + > wrapper { spy_allocator }; + + std::vector a { 1, 2 }; + std::vector b { 3, 4, 5 }; + + wrapper.append_range(a); + wrapper.append_range(b); + + CHECK(wrapper.size() == a.size() + b.size()); + CHECK(wrapper.capacity() >= a.size() + b.size()); + CHECK(spy_allocator.metrics->allocation_count <= 2); +} +#endif + +TEST_CASE("bulk_insert_wrapper clear", "[bulk_insert_wrapper][bulk_insert_wrapper-clear]") { + SpyAllocator spy_allocator{}; + bulk_insert_wrapper< + std::vector< + int, + SpyAllocator + > + > wrapper { spy_allocator }; + + std::vector a { 1, 2 }; + constexpr std::size_t extra_room = 1; + + wrapper.make_room_for(a.size()); + wrapper.make_room_for(extra_room); + + wrapper.append_range(a); + CHECK(wrapper.size() == a.size()); + CHECK(wrapper.capacity() >= a.size() + extra_room); + + wrapper.clear(); + CHECK(wrapper.empty()); + CHECK(spy_allocator.metrics->allocation_count == 1); +} + +TEST_CASE("bulk_insert_wrapper shrink_to_fit", "[bulk_insert_wrapper][bulk_insert_wrapper-shrink_to_fit]") { + SpyAllocator spy_allocator{}; + bulk_insert_wrapper< + std::vector< + int, + SpyAllocator + > + > wrapper { spy_allocator }; + + std::vector a { 1, 2 }; + constexpr std::size_t extra_room = 1; + + wrapper.make_room_for(a.size()); + wrapper.make_room_for(extra_room); + + wrapper.append_range(a); + CHECK(wrapper.size() == a.size()); + const std::size_t capacity_before_shrink = wrapper.capacity(); + CHECK(capacity_before_shrink >= a.size() + extra_room); + + wrapper.shrink_to_fit(); + CHECK(wrapper.size() == a.size()); + // shrink_to_fit() is non-binding, it may or may not actually shrink depending on underlying container implementation. + // just ensure we don't expand or allocate too often somehow + CHECK(wrapper.capacity() <= capacity_before_shrink); + CHECK(spy_allocator.metrics->allocation_count >= 1); + CHECK(spy_allocator.metrics->allocation_count <= 2); +} \ No newline at end of file