// MIT License - Copyright (c) 2026 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Utils
{
using System;
using System.Runtime.CompilerServices;
using WallstopStudios.UnityHelpers.Core.DataStructure;
using WallstopStudios.UnityHelpers.Core.Helper;
///
/// Tracks the rolling high-water mark (peak value) within a configurable time window.
/// Used by intelligent pool purging to determine typical usage patterns.
///
///
///
/// This data structure maintains a time-windowed maximum value using a sliding window approach.
/// It stores timestamped samples and efficiently computes the maximum within the window.
///
///
/// Thread safety: All operations are protected by a lock for multi-threaded environments.
///
///
internal sealed class RollingHighWaterMark
{
///
/// Multiplier for calculating cleanup interval from window size.
/// Cleanup runs every 10% of the window duration.
///
private const float CleanupIntervalMultiplier = 0.1f;
///
/// Minimum cleanup interval in seconds to avoid excessive cleanup operations.
///
private const float MinCleanupIntervalSeconds = 1f;
///
/// Maximum number of samples to store to prevent unbounded growth under extreme load.
/// When this limit is reached, old samples are removed before adding new ones.
///
private const int MaxSampleCount = 10000;
internal readonly struct Sample
{
public readonly float Time;
public readonly int Value;
public Sample(float time, int value)
{
Time = time;
Value = value;
}
}
private readonly CyclicBuffer _samples;
private readonly CyclicBuffer _peakDeque;
private readonly object _lock = new object();
private float _windowSeconds;
private int _cachedPeak;
private float _lastCleanupTime;
private long _runningSum;
///
/// Gets or sets the rolling window duration in seconds.
///
public float WindowSeconds
{
get
{
lock (_lock)
{
return _windowSeconds;
}
}
set
{
lock (_lock)
{
_windowSeconds =
value > 0f ? value : PoolPurgeSettings.DefaultRollingWindowSeconds;
}
}
}
///
/// Gets the current peak value within the rolling window.
///
public int Peak
{
get
{
lock (_lock)
{
return _cachedPeak;
}
}
}
///
/// Gets the number of samples currently stored.
///
internal int SampleCount
{
get
{
lock (_lock)
{
return _samples.Count;
}
}
}
///
/// Creates a new rolling high-water mark tracker.
///
/// The rolling window duration in seconds.
public RollingHighWaterMark(float windowSeconds)
{
_windowSeconds =
windowSeconds > 0f ? windowSeconds : PoolPurgeSettings.DefaultRollingWindowSeconds;
_cachedPeak = 0;
_samples = new CyclicBuffer(MaxSampleCount);
_peakDeque = new CyclicBuffer(MaxSampleCount);
}
///
/// Records a new sample value at the specified time.
///
/// The current time.
/// The value to record.
public void Record(float currentTime, int value)
{
lock (_lock)
{
RecordCore(currentTime, value);
}
}
///
/// Gets the current peak value within the rolling window, cleaning up expired samples.
///
/// The current time.
/// The peak value within the window.
public int GetPeak(float currentTime)
{
lock (_lock)
{
CleanupIfNeeded(currentTime);
return _cachedPeak;
}
}
///
/// Records a new sample and returns the average in a single lock acquisition.
/// This avoids the overhead of acquiring the lock twice when both operations are needed.
///
/// The current time.
/// The value to record.
/// The average value within the window after recording, or 0 if no samples exist.
public float RecordAndGetAverage(float currentTime, int value)
{
lock (_lock)
{
RecordCore(currentTime, value);
if (_samples.Count == 0)
{
return 0f;
}
return (float)_runningSum / _samples.Count;
}
}
///
/// Core record logic shared by and .
/// Must be called while holding .
///
private void RecordCore(float currentTime, int value)
{
// Handle overflow: if buffer is full, account for the item about to be overwritten
if (_samples.Count == _samples.Capacity)
{
Sample oldest = _samples[0];
_runningSum -= oldest.Value;
}
Sample newSample = new Sample(currentTime, value);
_samples.Add(newSample);
_runningSum += value;
// Update peak deque: maintain strictly decreasing invariant
while (_peakDeque.Count > 0 && _peakDeque[_peakDeque.Count - 1].Value <= value)
{
_peakDeque.TryPopBack(out _);
}
_peakDeque.Add(newSample);
// Synchronize peak deque with samples buffer to evict stale entries
SyncPeakDeque();
CleanupIfNeeded(currentTime);
}
///
/// Gets the average value within the rolling window.
///
/// The current time.
/// The average value, or 0 if no samples exist.
public float GetAverage(float currentTime)
{
lock (_lock)
{
CleanupIfNeeded(currentTime);
if (_samples.Count == 0)
{
return 0f;
}
return (float)_runningSum / _samples.Count;
}
}
///
/// Clears all recorded samples and resets the peak.
///
public void Clear()
{
lock (_lock)
{
_samples.Clear();
_peakDeque.Clear();
_runningSum = 0;
_cachedPeak = 0;
_lastCleanupTime = 0f;
}
}
private void CleanupIfNeeded(float currentTime)
{
float cleanupInterval = _windowSeconds * CleanupIntervalMultiplier;
if (cleanupInterval < MinCleanupIntervalSeconds)
{
cleanupInterval = MinCleanupIntervalSeconds;
}
if (currentTime - _lastCleanupTime < cleanupInterval)
{
return;
}
_lastCleanupTime = currentTime;
float cutoff = currentTime - _windowSeconds;
while (_samples.Count > 0 && _samples[0].Time < cutoff)
{
_samples.TryPopFront(out Sample expired);
_runningSum -= expired.Value;
}
SyncPeakDeque();
}
///
/// Evicts stale entries from the peak deque that correspond to samples no longer
/// in the samples buffer. Must be called while holding .
///
private void SyncPeakDeque()
{
if (_samples.Count == 0)
{
_peakDeque.Clear();
_cachedPeak = 0;
return;
}
float oldestSampleTime = _samples[0].Time;
while (_peakDeque.Count > 0 && _peakDeque[0].Time < oldestSampleTime)
{
_peakDeque.TryPopFront(out _);
}
_cachedPeak = _peakDeque.Count > 0 ? _peakDeque[0].Value : 0;
}
}
///
/// Immutable snapshot of all purge-related parameters computed in a single lock acquisition.
/// Used to reduce lock contention on the hot purge path.
///
internal readonly struct PurgeParameters
{
public readonly float EffectiveIdleTimeout;
public readonly int EffectiveMinRetainCount;
public readonly int ComfortableSize;
public readonly bool InHysteresis;
public PurgeParameters(
float effectiveIdleTimeout,
int effectiveMinRetainCount,
int comfortableSize,
bool inHysteresis
)
{
EffectiveIdleTimeout = effectiveIdleTimeout;
EffectiveMinRetainCount = effectiveMinRetainCount;
ComfortableSize = comfortableSize;
InHysteresis = inHysteresis;
}
}
///
/// Tracks usage statistics for intelligent pool purging.
///
///
/// This class maintains concurrent rental tracking, spike detection,
/// and access frequency metrics to enable hysteresis-based purge protection
/// and frequency-informed purge decisions.
///
internal sealed class PoolUsageTracker
{
///
/// Number of seconds per minute for time conversions.
///
private const float SecondsPerMinute = 60f;
///
/// Default duration in seconds for frequency tracking window (1 minute).
///
private const float DefaultFrequencyWindowSeconds = 60f;
///
/// Buffer cap applied under medium memory pressure (no additional buffer).
///
private const float MediumPressureBufferCap = 1.0f;
///
/// Buffer cap applied under low memory pressure (modest additional buffer).
///
private const float LowPressureBufferCap = 1.5f;
///
/// Multiplier for high-frequency pools to increase buffer size.
/// High-frequency pools get 50% extra buffer.
///
private const float HighFrequencyBufferBoost = 1.5f;
///
/// Threshold for rentals-per-minute to be considered high frequency.
/// Pools with 10+ rentals per minute are high-frequency.
///
private const float HighFrequencyThreshold = 10f;
///
/// Multiplier for low-frequency pools idle timeout reduction.
/// Low-frequency pools purge 50% faster.
///
private const float LowFrequencyTimeoutMultiplier = 0.5f;
///
/// Threshold for rentals-per-minute to be considered low frequency.
/// Pools with at most 1 rental per minute are low-frequency.
///
private const float LowFrequencyThreshold = 1f;
///
/// Threshold in minutes for unused pool aggressive purge.
/// Pools with no access for 5+ minutes are candidates for aggressive purge.
///
private const float UnusedPoolThresholdMinutes = 5f;
private readonly RollingHighWaterMark _rollingHighWaterMark;
private readonly object _lock = new object();
private int _currentlyRented;
private int _peakConcurrentRentals;
private float _lastRentalTime;
private float _lastReturnTime;
private float _lastSpikeTime;
private float _hysteresisSeconds;
private float _spikeThresholdMultiplier;
private float _bufferMultiplier;
private int _rentalCountThisWindow;
private float _windowStartTime;
private float _cachedRentalsPerMinute;
private long _totalRentalCount;
private double _totalInterRentalTimeSeconds;
private int _interRentalCount;
private float _previousRentalTime;
///
/// Gets the current number of items rented from the pool.
///
public int CurrentlyRented
{
get
{
lock (_lock)
{
return _currentlyRented;
}
}
}
///
/// Gets the all-time peak of concurrent rentals.
///
public int PeakConcurrentRentals
{
get
{
lock (_lock)
{
return _peakConcurrentRentals;
}
}
}
///
/// Gets the time of the last rental.
///
public float LastRentalTime
{
get
{
lock (_lock)
{
return _lastRentalTime;
}
}
}
///
/// Gets the time of the last return.
///
public float LastReturnTime
{
get
{
lock (_lock)
{
return _lastReturnTime;
}
}
}
///
/// Gets the time of the most recent access (rent or return).
///
public float LastAccessTime
{
get
{
lock (_lock)
{
return _lastRentalTime > _lastReturnTime ? _lastRentalTime : _lastReturnTime;
}
}
}
///
/// Gets the current rentals-per-minute rate based on the rolling frequency window.
///
public float RentalsPerMinute
{
get
{
lock (_lock)
{
return _cachedRentalsPerMinute;
}
}
}
///
/// Gets the average time between consecutive rentals in seconds.
/// This represents the inter-arrival time between rental operations, not the duration items are held.
/// Returns 0 if fewer than two rentals have occurred.
///
///
///
/// This metric tracks the time between consecutive rent calls, which can be accurately measured
/// without per-item state tracking. For concurrent pools with overlapping rentals, this provides
/// a useful measure of rental frequency (inverse of rentals-per-second).
///
///
/// Example: If rentals occur at t=0, t=1, t=3, the average inter-rental time is (1 + 2) / 2 = 1.5 seconds.
///
///
public float AverageInterRentalTimeSeconds
{
get
{
lock (_lock)
{
if (_interRentalCount == 0)
{
return 0f;
}
return (float)(_totalInterRentalTimeSeconds / _interRentalCount);
}
}
}
///
/// Gets the total number of rentals since pool creation.
///
public long TotalRentalCount
{
get
{
lock (_lock)
{
return _totalRentalCount;
}
}
}
///
/// Gets or sets the hysteresis duration in seconds.
///
public float HysteresisSeconds
{
get
{
lock (_lock)
{
return _hysteresisSeconds;
}
}
set
{
lock (_lock)
{
_hysteresisSeconds = value;
}
}
}
///
/// Gets or sets the spike threshold multiplier.
///
public float SpikeThresholdMultiplier
{
get
{
lock (_lock)
{
return _spikeThresholdMultiplier;
}
}
set
{
lock (_lock)
{
_spikeThresholdMultiplier = value;
}
}
}
///
/// Gets or sets the buffer multiplier.
///
public float BufferMultiplier
{
get
{
lock (_lock)
{
return _bufferMultiplier;
}
}
set
{
lock (_lock)
{
_bufferMultiplier = value;
}
}
}
///
/// Gets or sets the rolling window duration in seconds.
///
public float RollingWindowSeconds
{
get => _rollingHighWaterMark.WindowSeconds;
set => _rollingHighWaterMark.WindowSeconds = value;
}
///
/// Creates a new pool usage tracker.
///
/// The rolling window duration for high-water mark tracking.
/// The hysteresis duration after spikes.
/// The multiplier for spike detection.
/// The buffer multiplier for comfortable size calculation.
public PoolUsageTracker(
float rollingWindowSeconds,
float hysteresisSeconds,
float spikeThresholdMultiplier,
float bufferMultiplier
)
{
_rollingHighWaterMark = new RollingHighWaterMark(rollingWindowSeconds);
_hysteresisSeconds = hysteresisSeconds;
_spikeThresholdMultiplier = spikeThresholdMultiplier;
_bufferMultiplier = bufferMultiplier;
}
///
/// Records a rental operation.
///
/// The current time.
public void RecordRent(float currentTime)
{
lock (_lock)
{
_currentlyRented++;
_totalRentalCount++;
if (_previousRentalTime > 0f && currentTime >= _previousRentalTime)
{
float interRentalTime = currentTime - _previousRentalTime;
_totalInterRentalTimeSeconds += interRentalTime;
_interRentalCount++;
}
_previousRentalTime = currentTime;
_lastRentalTime = currentTime;
if (_currentlyRented > _peakConcurrentRentals)
{
_peakConcurrentRentals = _currentlyRented;
}
float average = _rollingHighWaterMark.RecordAndGetAverage(
currentTime,
_currentlyRented
);
if (
_spikeThresholdMultiplier > 0f
&& _currentlyRented > average * _spikeThresholdMultiplier
)
{
_lastSpikeTime = currentTime;
}
UpdateFrequencyTracking(currentTime);
}
}
///
/// Records a return operation.
///
/// The current time.
public void RecordReturn(float currentTime)
{
lock (_lock)
{
if (_currentlyRented > 0)
{
_currentlyRented--;
}
_lastReturnTime = currentTime;
_rollingHighWaterMark.Record(currentTime, _currentlyRented);
}
}
///
/// Gets the rolling high-water mark (peak within the rolling window).
///
/// The current time.
/// The peak concurrent rentals within the rolling window.
public int GetRollingHighWaterMark(float currentTime)
{
return _rollingHighWaterMark.GetPeak(currentTime);
}
///
/// Gets the rolling average of concurrent rentals.
///
/// The current time.
/// The average concurrent rentals within the rolling window.
public float GetRollingAverage(float currentTime)
{
return _rollingHighWaterMark.GetAverage(currentTime);
}
///
/// Calculates the "comfortable" pool size based on usage patterns.
///
/// The current time.
/// The effective minimum retain count (already accounts for warm/min logic).
/// The comfortable pool size.
public int GetComfortableSize(float currentTime, int effectiveMinRetainCount)
{
return GetComfortableSize(
currentTime,
effectiveMinRetainCount,
MemoryPressureLevel.None
);
}
///
/// Calculates the "comfortable" pool size based on usage patterns and memory pressure.
///
/// The current time.
/// The effective minimum retain count (already accounts for warm/min logic).
/// The current memory pressure level.
/// The comfortable pool size, adjusted for memory pressure.
///
///
/// Memory pressure affects the buffer multiplier used:
///
/// - : Uses configured buffer multiplier
/// - : Caps buffer at 1.5x
/// - : Caps buffer at 1.0x
/// - and above: Returns effective min retain count
///
///
///
public int GetComfortableSize(
float currentTime,
int effectiveMinRetainCount,
MemoryPressureLevel pressureLevel
)
{
if (pressureLevel >= MemoryPressureLevel.High)
{
return effectiveMinRetainCount;
}
int rollingPeak = _rollingHighWaterMark.GetPeak(currentTime);
float buffer;
float rentalsPerMin;
float lastAccess;
lock (_lock)
{
buffer = _bufferMultiplier;
rentalsPerMin = _cachedRentalsPerMinute;
lastAccess = _lastRentalTime > _lastReturnTime ? _lastRentalTime : _lastReturnTime;
}
if (rentalsPerMin >= HighFrequencyThreshold)
{
buffer *= HighFrequencyBufferBoost;
}
if (pressureLevel == MemoryPressureLevel.Medium)
{
buffer = buffer > MediumPressureBufferCap ? MediumPressureBufferCap : buffer;
}
else if (pressureLevel == MemoryPressureLevel.Low)
{
buffer = buffer > LowPressureBufferCap ? LowPressureBufferCap : buffer;
}
float unusedThresholdSeconds = UnusedPoolThresholdMinutes * SecondsPerMinute;
if (lastAccess > 0f && (currentTime - lastAccess) >= unusedThresholdSeconds)
{
return effectiveMinRetainCount;
}
int bufferedSize = (int)(rollingPeak * buffer);
return bufferedSize > effectiveMinRetainCount ? bufferedSize : effectiveMinRetainCount;
}
///
/// Computes all purge-related parameters in a single lock acquisition to reduce contention.
/// This combines the logic of ,
/// ,
/// , and .
///
public PurgeParameters GetPurgeParameters(
float currentTime,
float baseIdleTimeoutSeconds,
int minRetainCount,
int warmRetainCount,
bool useIntelligent,
MemoryPressureLevel pressureLevel
)
{
lock (_lock)
{
// Frequency-adjusted idle timeout
float effectiveIdleTimeout = baseIdleTimeoutSeconds;
if (
_cachedRentalsPerMinute <= LowFrequencyThreshold
&& _cachedRentalsPerMinute > 0f
&& _totalRentalCount > 0
)
{
effectiveIdleTimeout = baseIdleTimeoutSeconds * LowFrequencyTimeoutMultiplier;
}
// Effective min retain count
int effectiveMinRetain = minRetainCount;
if (pressureLevel < MemoryPressureLevel.Medium)
{
bool isActive =
baseIdleTimeoutSeconds > 0f
&& (currentTime - _lastRentalTime) < baseIdleTimeoutSeconds;
int warmFloor = isActive ? warmRetainCount : 0;
effectiveMinRetain = warmFloor > minRetainCount ? warmFloor : minRetainCount;
}
// Comfortable size
int comfortableSize;
if (!useIntelligent)
{
comfortableSize = effectiveMinRetain;
}
else if (pressureLevel >= MemoryPressureLevel.High)
{
comfortableSize = effectiveMinRetain;
}
else
{
int rollingPeak = _rollingHighWaterMark.GetPeak(currentTime);
float buffer = _bufferMultiplier;
if (_cachedRentalsPerMinute >= HighFrequencyThreshold)
{
buffer *= HighFrequencyBufferBoost;
}
if (pressureLevel == MemoryPressureLevel.Medium)
{
buffer =
buffer > MediumPressureBufferCap ? MediumPressureBufferCap : buffer;
}
else if (pressureLevel == MemoryPressureLevel.Low)
{
buffer = buffer > LowPressureBufferCap ? LowPressureBufferCap : buffer;
}
float unusedThresholdSeconds = UnusedPoolThresholdMinutes * SecondsPerMinute;
float lastAccess =
_lastRentalTime > _lastReturnTime ? _lastRentalTime : _lastReturnTime;
if (lastAccess > 0f && (currentTime - lastAccess) >= unusedThresholdSeconds)
{
comfortableSize = effectiveMinRetain;
}
else
{
int bufferedSize = (int)(rollingPeak * buffer);
comfortableSize =
bufferedSize > effectiveMinRetain ? bufferedSize : effectiveMinRetain;
}
}
// Hysteresis check
bool inHysteresis = false;
if (useIntelligent)
{
bool ignoreForPressure = pressureLevel >= MemoryPressureLevel.High;
if (
!ignoreForPressure
&& _lastSpikeTime > 0f
&& currentTime - _lastSpikeTime < _hysteresisSeconds
)
{
inHysteresis = true;
}
}
return new PurgeParameters(
effectiveIdleTimeout,
effectiveMinRetain,
comfortableSize,
inHysteresis
);
}
}
///
/// Calculates the effective minimum retain count based on whether the pool is active.
/// Active pools use WarmRetainCount, idle pools use MinRetainCount.
///
/// The current time.
/// The idle timeout in seconds.
/// The absolute floor (MinRetainCount).
/// The warm retain count for active pools.
/// The effective minimum retain count.
public int GetEffectiveMinRetainCount(
float currentTime,
float idleTimeoutSeconds,
int minRetainCount,
int warmRetainCount
)
{
return GetEffectiveMinRetainCount(
currentTime,
idleTimeoutSeconds,
minRetainCount,
warmRetainCount,
MemoryPressureLevel.None
);
}
///
/// Calculates the effective minimum retain count based on pool activity and memory pressure.
///
/// The current time.
/// The idle timeout in seconds.
/// The absolute floor (MinRetainCount).
/// The warm retain count for active pools.
/// The current memory pressure level.
/// The effective minimum retain count, adjusted for memory pressure.
///
///
/// Memory pressure affects warm retain count:
///
/// - and : Uses full warm retain count for active pools
/// - and above: Ignores warm retain count, returns min retain count
///
///
///
public int GetEffectiveMinRetainCount(
float currentTime,
float idleTimeoutSeconds,
int minRetainCount,
int warmRetainCount,
MemoryPressureLevel pressureLevel
)
{
if (pressureLevel >= MemoryPressureLevel.Medium)
{
return minRetainCount;
}
float lastRental;
lock (_lock)
{
lastRental = _lastRentalTime;
}
bool isActive =
idleTimeoutSeconds > 0f && (currentTime - lastRental) < idleTimeoutSeconds;
int warmFloor = isActive ? warmRetainCount : 0;
return warmFloor > minRetainCount ? warmFloor : minRetainCount;
}
///
/// Checks if purging should be suppressed due to recent spike activity.
///
/// The current time.
/// true if purging should be suppressed; otherwise, false.
public bool IsInHysteresisPeriod(float currentTime)
{
lock (_lock)
{
if (_lastSpikeTime <= 0f)
{
return false;
}
return currentTime - _lastSpikeTime < _hysteresisSeconds;
}
}
///
/// Checks if this pool is high-frequency (many rentals per minute).
/// High-frequency pools benefit from larger buffers.
///
/// true if the pool is high-frequency; otherwise, false.
public bool IsHighFrequency()
{
lock (_lock)
{
return _cachedRentalsPerMinute >= HighFrequencyThreshold;
}
}
///
/// Checks if this pool is low-frequency (few rentals per minute).
/// Low-frequency pools can be purged more aggressively.
///
/// true if the pool is low-frequency; otherwise, false.
public bool IsLowFrequency()
{
lock (_lock)
{
return _cachedRentalsPerMinute <= LowFrequencyThreshold && _totalRentalCount > 0;
}
}
///
/// Checks if the pool has been unused for an extended period.
///
/// The current time.
/// true if the pool is unused; otherwise, false.
public bool IsUnused(float currentTime)
{
lock (_lock)
{
float lastAccess =
_lastRentalTime > _lastReturnTime ? _lastRentalTime : _lastReturnTime;
if (lastAccess <= 0f)
{
return false;
}
float unusedThresholdSeconds = UnusedPoolThresholdMinutes * SecondsPerMinute;
return (currentTime - lastAccess) >= unusedThresholdSeconds;
}
}
///
/// Gets the effective buffer multiplier adjusted for frequency.
/// High-frequency pools get a larger buffer, low-frequency pools get standard buffer.
///
/// The adjusted buffer multiplier.
public float GetFrequencyAdjustedBufferMultiplier()
{
lock (_lock)
{
if (_cachedRentalsPerMinute >= HighFrequencyThreshold)
{
return _bufferMultiplier * HighFrequencyBufferBoost;
}
return _bufferMultiplier;
}
}
///
/// Gets the effective idle timeout adjusted for frequency.
/// Low-frequency pools have shorter effective timeout for faster purging.
///
/// The base idle timeout in seconds.
/// The adjusted idle timeout in seconds.
public float GetFrequencyAdjustedIdleTimeout(float baseIdleTimeoutSeconds)
{
lock (_lock)
{
if (
_cachedRentalsPerMinute <= LowFrequencyThreshold
&& _cachedRentalsPerMinute > 0f
&& _totalRentalCount > 0
)
{
return baseIdleTimeoutSeconds * LowFrequencyTimeoutMultiplier;
}
return baseIdleTimeoutSeconds;
}
}
///
/// Gets a snapshot of the current frequency statistics.
///
/// The current time.
/// A snapshot of frequency metrics.
public PoolFrequencyStatistics GetFrequencyStatistics(float currentTime)
{
lock (_lock)
{
UpdateFrequencyTrackingLocked(currentTime);
float lastAccess =
_lastRentalTime > _lastReturnTime ? _lastRentalTime : _lastReturnTime;
float averageInterRentalTime =
_interRentalCount > 0
? (float)(_totalInterRentalTimeSeconds / _interRentalCount)
: 0f;
return new PoolFrequencyStatistics(
rentalsPerMinute: _cachedRentalsPerMinute,
averageInterRentalTimeSeconds: averageInterRentalTime,
lastAccessTime: lastAccess,
totalRentalCount: _totalRentalCount,
isHighFrequency: _cachedRentalsPerMinute >= HighFrequencyThreshold,
isLowFrequency: _cachedRentalsPerMinute <= LowFrequencyThreshold
&& _totalRentalCount > 0,
isUnused: lastAccess > 0f
&& (currentTime - lastAccess)
>= UnusedPoolThresholdMinutes * SecondsPerMinute
);
}
}
///
/// Clears all tracking data.
///
public void Clear()
{
lock (_lock)
{
_currentlyRented = 0;
_peakConcurrentRentals = 0;
_lastRentalTime = 0f;
_lastReturnTime = 0f;
_lastSpikeTime = 0f;
_rentalCountThisWindow = 0;
_windowStartTime = 0f;
_cachedRentalsPerMinute = 0f;
_totalRentalCount = 0;
_totalInterRentalTimeSeconds = 0;
_interRentalCount = 0;
_previousRentalTime = 0f;
_rollingHighWaterMark.Clear();
}
}
private void UpdateFrequencyTracking(float currentTime)
{
UpdateFrequencyTrackingLocked(currentTime, incrementRental: true);
}
///
/// Updates frequency tracking metrics. Must be called while holding the lock.
///
/// The current time.
///
/// When true, increments the rental count for this window (used by actual rental operations).
/// When false, only updates time-based calculations without counting a rental
/// (used by read-only statistics queries like ).
///
private void UpdateFrequencyTrackingLocked(float currentTime, bool incrementRental = false)
{
if (incrementRental)
{
_rentalCountThisWindow++;
}
if (_windowStartTime <= 0f)
{
_windowStartTime = currentTime;
}
float windowElapsed = currentTime - _windowStartTime;
if (windowElapsed >= DefaultFrequencyWindowSeconds)
{
if (windowElapsed > 0f)
{
_cachedRentalsPerMinute =
_rentalCountThisWindow * (SecondsPerMinute / DefaultFrequencyWindowSeconds);
}
_rentalCountThisWindow = incrementRental ? 1 : 0;
_windowStartTime = currentTime;
}
else if (windowElapsed > 0f)
{
float estimatedMinuteRate =
_rentalCountThisWindow * (SecondsPerMinute / windowElapsed);
_cachedRentalsPerMinute = estimatedMinuteRate;
}
}
}
///
/// Immutable snapshot of pool frequency statistics for debugging and monitoring.
///
public readonly struct PoolFrequencyStatistics : IEquatable
{
///
/// Tolerance for floating-point equality comparisons.
///
private const float FloatEqualityTolerance = 0.0001f;
///
/// Gets the current rentals-per-minute rate.
///
public float RentalsPerMinute { get; }
///
/// Gets the average time between consecutive rentals in seconds.
/// This represents the inter-arrival time between rental operations, not the duration items are held.
/// Returns 0 if fewer than two rentals have occurred.
///
public float AverageInterRentalTimeSeconds { get; }
///
/// Gets the time of the most recent access (rent or return).
///
public float LastAccessTime { get; }
///
/// Gets the total number of rentals since pool creation.
///
public long TotalRentalCount { get; }
///
/// Gets whether this pool is considered high-frequency.
///
public bool IsHighFrequency { get; }
///
/// Gets whether this pool is considered low-frequency.
///
public bool IsLowFrequency { get; }
///
/// Gets whether this pool is considered unused.
///
public bool IsUnused { get; }
///
/// Creates a new frequency statistics snapshot.
///
public PoolFrequencyStatistics(
float rentalsPerMinute,
float averageInterRentalTimeSeconds,
float lastAccessTime,
long totalRentalCount,
bool isHighFrequency,
bool isLowFrequency,
bool isUnused
)
{
RentalsPerMinute = rentalsPerMinute;
AverageInterRentalTimeSeconds = averageInterRentalTimeSeconds;
LastAccessTime = lastAccessTime;
TotalRentalCount = totalRentalCount;
IsHighFrequency = isHighFrequency;
IsLowFrequency = isLowFrequency;
IsUnused = isUnused;
}
///
public bool Equals(PoolFrequencyStatistics other)
{
return Math.Abs(RentalsPerMinute - other.RentalsPerMinute) < FloatEqualityTolerance
&& Math.Abs(AverageInterRentalTimeSeconds - other.AverageInterRentalTimeSeconds)
< FloatEqualityTolerance
&& Math.Abs(LastAccessTime - other.LastAccessTime) < FloatEqualityTolerance
&& TotalRentalCount == other.TotalRentalCount
&& IsHighFrequency == other.IsHighFrequency
&& IsLowFrequency == other.IsLowFrequency
&& IsUnused == other.IsUnused;
}
///
public override bool Equals(object obj)
{
return obj is PoolFrequencyStatistics other && Equals(other);
}
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
{
return Objects.HashCode(
RentalsPerMinute,
AverageInterRentalTimeSeconds,
LastAccessTime,
TotalRentalCount,
IsHighFrequency,
IsLowFrequency,
IsUnused
);
}
///
/// Determines whether two instances are equal.
///
public static bool operator ==(PoolFrequencyStatistics left, PoolFrequencyStatistics right)
{
return left.Equals(right);
}
///
/// Determines whether two instances are not equal.
///
public static bool operator !=(PoolFrequencyStatistics left, PoolFrequencyStatistics right)
{
return !left.Equals(right);
}
///
public override string ToString()
{
return $"PoolFrequencyStatistics(RentalsPerMin={RentalsPerMinute:F2}, "
+ $"AvgInterRentalTime={AverageInterRentalTimeSeconds:F3}s, "
+ $"LastAccess={LastAccessTime:F2}s, Total={TotalRentalCount}, "
+ $"High={IsHighFrequency}, Low={IsLowFrequency}, Unused={IsUnused})";
}
}
}