diff --git a/src/AdaptiveArrayPools.jl b/src/AdaptiveArrayPools.jl index e51e810..24bc999 100644 --- a/src/AdaptiveArrayPools.jl +++ b/src/AdaptiveArrayPools.jl @@ -6,7 +6,7 @@ export AdaptiveArrayPool, acquire!, unsafe_acquire!, pool_stats, get_task_local_ export acquire_view!, acquire_array! # Explicit naming aliases export @with_pool, @maybe_with_pool export USE_POOLING, MAYBE_POOLING_ENABLED, POOL_DEBUG -export checkpoint!, rewind! +export checkpoint!, rewind!, reset! export CACHE_WAYS, set_cache_ways! # N-way cache configuration # Core data structures @@ -18,7 +18,7 @@ include("utils.jl") # Acquisition operations: get_view!, acquire!, unsafe_acquire!, aliases include("acquire.jl") -# State management: checkpoint!, rewind!, empty! +# State management: checkpoint!, rewind!, reset!, empty! include("state.jl") # Task-local pool diff --git a/src/state.jl b/src/state.jl index f842020..708770c 100644 --- a/src/state.jl +++ b/src/state.jl @@ -1,5 +1,5 @@ # ============================================================================== -# State Management +# State Management - checkpoint! # ============================================================================== """ @@ -31,14 +31,53 @@ function checkpoint!(pool::AdaptiveArrayPool) return nothing end -# Internal helper for full checkpoint +""" + checkpoint!(pool::AdaptiveArrayPool, ::Type{T}) + +Save state for a specific type only. Used by optimized macros that know +which types will be used at compile time. + +Also updates _current_depth and _untracked_flags for untracked acquire detection. + +~77% faster than full checkpoint! when only one type is used. +""" +@inline function checkpoint!(pool::AdaptiveArrayPool, ::Type{T}) where T + pool._current_depth += 1 + push!(pool._untracked_flags, false) + _checkpoint_typed_pool!(get_typed_pool!(pool, T), pool._current_depth) + nothing +end + +""" + checkpoint!(pool::AdaptiveArrayPool, types::Type...) + +Save state for multiple specific types. Uses @generated for zero-overhead +compile-time unrolling. Increments _current_depth once for all types. +""" +@generated function checkpoint!(pool::AdaptiveArrayPool, types::Type...) + checkpoint_exprs = [:(_checkpoint_typed_pool!(get_typed_pool!(pool, types[$i]), pool._current_depth)) for i in 1:length(types)] + quote + pool._current_depth += 1 + push!(pool._untracked_flags, false) + $(checkpoint_exprs...) + nothing + end +end + +checkpoint!(::Nothing) = nothing +checkpoint!(::Nothing, ::Type) = nothing +checkpoint!(::Nothing, types::Type...) = nothing + +# Internal helper for checkpoint @inline function _checkpoint_typed_pool!(tp::TypedPool, depth::Int) push!(tp._checkpoint_n_active, tp.n_active) push!(tp._checkpoint_depths, depth) nothing end -checkpoint!(::Nothing) = nothing +# ============================================================================== +# State Management - rewind! +# ============================================================================== """ rewind!(pool::AdaptiveArrayPool) @@ -49,11 +88,21 @@ Uses _checkpoint_depths to accurately determine which entries to pop vs restore. Only the counters are restored; allocated memory remains for reuse. Handles untracked acquires by checking _checkpoint_depths for accurate restoration. -See also: [`checkpoint!`](@ref), [`@with_pool`](@ref) +**Safety**: If called at global scope (depth=1, no pending checkpoints), +automatically delegates to `reset!` to safely clear all n_active counters. + +See also: [`checkpoint!`](@ref), [`reset!`](@ref), [`@with_pool`](@ref) """ function rewind!(pool::AdaptiveArrayPool) cur_depth = pool._current_depth + # Safety guard: at global scope (depth=1), no checkpoint to rewind to + # Delegate to reset! which safely clears all n_active counters + if cur_depth == 1 + reset!(pool) + return nothing + end + # Fixed slots - zero allocation via @generated iteration foreach_fixed_slot(pool) do tp _rewind_typed_pool!(tp, cur_depth) @@ -70,107 +119,6 @@ function rewind!(pool::AdaptiveArrayPool) return nothing end -# Internal helper for full rewind with _checkpoint_depths -# Uses 1-based sentinel pattern: no isempty checks needed (sentinel [0] guarantees non-empty) -@inline function _rewind_typed_pool!(tp::TypedPool, current_depth::Int) - # 1. Orphaned Checkpoints Cleanup - # If there are checkpoints from deeper scopes (depth > current), pop them first. - # This happens when a nested scope did full checkpoint but typed rewind, - # leaving orphaned checkpoints that must be cleaned before finding current state. - while @inbounds tp._checkpoint_depths[end] > current_depth - pop!(tp._checkpoint_depths) - pop!(tp._checkpoint_n_active) - end - - # 2. Normal Rewind Logic (Sentinel Pattern) - # Now the stack top is guaranteed to be at depth <= current depth. - if @inbounds tp._checkpoint_depths[end] == current_depth - # Checkpointed at current depth: pop and restore - pop!(tp._checkpoint_depths) - tp.n_active = pop!(tp._checkpoint_n_active) - else - # No checkpoint at current depth (this type was excluded from typed checkpoint) - # MUST restore n_active from parent checkpoint value! - # - Untracked acquire may have modified n_active - # - If sentinel (_checkpoint_n_active=[0]), restores to n_active=0 - tp.n_active = @inbounds tp._checkpoint_n_active[end] - end - nothing -end - -rewind!(::Nothing) = nothing - -# ============================================================================== -# Type-Specific State Management (for optimized macros) -# ============================================================================== - -""" - checkpoint!(tp::TypedPool) - -Internal method for saving TypedPool state (legacy, uses depth=0). - -!!! warning "Internal API" - This is an internal implementation detail. For manual pool management, - use the public API instead: - ```julia - checkpoint!(pool, Float64) # Type-specific checkpoint - ``` - -See also: [`checkpoint!(::AdaptiveArrayPool, ::Type)`](@ref), [`rewind!`](@ref) -""" -@inline function checkpoint!(tp::TypedPool) - push!(tp._checkpoint_n_active, tp.n_active) - push!(tp._checkpoint_depths, 0) # Legacy depth - nothing -end - -""" - checkpoint!(tp::TypedPool, depth::Int) - -Internal method for saving TypedPool state with depth tracking. -""" -@inline function checkpoint!(tp::TypedPool, depth::Int) - push!(tp._checkpoint_n_active, tp.n_active) - push!(tp._checkpoint_depths, depth) - nothing -end - -""" - rewind!(tp::TypedPool) - -Internal method for restoring TypedPool state (pops both stacks). - -!!! warning "Internal API" - This is an internal implementation detail. For manual pool management, - use the public API instead: - ```julia - rewind!(pool, Float64) # Type-specific rewind - ``` - -See also: [`rewind!(::AdaptiveArrayPool, ::Type)`](@ref), [`checkpoint!`](@ref) -""" -@inline function rewind!(tp::TypedPool) - pop!(tp._checkpoint_depths) - tp.n_active = pop!(tp._checkpoint_n_active) - nothing -end - -""" - checkpoint!(pool::AdaptiveArrayPool, ::Type{T}) - -Save state for a specific type only. Used by optimized macros that know -which types will be used at compile time. - -Also updates _current_depth and _untracked_flags for untracked acquire detection. - -~77% faster than full checkpoint! when only one type is used. -""" -@inline function checkpoint!(pool::AdaptiveArrayPool, ::Type{T}) where T - pool._current_depth += 1 - push!(pool._untracked_flags, false) - checkpoint!(get_typed_pool!(pool, T), pool._current_depth) -end - """ rewind!(pool::AdaptiveArrayPool, ::Type{T}) @@ -178,29 +126,15 @@ Restore state for a specific type only. Also updates _current_depth and _untracked_flags. """ @inline function rewind!(pool::AdaptiveArrayPool, ::Type{T}) where T - rewind!(get_typed_pool!(pool, T)) + # Safety guard: at global scope (depth=1), delegate to reset! + if pool._current_depth == 1 + reset!(get_typed_pool!(pool, T)) + return nothing + end + _rewind_typed_pool!(get_typed_pool!(pool, T), pool._current_depth) pop!(pool._untracked_flags) pool._current_depth -= 1 -end - -checkpoint!(::Nothing, ::Type) = nothing -rewind!(::Nothing, ::Type) = nothing - -""" - checkpoint!(pool::AdaptiveArrayPool, types::Type...) - -Save state for multiple specific types. Uses @generated for zero-overhead -compile-time unrolling. Increments _current_depth once for all types. -""" -@generated function checkpoint!(pool::AdaptiveArrayPool, types::Type...) - # First increment depth, then checkpoint each type with that depth - checkpoint_exprs = [:(checkpoint!(get_typed_pool!(pool, types[$i]), pool._current_depth)) for i in 1:length(types)] - quote - pool._current_depth += 1 - push!(pool._untracked_flags, false) - $(checkpoint_exprs...) - nothing - end + nothing end """ @@ -210,9 +144,14 @@ Restore state for multiple specific types in reverse order. Decrements _current_depth once after all types are rewound. """ @generated function rewind!(pool::AdaptiveArrayPool, types::Type...) - # Reverse order for proper stack unwinding, rewind TypedPools directly - rewind_exprs = [:(rewind!(get_typed_pool!(pool, types[$i]))) for i in length(types):-1:1] + rewind_exprs = [:(_rewind_typed_pool!(get_typed_pool!(pool, types[$i]), pool._current_depth)) for i in length(types):-1:1] + reset_exprs = [:(reset!(get_typed_pool!(pool, types[$i]))) for i in 1:length(types)] quote + # Safety guard: at global scope (depth=1), delegate to reset! + if pool._current_depth == 1 + $(reset_exprs...) + return nothing + end $(rewind_exprs...) pop!(pool._untracked_flags) pool._current_depth -= 1 @@ -220,11 +159,40 @@ Decrements _current_depth once after all types are rewound. end end -checkpoint!(::Nothing, types::Type...) = nothing +rewind!(::Nothing) = nothing +rewind!(::Nothing, ::Type) = nothing rewind!(::Nothing, types::Type...) = nothing +# Internal helper for rewind with orphan cleanup +# Uses 1-based sentinel pattern: no isempty checks needed (sentinel [0] guarantees non-empty) +@inline function _rewind_typed_pool!(tp::TypedPool, current_depth::Int) + # 1. Orphaned Checkpoints Cleanup + # If there are checkpoints from deeper scopes (depth > current), pop them first. + # This happens when a nested scope did full checkpoint but typed rewind, + # leaving orphaned checkpoints that must be cleaned before finding current state. + while @inbounds tp._checkpoint_depths[end] > current_depth + pop!(tp._checkpoint_depths) + pop!(tp._checkpoint_n_active) + end + + # 2. Normal Rewind Logic (Sentinel Pattern) + # Now the stack top is guaranteed to be at depth <= current depth. + if @inbounds tp._checkpoint_depths[end] == current_depth + # Checkpointed at current depth: pop and restore + pop!(tp._checkpoint_depths) + tp.n_active = pop!(tp._checkpoint_n_active) + else + # No checkpoint at current depth (this type was excluded from typed checkpoint) + # MUST restore n_active from parent checkpoint value! + # - Untracked acquire may have modified n_active + # - If sentinel (_checkpoint_n_active=[0]), restores to n_active=0 + tp.n_active = @inbounds tp._checkpoint_n_active[end] + end + nothing +end + # ============================================================================== -# Pool Clearing +# State Management - empty! # ============================================================================== """ @@ -291,3 +259,119 @@ function Base.empty!(pool::AdaptiveArrayPool) end Base.empty!(::Nothing) = nothing + +# ============================================================================== +# State Management - reset! +# ============================================================================== + +""" + reset!(tp::TypedPool) + +Reset TypedPool state without clearing allocated storage. + +Sets `n_active = 0` and restores checkpoint stacks to sentinel state. +All vectors, views, and N-D arrays are preserved for reuse. + +This is useful when you want to "start fresh" without reallocating memory. +""" +function reset!(tp::TypedPool) + tp.n_active = 0 + # Restore sentinel values (1-based sentinel pattern) + empty!(tp._checkpoint_n_active) + push!(tp._checkpoint_n_active, 0) # Sentinel: n_active=0 at depth=0 + empty!(tp._checkpoint_depths) + push!(tp._checkpoint_depths, 0) # Sentinel: depth=0 = no checkpoint + return tp +end + +""" + reset!(pool::AdaptiveArrayPool) + +Reset pool state without clearing allocated storage. + +This function: +- Resets all `n_active` counters to 0 +- Restores all checkpoint stacks to sentinel state +- Resets `_current_depth` and `_untracked_flags` + +Unlike `empty!`, this **preserves** all allocated vectors, views, and N-D arrays +for reuse, avoiding reallocation costs. + +## Use Case +When functions that acquire from the pool are called without proper +`checkpoint!/rewind!` management, `n_active` can grow indefinitely. +Use `reset!` to cleanly restore the pool to its initial state while +keeping allocated memory available. + +## Example +```julia +pool = AdaptiveArrayPool() + +# Some function that acquires without checkpoint management +function compute!(pool) + v = acquire!(pool, Float64, 100) + # ... use v ... + # No rewind! called +end + +for _ in 1:1000 + compute!(pool) # n_active grows each iteration +end + +reset!(pool) # Restore state, keep allocated memory +# Now pool.n_active == 0, but vectors are still available for reuse +``` + +See also: [`empty!`](@ref), [`rewind!`](@ref) +""" +function reset!(pool::AdaptiveArrayPool) + # Fixed slots - zero allocation via @generated iteration + foreach_fixed_slot(pool) do tp + reset!(tp) + end + + # Others - reset all TypedPools + for tp in values(pool.others) + reset!(tp) + end + + # Reset untracked detection state (1-based sentinel pattern) + pool._current_depth = 1 # 1 = global scope (sentinel) + empty!(pool._untracked_flags) + push!(pool._untracked_flags, false) # Sentinel: global scope starts with false + + return pool +end + +""" + reset!(pool::AdaptiveArrayPool, ::Type{T}) + +Reset state for a specific type only. Clears n_active and checkpoint stacks +to sentinel state while preserving allocated vectors. + +See also: [`reset!(::AdaptiveArrayPool)`](@ref), [`rewind!`](@ref) +""" +@inline function reset!(pool::AdaptiveArrayPool, ::Type{T}) where T + reset!(get_typed_pool!(pool, T)) + pool +end + +""" + reset!(pool::AdaptiveArrayPool, types::Type...) + +Reset state for multiple specific types. Uses @generated for zero-overhead +compile-time unrolling. + +See also: [`reset!(::AdaptiveArrayPool)`](@ref), [`rewind!`](@ref) +""" +@generated function reset!(pool::AdaptiveArrayPool, types::Type...) + reset_exprs = [:(reset!(get_typed_pool!(pool, types[$i]))) for i in 1:length(types)] + quote + $(reset_exprs...) + pool + end +end + +reset!(::Nothing) = nothing +reset!(::Nothing, ::Type) = nothing +reset!(::Nothing, types::Type...) = nothing diff --git a/test/test_state.jl b/test/test_state.jl index 2955040..0ba05de 100644 --- a/test/test_state.jl +++ b/test/test_state.jl @@ -243,6 +243,351 @@ rewind!(pool) end + @testset "reset! (state-only reset)" begin + import AdaptiveArrayPools: reset! + + @testset "basic reset! - n_active to zero" begin + pool = AdaptiveArrayPool() + + # Acquire some arrays + v1 = acquire!(pool, Float64, 100) + v2 = acquire!(pool, Float32, 50) + v3 = acquire!(pool, Int64, 30) + @test pool.float64.n_active == 1 + @test pool.float32.n_active == 1 + @test pool.int64.n_active == 1 + + # Reset + result = reset!(pool) + @test result === pool # Returns self + + # All n_active should be 0 + @test pool.float64.n_active == 0 + @test pool.float32.n_active == 0 + @test pool.int64.n_active == 0 + end + + @testset "reset! preserves vectors and caches" begin + pool = AdaptiveArrayPool() + + # Acquire arrays (populates vectors) + v1 = acquire!(pool, Float64, 100) + v2 = acquire!(pool, Float64, 200) + # Use unsafe_acquire! to populate 1D cache + v3 = unsafe_acquire!(pool, Float64, 50) + # Use N-D acquire to populate nd cache + m1 = acquire!(pool, Float64, 10, 10) + @test length(pool.float64.vectors) >= 3 + + # Reset - should preserve everything + reset!(pool) + @test pool.float64.n_active == 0 + @test length(pool.float64.vectors) >= 3 # Vectors preserved + @test length(pool.float64.views) >= 1 # 1D cache preserved + @test length(pool.float64.nd_arrays) >= 1 # N-D cache preserved + end + + @testset "reset! restores checkpoint stacks to sentinel" begin + pool = AdaptiveArrayPool() + + # Nested checkpoints + checkpoint!(pool) + acquire!(pool, Float64, 10) + checkpoint!(pool) + acquire!(pool, Float64, 20) + checkpoint!(pool) + acquire!(pool, Float64, 30) + + @test pool._current_depth == 4 + @test length(pool.float64._checkpoint_n_active) > 1 + @test length(pool.float64._checkpoint_depths) > 1 + + # Reset - should restore sentinel state + reset!(pool) + + @test pool._current_depth == 1 + @test pool._untracked_flags == [false] + @test pool.float64._checkpoint_n_active == [0] # Sentinel only + @test pool.float64._checkpoint_depths == [0] # Sentinel only + end + + @testset "reset! with fallback types" begin + pool = AdaptiveArrayPool() + + # Use fallback type (not in fixed slots) + v1 = acquire!(pool, UInt8, 100) + v2 = acquire!(pool, UInt16, 50) + @test pool.others[UInt8].n_active == 1 + @test pool.others[UInt16].n_active == 1 + @test length(pool.others[UInt8].vectors) == 1 + + # Reset + reset!(pool) + + # n_active reset but vectors preserved + @test pool.others[UInt8].n_active == 0 + @test pool.others[UInt16].n_active == 0 + @test length(pool.others[UInt8].vectors) == 1 # Preserved! + @test length(pool.others[UInt16].vectors) == 1 + end + + @testset "reset!(nothing) compatibility" begin + @test reset!(nothing) === nothing + end + + @testset "pool usable after reset!" begin + pool = AdaptiveArrayPool() + + # First use + v1 = acquire!(pool, Float64, 100) + v1 .= 42.0 + backing1 = parent(v1) + + # Reset + reset!(pool) + + # Should be usable and reuse existing vector + checkpoint!(pool) + v2 = acquire!(pool, Float64, 100) + @test parent(v2) === backing1 # Same backing vector reused + @test pool.float64.n_active == 1 + rewind!(pool) + @test pool.float64.n_active == 0 + end + + @testset "A/B scenario - unmanaged then reset" begin + # Simulates: inner function acquires without management, + # outer function calls reset! to clean up + + pool = AdaptiveArrayPool() + + # Function that acquires without checkpoint/rewind + function unmanaged_compute!(p) + v = acquire!(p, Float64, 100) + v .= 1.0 + # No rewind! + end + + # Call multiple times - n_active grows + for _ in 1:10 + unmanaged_compute!(pool) + end + @test pool.float64.n_active == 10 + @test length(pool.float64.vectors) == 10 + + # Reset - clean slate but vectors preserved + reset!(pool) + @test pool.float64.n_active == 0 + @test length(pool.float64.vectors) == 10 # All preserved for reuse + + # Next use reuses existing vectors + checkpoint!(pool) + for _ in 1:5 + acquire!(pool, Float64, 100) + end + @test pool.float64.n_active == 5 + @test length(pool.float64.vectors) == 10 # No new allocations + rewind!(pool) + end + + @testset "reset! vs empty! comparison" begin + # Verify reset! preserves while empty! clears + + pool1 = AdaptiveArrayPool() + pool2 = AdaptiveArrayPool() + + # Both acquire same arrays + for _ in 1:5 + acquire!(pool1, Float64, 100) + acquire!(pool2, Float64, 100) + end + @test length(pool1.float64.vectors) == 5 + @test length(pool2.float64.vectors) == 5 + + # reset! preserves + reset!(pool1) + @test pool1.float64.n_active == 0 + @test length(pool1.float64.vectors) == 5 # Preserved + + # empty! clears + empty!(pool2) + @test pool2.float64.n_active == 0 + @test length(pool2.float64.vectors) == 0 # Cleared + end + + @testset "TypedPool reset!" begin + import AdaptiveArrayPools: get_typed_pool!, _checkpoint_typed_pool! + + pool = AdaptiveArrayPool() + tp = get_typed_pool!(pool, Float64) + + # Acquire and checkpoint using internal helper + _checkpoint_typed_pool!(tp, 1) + acquire!(pool, Float64, 100) + _checkpoint_typed_pool!(tp, 2) + acquire!(pool, Float64, 200) + @test tp.n_active == 2 + @test length(tp._checkpoint_n_active) > 1 + + # Reset TypedPool directly + result = reset!(tp) + @test result === tp + @test tp.n_active == 0 + @test tp._checkpoint_n_active == [0] + @test tp._checkpoint_depths == [0] + @test length(tp.vectors) == 2 # Vectors preserved + end + + @testset "type-specific reset!(pool, Type)" begin + pool = AdaptiveArrayPool() + + # Acquire multiple types + acquire!(pool, Float64, 100) + acquire!(pool, Int64, 50) + @test pool.float64.n_active == 1 + @test pool.int64.n_active == 1 + + # Reset only Float64 + reset!(pool, Float64) + @test pool.float64.n_active == 0 + @test pool.int64.n_active == 1 # Int64 unchanged + @test pool.float64._checkpoint_n_active == [0] + @test length(pool.float64.vectors) == 1 # Vector preserved + end + + @testset "type-specific reset!(pool, Type...)" begin + pool = AdaptiveArrayPool() + + # Acquire multiple types + acquire!(pool, Float64, 100) + acquire!(pool, Int64, 50) + acquire!(pool, Float32, 25) + @test pool.float64.n_active == 1 + @test pool.int64.n_active == 1 + @test pool.float32.n_active == 1 + + # Reset Float64 and Int64, but not Float32 + reset!(pool, Float64, Int64) + @test pool.float64.n_active == 0 + @test pool.int64.n_active == 0 + @test pool.float32.n_active == 1 # Float32 unchanged + end + + @testset "reset!(nothing, Type) compatibility" begin + @test reset!(nothing, Float64) === nothing + @test reset!(nothing, Float64, Int64) === nothing + end + end + + @testset "Safe rewind! at depth=1" begin + @testset "rewind! without checkpoint (depth=1)" begin + pool = AdaptiveArrayPool() + + # Acquire without checkpoint + v1 = acquire!(pool, Float64, 100) + v2 = acquire!(pool, Float64, 200) + @test pool.float64.n_active == 2 + @test pool._current_depth == 1 + + # rewind! at depth=1 should be safe (delegates to reset!) + rewind!(pool) + @test pool.float64.n_active == 0 + @test pool._current_depth == 1 + @test pool._untracked_flags == [false] + end + + @testset "rewind! after reset!" begin + pool = AdaptiveArrayPool() + + checkpoint!(pool) + acquire!(pool, Float64, 100) + @test pool._current_depth == 2 + + # Reset clears everything + reset!(pool) + @test pool._current_depth == 1 + + # rewind! after reset should be safe + rewind!(pool) + @test pool._current_depth == 1 # Still at global scope + end + + @testset "checkpoint/rewind cycle after reset!" begin + pool = AdaptiveArrayPool() + + # Initial acquire and reset + acquire!(pool, Float64, 50) + reset!(pool) + + # Normal checkpoint/rewind should work + checkpoint!(pool) + @test pool._current_depth == 2 + acquire!(pool, Float64, 100) + @test pool.float64.n_active == 1 + + rewind!(pool) + @test pool.float64.n_active == 0 + @test pool._current_depth == 1 + end + + @testset "multiple rewind! at depth=1 is safe" begin + pool = AdaptiveArrayPool() + + acquire!(pool, Float64, 100) + @test pool.float64.n_active == 1 + + # Multiple rewind! calls should all be safe + rewind!(pool) + @test pool.float64.n_active == 0 + rewind!(pool) + @test pool.float64.n_active == 0 + rewind!(pool) + @test pool._current_depth == 1 + end + + @testset "type-specific rewind!(pool, Type) at depth=1" begin + pool = AdaptiveArrayPool() + + # Acquire without checkpoint + v1 = acquire!(pool, Float64, 100) + @test pool.float64.n_active == 1 + @test pool._current_depth == 1 + + # Type-specific rewind! at depth=1 should be safe + rewind!(pool, Float64) + @test pool.float64.n_active == 0 + @test pool._current_depth == 1 # Should not go to 0 + @test pool.float64._checkpoint_n_active == [0] # Sentinel preserved + + # Multiple calls should be safe + rewind!(pool, Float64) + @test pool._current_depth == 1 + end + + @testset "type-specific rewind!(pool, Type...) at depth=1" begin + pool = AdaptiveArrayPool() + + # Acquire multiple types without checkpoint + v1 = acquire!(pool, Float64, 100) + v2 = acquire!(pool, Int64, 50) + @test pool.float64.n_active == 1 + @test pool.int64.n_active == 1 + @test pool._current_depth == 1 + + # Multi-type rewind! at depth=1 should be safe + rewind!(pool, Float64, Int64) + @test pool.float64.n_active == 0 + @test pool.int64.n_active == 0 + @test pool._current_depth == 1 # Should not go to 0 + @test pool.float64._checkpoint_n_active == [0] + @test pool.int64._checkpoint_n_active == [0] + + # Multiple calls should be safe + rewind!(pool, Float64, Int64) + @test pool._current_depth == 1 + end + end + @testset "Typed checkpoint!/rewind! (generated functions)" begin pool = AdaptiveArrayPool() @@ -308,47 +653,46 @@ @test rewind!(nothing, Float64, Int64) === nothing end - @testset "Direct TypedPool checkpoint!/rewind!" begin - import AdaptiveArrayPools: get_typed_pool! + @testset "Internal TypedPool helpers" begin + import AdaptiveArrayPools: get_typed_pool!, _checkpoint_typed_pool!, _rewind_typed_pool! pool = AdaptiveArrayPool() # Get TypedPool directly tp = get_typed_pool!(pool, Float64) @test tp.n_active == 0 - # Direct TypedPool checkpoint and rewind - checkpoint!(tp) + # Direct TypedPool checkpoint and rewind using internal helpers + _checkpoint_typed_pool!(tp, 1) v1 = acquire!(pool, Float64, 100) @test tp.n_active == 1 v2 = acquire!(pool, Float64, 200) @test tp.n_active == 2 - rewind!(tp) + _rewind_typed_pool!(tp, 1) @test tp.n_active == 0 # Nested checkpoint/rewind on TypedPool - checkpoint!(tp) + _checkpoint_typed_pool!(tp, 1) v1 = acquire!(pool, Float64, 10) @test tp.n_active == 1 - checkpoint!(tp) + _checkpoint_typed_pool!(tp, 2) v2 = acquire!(pool, Float64, 20) @test tp.n_active == 2 - checkpoint!(tp) + _checkpoint_typed_pool!(tp, 3) v3 = acquire!(pool, Float64, 30) @test tp.n_active == 3 - rewind!(tp) + _rewind_typed_pool!(tp, 3) @test tp.n_active == 2 - rewind!(tp) + _rewind_typed_pool!(tp, 2) @test tp.n_active == 1 - rewind!(tp) + _rewind_typed_pool!(tp, 1) @test tp.n_active == 0 # Verify type-specific checkpoint delegates to TypedPool - # (This tests the refactored implementation) checkpoint!(pool, Float64) v = acquire!(pool, Float64, 50) @test tp.n_active == 1