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