// 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})"; } } }