// 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.Diagnostics; using System.Threading; /// /// Monitors system memory pressure and provides proactive memory management for pool purging. /// /// /// /// This class tracks memory usage via and GC collection frequency /// to detect memory pressure before the system triggers . /// /// /// Memory pressure detection is based on three factors: /// /// Absolute memory usage compared to /// GC collection rate (rapid Gen0 collections indicate memory stress) /// Memory growth rate between checks /// /// /// /// The monitor is designed to be lightweight and can be called frequently. It throttles actual /// pressure calculations based on to avoid overhead. /// /// /// = MemoryPressureLevel.High) /// { /// // Take action /// } /// ]]> /// /// public static class MemoryPressureMonitor { /// /// Default memory threshold in bytes (512MB). /// public const long DefaultMemoryPressureThresholdBytes = 512L * 1024 * 1024; /// /// Default check interval in seconds (5 seconds). /// public const float DefaultCheckIntervalSeconds = 5f; /// /// Default GC collection rate threshold (collections per second) that indicates stress. /// public const float DefaultGCCollectionRateThreshold = 2f; /// /// Default memory growth rate threshold (bytes per second) that indicates stress. /// public const long DefaultMemoryGrowthRateThreshold = 50L * 1024 * 1024; // 50MB/s private static int _enabled = 1; private static long _memoryPressureThresholdBytes = DefaultMemoryPressureThresholdBytes; private static float _checkIntervalSeconds = DefaultCheckIntervalSeconds; private static float _gcCollectionRateThreshold = DefaultGCCollectionRateThreshold; private static long _memoryGrowthRateThreshold = DefaultMemoryGrowthRateThreshold; private static long _lastTotalMemory; private static int _lastGCCount; private static float _lastCheckTime; private static int _currentPressure; private static readonly Stopwatch MonitorStopwatch = Stopwatch.StartNew(); private static readonly object UpdateLock = new(); // Memory ratio thresholds for pressure calculation private const float CriticalMemoryRatio = 1.25f; private const float HighMemoryRatio = 1.0f; private const float MediumMemoryRatio = 0.9f; private const float LowMemoryRatio = 0.75f; // Pressure score thresholds for level determination private const int CriticalScoreThreshold = 6; private const int HighScoreThreshold = 4; private const int MediumScoreThreshold = 2; private const int LowScoreThreshold = 1; // Score contributions for memory ratio private const int CriticalMemoryScoreContribution = 4; private const int HighMemoryScoreContribution = 3; private const int MediumMemoryScoreContribution = 2; private const int LowMemoryScoreContribution = 1; // Score contributions for GC and growth rates private const int HighGCRateScoreContribution = 2; private const int MediumGCRateScoreContribution = 1; private const int HighGrowthRateScoreContribution = 2; private const int MediumGrowthRateScoreContribution = 1; // Multipliers for rate thresholds private const float HighGCRateMultiplier = 3f; private const float HighGrowthRateMultiplier = 2f; /// /// Calculates the pressure level from provided metrics without querying the GC. /// Used for testing pressure calculation logic with controlled inputs. /// /// Current memory as ratio of threshold (e.g., 0.9 = 90% of threshold). /// GC rate as multiple of threshold (e.g., 2.0 = 2x the threshold rate). /// Growth rate as multiple of threshold (e.g., 1.5 = 1.5x the threshold rate). /// The calculated pressure level. internal static MemoryPressureLevel CalculatePressureFromMetrics( float memoryRatio, float gcRateMultiplier, float growthRateMultiplier ) { int pressureScore = 0; if (memoryRatio >= CriticalMemoryRatio) { pressureScore += CriticalMemoryScoreContribution; } else if (memoryRatio >= HighMemoryRatio) { pressureScore += HighMemoryScoreContribution; } else if (memoryRatio >= MediumMemoryRatio) { pressureScore += MediumMemoryScoreContribution; } else if (memoryRatio >= LowMemoryRatio) { pressureScore += LowMemoryScoreContribution; } if (gcRateMultiplier >= HighGCRateMultiplier) { pressureScore += HighGCRateScoreContribution; } else if (gcRateMultiplier >= 1f) { pressureScore += MediumGCRateScoreContribution; } if (growthRateMultiplier >= HighGrowthRateMultiplier) { pressureScore += HighGrowthRateScoreContribution; } else if (growthRateMultiplier >= 1f) { pressureScore += MediumGrowthRateScoreContribution; } if (pressureScore >= CriticalScoreThreshold) { return MemoryPressureLevel.Critical; } if (pressureScore >= HighScoreThreshold) { return MemoryPressureLevel.High; } if (pressureScore >= MediumScoreThreshold) { return MemoryPressureLevel.Medium; } if (pressureScore >= LowScoreThreshold) { return MemoryPressureLevel.Low; } return MemoryPressureLevel.None; } /// /// Gets or sets whether memory pressure monitoring is enabled. /// When disabled, always returns . /// Default is true. /// public static bool Enabled { get => Volatile.Read(ref _enabled) != 0; set => Volatile.Write(ref _enabled, value ? 1 : 0); } /// /// Gets or sets the memory threshold in bytes above which pressure increases. /// Default is 512MB. /// /// /// /// Memory pressure levels are calculated as percentages of this threshold: /// /// None: Below 75% of threshold /// Low: 75-90% of threshold /// Medium: 90-100% of threshold /// High: 100-125% of threshold /// Critical: Above 125% of threshold /// /// /// /// Set this value based on your target platform's available memory. For mobile platforms, /// consider using lower thresholds (128-256MB). For desktop, higher thresholds may be appropriate. /// /// public static long MemoryPressureThresholdBytes { get => Volatile.Read(ref _memoryPressureThresholdBytes); set => Volatile.Write( ref _memoryPressureThresholdBytes, value > 0 ? value : DefaultMemoryPressureThresholdBytes ); } /// /// Gets or sets the minimum interval in seconds between pressure calculations. /// Default is 5 seconds. /// /// /// Lower values provide more responsive pressure detection but increase CPU overhead. /// Higher values reduce overhead but may miss rapid memory changes. /// public static float CheckIntervalSeconds { get => Volatile.Read(ref _checkIntervalSeconds); set => Volatile.Write( ref _checkIntervalSeconds, value > 0f ? value : DefaultCheckIntervalSeconds ); } /// /// Gets or sets the GC collection rate threshold (collections per second) that contributes to pressure. /// Default is 2.0 (two Gen0 collections per second). /// /// /// Frequent GC collections indicate memory churn and stress even if absolute usage is low. /// This threshold helps detect rapid allocation patterns that benefit from pool purging. /// public static float GCCollectionRateThreshold { get => Volatile.Read(ref _gcCollectionRateThreshold); set => Volatile.Write( ref _gcCollectionRateThreshold, value > 0f ? value : DefaultGCCollectionRateThreshold ); } /// /// Gets or sets the memory growth rate threshold (bytes per second) that contributes to pressure. /// Default is 50MB per second. /// /// /// Rapid memory growth indicates potential memory issues even if absolute usage is below threshold. /// This helps detect runaway allocation patterns before they become critical. /// public static long MemoryGrowthRateThreshold { get => Volatile.Read(ref _memoryGrowthRateThreshold); set => Volatile.Write( ref _memoryGrowthRateThreshold, value > 0 ? value : DefaultMemoryGrowthRateThreshold ); } /// /// Gets the current memory pressure level. /// /// /// This property returns the most recently calculated pressure level. /// Call to refresh the calculation if the check interval has elapsed. /// When is false, always returns . /// public static MemoryPressureLevel CurrentPressure { get { if (!Enabled) { return MemoryPressureLevel.None; } return (MemoryPressureLevel)Volatile.Read(ref _currentPressure); } } /// /// Gets the total memory currently in use, as reported by the last check. /// public static long LastTotalMemory => Volatile.Read(ref _lastTotalMemory); /// /// Gets the Gen0 GC collection count at the time of the last check. /// public static int LastGCCount => Volatile.Read(ref _lastGCCount); /// /// Updates the memory pressure calculation if the check interval has elapsed. /// /// /// /// This method is designed to be called frequently (e.g., on every pool operation). /// It internally throttles actual pressure calculations based on . /// /// /// The calculation considers: /// /// Absolute memory usage vs threshold /// GC collection rate (rapid collections = higher pressure) /// Memory growth rate (rapid growth = higher pressure) /// /// /// public static void Update() { if (!Enabled) { return; } float currentTime = (float)MonitorStopwatch.Elapsed.TotalSeconds; float checkInterval = CheckIntervalSeconds; if (currentTime - Volatile.Read(ref _lastCheckTime) < checkInterval) { return; } lock (UpdateLock) { float lastCheck = _lastCheckTime; if (currentTime - lastCheck < checkInterval) { return; } CalculatePressure(currentTime, lastCheck); _lastCheckTime = currentTime; } } /// /// Forces an immediate recalculation of memory pressure, bypassing the check interval. /// /// The newly calculated pressure level. /// /// Use this method sparingly as it involves GC queries that have some overhead. /// Prefer calling which respects the throttling interval. /// public static MemoryPressureLevel ForceUpdate() { if (!Enabled) { return MemoryPressureLevel.None; } float currentTime = (float)MonitorStopwatch.Elapsed.TotalSeconds; lock (UpdateLock) { CalculatePressure(currentTime, _lastCheckTime); _lastCheckTime = currentTime; } return CurrentPressure; } /// /// Resets all monitoring state and settings to defaults. /// /// /// Primarily used for testing. Clears tracked memory values and resets all configuration. /// public static void Reset() { lock (UpdateLock) { _enabled = 1; _memoryPressureThresholdBytes = DefaultMemoryPressureThresholdBytes; _checkIntervalSeconds = DefaultCheckIntervalSeconds; _gcCollectionRateThreshold = DefaultGCCollectionRateThreshold; _memoryGrowthRateThreshold = DefaultMemoryGrowthRateThreshold; _lastTotalMemory = 0; _lastGCCount = 0; _lastCheckTime = 0f; _currentPressure = 0; } } private static void CalculatePressure(float currentTime, float lastCheckTime) { long totalMemory = GC.GetTotalMemory(false); int gcCount = GC.CollectionCount(0); long previousMemory = _lastTotalMemory; int previousGCCount = _lastGCCount; _lastTotalMemory = totalMemory; _lastGCCount = gcCount; float elapsed = currentTime - lastCheckTime; if (elapsed <= 0f) { elapsed = CheckIntervalSeconds; } int pressureScore = 0; long threshold = MemoryPressureThresholdBytes; if (threshold > 0) { float memoryRatio = (float)totalMemory / threshold; if (memoryRatio >= CriticalMemoryRatio) { pressureScore += CriticalMemoryScoreContribution; } else if (memoryRatio >= HighMemoryRatio) { pressureScore += HighMemoryScoreContribution; } else if (memoryRatio >= MediumMemoryRatio) { pressureScore += MediumMemoryScoreContribution; } else if (memoryRatio >= LowMemoryRatio) { pressureScore += LowMemoryScoreContribution; } } if (previousGCCount > 0) { int gcDelta = gcCount - previousGCCount; if (gcDelta > 0) { float gcRate = gcDelta / elapsed; float gcRateThreshold = GCCollectionRateThreshold; if (gcRate >= gcRateThreshold * HighGCRateMultiplier) { pressureScore += HighGCRateScoreContribution; } else if (gcRate >= gcRateThreshold) { pressureScore += MediumGCRateScoreContribution; } } } if (previousMemory > 0) { long memoryDelta = totalMemory - previousMemory; if (memoryDelta > 0) { float growthRate = memoryDelta / elapsed; long growthThreshold = MemoryGrowthRateThreshold; if (growthRate >= growthThreshold * HighGrowthRateMultiplier) { pressureScore += HighGrowthRateScoreContribution; } else if (growthRate >= growthThreshold) { pressureScore += MediumGrowthRateScoreContribution; } } } MemoryPressureLevel level; if (pressureScore >= CriticalScoreThreshold) { level = MemoryPressureLevel.Critical; } else if (pressureScore >= HighScoreThreshold) { level = MemoryPressureLevel.High; } else if (pressureScore >= MediumScoreThreshold) { level = MemoryPressureLevel.Medium; } else if (pressureScore >= LowScoreThreshold) { level = MemoryPressureLevel.Low; } else { level = MemoryPressureLevel.None; } Volatile.Write(ref _currentPressure, (int)level); } } }