diff --git a/src/libraries/System.Runtime.Caching/src/System.Runtime.Caching.csproj b/src/libraries/System.Runtime.Caching/src/System.Runtime.Caching.csproj
index b212c942546be5..056b3ddfe34655 100644
--- a/src/libraries/System.Runtime.Caching/src/System.Runtime.Caching.csproj
+++ b/src/libraries/System.Runtime.Caching/src/System.Runtime.Caching.csproj
@@ -83,6 +83,7 @@ System.Runtime.Caching.ObjectCache
+
diff --git a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/Configuration/ConfigUtil.cs b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/Configuration/ConfigUtil.cs
index 6110d03c42ba08..c6fb0cb46fdcaa 100644
--- a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/Configuration/ConfigUtil.cs
+++ b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/Configuration/ConfigUtil.cs
@@ -13,6 +13,7 @@ internal static class ConfigUtil
{
internal const string CacheMemoryLimitMegabytes = "cacheMemoryLimitMegabytes";
internal const string PhysicalMemoryLimitPercentage = "physicalMemoryLimitPercentage";
+ internal const string PhysicalMemoryMode = "physicalMemoryMode";
internal const string PollingInterval = "pollingInterval";
internal const string UseMemoryCacheManager = "useMemoryCacheManager";
internal const string ThrowOnDisposed = "throwOnDisposed";
@@ -93,5 +94,21 @@ internal static bool GetBooleanValue(NameValueCollection config, string valueNam
return bValue;
}
+
+ internal static string GetStringValue(NameValueCollection config, string valueName, string defaultValue)
+ {
+ string sValue = config[valueName];
+ return sValue ?? defaultValue;
+ }
+
+ internal static PhysicalMemoryMode ParsePhysicalMemoryMode(string rawValue)
+ {
+ if (!Enum.TryParse(rawValue, true, out PhysicalMemoryMode mode))
+ {
+ throw new ArgumentException(null, ConfigUtil.PhysicalMemoryMode);
+ }
+
+ return mode;
+ }
}
}
diff --git a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/Configuration/MemoryCacheElement.cs b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/Configuration/MemoryCacheElement.cs
index 9fcfd59126aa40..476f217851ba29 100644
--- a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/Configuration/MemoryCacheElement.cs
+++ b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/Configuration/MemoryCacheElement.cs
@@ -4,10 +4,32 @@
using System;
using System.ComponentModel;
using System.Configuration;
+using System.Runtime.Caching.Resources;
using System.Runtime.Versioning;
namespace System.Runtime.Caching.Configuration
{
+ ///
+ /// Defines the physical memory monitoring modes for the cache.
+ ///
+ internal enum PhysicalMemoryMode
+ {
+ ///
+ /// Legacy mode - uses platform-specific memory detection with GC-induced stats on non-Windows.
+ ///
+ Legacy = 0,
+
+ ///
+ /// Standard mode - uses GCMemoryInfo without inducing GC collections.
+ ///
+ Standard = 1,
+
+ ///
+ /// GC thresholds mode - uses GCMemoryInfo.HighMemoryLoadThresholdBytes instead of percentage of total memory.
+ ///
+ GCThresholds = 2
+ }
+
#if NETCOREAPP
[UnsupportedOSPlatform("browser")]
#endif
@@ -28,6 +50,13 @@ internal sealed class MemoryCacheElement : ConfigurationElement
null,
new IntegerValidator(0, 100),
ConfigurationPropertyOptions.None);
+ private static readonly ConfigurationProperty s_propPhysicalMemoryMode =
+ new ConfigurationProperty("physicalMemoryMode",
+ typeof(string),
+ "Legacy",
+ new GenericEnumConverter(typeof(PhysicalMemoryMode)),
+ null,
+ ConfigurationPropertyOptions.None);
private static readonly ConfigurationProperty s_propCacheMemoryLimitMegabytes =
new ConfigurationProperty("cacheMemoryLimitMegabytes",
typeof(int),
@@ -46,6 +75,7 @@ internal sealed class MemoryCacheElement : ConfigurationElement
{
s_propName,
s_propPhysicalMemoryLimitPercentage,
+ s_propPhysicalMemoryMode,
s_propCacheMemoryLimitMegabytes,
s_propPollingInterval
};
@@ -82,6 +112,10 @@ public string Name
}
}
+ ///
+ /// Gets or sets the percentage of physical memory that can be used before cache entries are removed.
+ /// Valid values: 0 (auto-calculated defaults), 1-100 (specific percentage of physical memory).
+ ///
[ConfigurationProperty("physicalMemoryLimitPercentage", DefaultValue = (int)0)]
[IntegerValidator(MinValue = 0, MaxValue = 100)]
public int PhysicalMemoryLimitPercentage
@@ -96,6 +130,22 @@ public int PhysicalMemoryLimitPercentage
}
}
+ ///
+ /// Gets or sets the physical memory monitoring mode.
+ /// Valid values:
+ /// - "Legacy": Platform-specific memory detection (default)
+ /// - "Standard": Use GC.GetGCMemoryInfo().TotalAvailableMemoryBytes without inducing GC
+ /// - "GCThresholds": Follow GC's high memory load threshold
+ ///
+ [ConfigurationProperty("physicalMemoryMode", DefaultValue = "Legacy")]
+ internal PhysicalMemoryMode PhysicalMemoryMode
+ {
+ get
+ {
+ return (PhysicalMemoryMode)base["physicalMemoryMode"];
+ }
+ }
+
[ConfigurationProperty("cacheMemoryLimitMegabytes", DefaultValue = (int)0)]
[IntegerValidator(MinValue = 0)]
public int CacheMemoryLimitMegabytes
diff --git a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/Configuration/MemoryCacheSection.cs b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/Configuration/MemoryCacheSection.cs
index 05e0387374ec8d..13be4dda748364 100644
--- a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/Configuration/MemoryCacheSection.cs
+++ b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/Configuration/MemoryCacheSection.cs
@@ -13,9 +13,9 @@ namespace System.Runtime.Caching.Configuration
-
-
-
+
+
+
diff --git a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/MemoryCacheStatistics.cs b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/MemoryCacheStatistics.cs
index 96cfc3bda655bd..d6ccc33bfed151 100644
--- a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/MemoryCacheStatistics.cs
+++ b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/MemoryCacheStatistics.cs
@@ -21,6 +21,7 @@ internal sealed class MemoryCacheStatistics : IDisposable
private int _configCacheMemoryLimitMegabytes;
private int _configPhysicalMemoryLimitPercentage;
+ private PhysicalMemoryMode _configPhysicalMemoryMode;
private int _configPollingInterval;
private int _inCacheManagerThread;
private int _disposed;
@@ -137,6 +138,7 @@ private void InitializeConfiguration(NameValueCollection config)
{
_configCacheMemoryLimitMegabytes = element.CacheMemoryLimitMegabytes;
_configPhysicalMemoryLimitPercentage = element.PhysicalMemoryLimitPercentage;
+ _configPhysicalMemoryMode = element.PhysicalMemoryMode;
double milliseconds = element.PollingInterval.TotalMilliseconds;
_configPollingInterval = (milliseconds < (double)int.MaxValue) ? (int)milliseconds : int.MaxValue;
}
@@ -145,6 +147,7 @@ private void InitializeConfiguration(NameValueCollection config)
_configPollingInterval = ConfigUtil.DefaultPollingTimeMilliseconds;
_configCacheMemoryLimitMegabytes = 0;
_configPhysicalMemoryLimitPercentage = 0;
+ _configPhysicalMemoryMode = PhysicalMemoryMode.Legacy;
}
if (config != null)
@@ -152,6 +155,13 @@ private void InitializeConfiguration(NameValueCollection config)
_configPollingInterval = ConfigUtil.GetIntValueFromTimeSpan(config, ConfigUtil.PollingInterval, _configPollingInterval);
_configCacheMemoryLimitMegabytes = ConfigUtil.GetIntValue(config, ConfigUtil.CacheMemoryLimitMegabytes, _configCacheMemoryLimitMegabytes, true, int.MaxValue);
_configPhysicalMemoryLimitPercentage = ConfigUtil.GetIntValue(config, ConfigUtil.PhysicalMemoryLimitPercentage, _configPhysicalMemoryLimitPercentage, true, 100);
+
+ // Handle physicalMemoryMode from config
+ string physicalMemoryModeRaw = ConfigUtil.GetStringValue(config, ConfigUtil.PhysicalMemoryMode, "Legacy");
+ if (!string.IsNullOrWhiteSpace(physicalMemoryModeRaw))
+ {
+ _configPhysicalMemoryMode = ConfigUtil.ParsePhysicalMemoryMode(physicalMemoryModeRaw);
+ }
}
#if !NETCOREAPP
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _configPhysicalMemoryLimitPercentage > 0)
@@ -261,7 +271,7 @@ internal MemoryCacheStatistics(MemoryCache memoryCache, NameValueCollection conf
_timerLock = new object();
InitializeConfiguration(config);
_pollingInterval = _configPollingInterval;
- _physicalMemoryMonitor = new PhysicalMemoryMonitor(_configPhysicalMemoryLimitPercentage);
+ _physicalMemoryMonitor = new PhysicalMemoryMonitor(_configPhysicalMemoryLimitPercentage, _configPhysicalMemoryMode);
InitDisposableMembers();
}
@@ -359,6 +369,14 @@ internal void UpdateConfig(NameValueCollection config)
int cacheMemoryLimitMegabytes = ConfigUtil.GetIntValue(config, ConfigUtil.CacheMemoryLimitMegabytes, _configCacheMemoryLimitMegabytes, true, int.MaxValue);
int physicalMemoryLimitPercentage = ConfigUtil.GetIntValue(config, ConfigUtil.PhysicalMemoryLimitPercentage, _configPhysicalMemoryLimitPercentage, true, 100);
+ // Parse physicalMemoryMode from config
+ PhysicalMemoryMode physicalMemoryMode = _configPhysicalMemoryMode;
+ string physicalMemoryModeRaw = ConfigUtil.GetStringValue(config, ConfigUtil.PhysicalMemoryMode, "Legacy");
+ if (!string.IsNullOrWhiteSpace(physicalMemoryModeRaw))
+ {
+ physicalMemoryMode = ConfigUtil.ParsePhysicalMemoryMode(physicalMemoryModeRaw);
+ }
+
if (pollingInterval != _configPollingInterval)
{
lock (_timerLock)
@@ -368,7 +386,8 @@ internal void UpdateConfig(NameValueCollection config)
}
if (cacheMemoryLimitMegabytes == _configCacheMemoryLimitMegabytes
- && physicalMemoryLimitPercentage == _configPhysicalMemoryLimitPercentage)
+ && physicalMemoryLimitPercentage == _configPhysicalMemoryLimitPercentage
+ && physicalMemoryMode == _configPhysicalMemoryMode)
{
return;
}
@@ -393,10 +412,12 @@ internal void UpdateConfig(NameValueCollection config)
_cacheMemoryMonitor.SetLimit(cacheMemoryLimitMegabytes);
_configCacheMemoryLimitMegabytes = cacheMemoryLimitMegabytes;
}
- if (physicalMemoryLimitPercentage != _configPhysicalMemoryLimitPercentage)
+ if (physicalMemoryLimitPercentage != _configPhysicalMemoryLimitPercentage
+ || physicalMemoryMode != _configPhysicalMemoryMode)
{
- _physicalMemoryMonitor.SetLimit(physicalMemoryLimitPercentage);
+ _physicalMemoryMonitor.SetLimit(physicalMemoryLimitPercentage, physicalMemoryMode);
_configPhysicalMemoryLimitPercentage = physicalMemoryLimitPercentage;
+ _configPhysicalMemoryMode = physicalMemoryMode;
}
}
}
diff --git a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/MemoryMonitor.Unix.cs b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/MemoryMonitor.Unix.cs
new file mode 100644
index 00000000000000..f8200064d8f94a
--- /dev/null
+++ b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/MemoryMonitor.Unix.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Specialized;
+using System.Security;
+using System.Runtime.InteropServices;
+
+
+namespace System.Runtime.Caching
+{
+ internal abstract partial class MemoryMonitor
+ {
+#if NETCOREAPP
+#pragma warning disable CA1810 // explicit static cctor
+ static MemoryMonitor()
+ {
+ // Get stats from GC.
+ GCMemoryInfo memInfo = GC.GetGCMemoryInfo();
+
+ // s_totalPhysical/TotalPhysical are used in two places:
+ // 1. PhysicalMemoryMonitor - to determine the high pressure level. We really just need to know if we have more memory than a 2005-era x86 machine.
+ // 2. CacheMemoryMonitor - for setting the "Auto" memory limit of the cache size - which is never enforced anyway because we don't have SRef's in .NET Core.
+ // Which is to say, it's OK if 'TotalAvailableMemoryBytes' is not an exact representation of the physical memory on the machine, as long as it is in the ballpark of magnitude.
+ s_totalPhysical = memInfo.TotalAvailableMemoryBytes;
+
+ // s_totalVirtual/TotalVirtual on the other hand is only used to decide if an x86 Windows machine is running in /3GB mode or not...
+ // ... but only so it can appropriately set the "Auto" memory limit of the cache size - which again, is never enforced in .Net Core.
+ // So we don't need to worry about it here.
+ //s_totalVirtual = default(long);
+ }
+#pragma warning restore CA1810
+#endif
+ }
+}
diff --git a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/PhysicalMemoryMonitor.Unix.cs b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/PhysicalMemoryMonitor.Unix.cs
index 9629e440067137..2c62994532ab00 100644
--- a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/PhysicalMemoryMonitor.Unix.cs
+++ b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/PhysicalMemoryMonitor.Unix.cs
@@ -25,7 +25,7 @@ internal sealed partial class PhysicalMemoryMonitor : MemoryMonitor
#if NETCOREAPP
private int lastGCCount;
- protected override int GetCurrentPressure()
+ private int LegacyGetCurrentPressure()
{
// Try to refresh GC stats if they haven't been updated since our last check.
int ccount = GC.CollectionCount(0);
@@ -39,7 +39,7 @@ protected override int GetCurrentPressure()
// Get stats from GC.
GCMemoryInfo memInfo = GC.GetGCMemoryInfo();
- if (memInfo.TotalAvailableMemoryBytes >= memInfo.MemoryLoadBytes)
+ if (memInfo.TotalAvailableMemoryBytes > memInfo.MemoryLoadBytes)
{
int memoryLoad = (int)((float)memInfo.MemoryLoadBytes * 100.0 / (float)memInfo.TotalAvailableMemoryBytes);
return Math.Max(1, memoryLoad);
@@ -50,7 +50,7 @@ protected override int GetCurrentPressure()
return (memInfo.MemoryLoadBytes > 0) ? 100 : 0;
}
#else
- protected override int GetCurrentPressure() => 0;
+ private static int LegacyGetCurrentPressure() => 0;
#endif
}
}
diff --git a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/PhysicalMemoryMonitor.Windows.cs b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/PhysicalMemoryMonitor.Windows.cs
index 6aff99b66b19d3..3194c9d6e64b45 100644
--- a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/PhysicalMemoryMonitor.Windows.cs
+++ b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/PhysicalMemoryMonitor.Windows.cs
@@ -10,7 +10,7 @@ namespace System.Runtime.Caching
{
internal sealed partial class PhysicalMemoryMonitor : MemoryMonitor
{
- protected override unsafe int GetCurrentPressure()
+ private static unsafe int LegacyGetCurrentPressure()
{
Interop.Kernel32.MEMORYSTATUSEX memoryStatus = default;
memoryStatus.dwLength = (uint)sizeof(Interop.Kernel32.MEMORYSTATUSEX);
diff --git a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/PhysicalMemoryMonitor.cs b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/PhysicalMemoryMonitor.cs
index 99f2855dd9aa94..6960dd0f8455a0 100644
--- a/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/PhysicalMemoryMonitor.cs
+++ b/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/PhysicalMemoryMonitor.cs
@@ -17,6 +17,16 @@ internal sealed partial class PhysicalMemoryMonitor : MemoryMonitor
private const int MinTotalMemoryTrimPercent = 10;
private const long TargetTotalMemoryTrimIntervalTicks = 5 * TimeSpan.TicksPerMinute;
+ // Controls memory monitoring behavior based on the PhysicalMemoryMode enum
+ private PhysicalMemoryMode _physicalMemoryMode;
+
+#if NETCOREAPP
+ // Track heap size after trimming to measure effectiveness in GCThresholds mode
+ private GCMemoryInfo? _lastTrimMemInfo;
+
+ private static long GetGCThresholdsLimit(GCMemoryInfo memInfo) => Math.Min(memInfo.TotalAvailableMemoryBytes, memInfo.HighMemoryLoadThresholdBytes);
+#endif
+
// Returns the percentage of physical machine memory that can be consumed by an
// application before the cache starts forcibly removing items.
internal long MemoryLimit
@@ -29,7 +39,52 @@ private PhysicalMemoryMonitor()
// hide default ctor
}
- internal PhysicalMemoryMonitor(int physicalMemoryLimitPercentage)
+ internal PhysicalMemoryMonitor(int physicalMemoryLimitPercentage, PhysicalMemoryMode physicalMemoryMode)
+ {
+ SetLimit(physicalMemoryLimitPercentage, physicalMemoryMode);
+ InitHistory();
+ }
+
+ internal void SetLimit(int physicalMemoryLimitPercentage, PhysicalMemoryMode physicalMemoryMode)
+ {
+#if NETCOREAPP
+ _physicalMemoryMode = physicalMemoryMode;
+#else
+ // For non-netcoreapp, we only support Legacy mode because the GC.GetGCMemoryInfo API is not available.
+ _physicalMemoryMode = PhysicalMemoryMode.Legacy;
+ if (physicalMemoryMode != PhysicalMemoryMode.Legacy)
+ {
+ throw new PlatformNotSupportedException("PhysicalMemoryMonitor only supports Legacy mode on non-netcoreapp platforms.");
+ }
+#endif // NETCOREAPP
+
+ if (physicalMemoryLimitPercentage == 0)
+ {
+ UseDefaultLimits();
+ }
+ else
+ {
+#if NETCOREAPP
+ // The GC.GetGCMemoryInfo API is available only in .NET Core
+ if (_physicalMemoryMode == PhysicalMemoryMode.GCThresholds)
+ {
+ // GCThresholds mode always targets HighMemoryLoadThresholdBytes, but we can still adjust the low threshold.
+ _pressureHigh = 100;
+ _pressureLow = Math.Max(3, physicalMemoryLimitPercentage);
+ }
+ else
+#endif
+ {
+ // Legacy and Standard modes: Use specified percentage with appropriate monitoring
+ _pressureHigh = Math.Max(3, physicalMemoryLimitPercentage);
+ _pressureLow = Math.Max(1, _pressureHigh - 9);
+ }
+ }
+
+ Dbg.Trace("MemoryCacheStats", $"PhysicalMemoryMonitor.SetLimit: _pressureHigh={_pressureHigh}, _pressureLow={_pressureLow}, mode={_physicalMemoryMode}");
+ }
+
+ private void UseDefaultLimits()
{
/*
The chart below shows physical memory in megabytes, and the 1, 3, and 10% values.
@@ -68,46 +123,154 @@ 8192 81.92 245.76 819.2
{
_pressureHigh = 95;
}
-
_pressureLow = _pressureHigh - 9;
- SetLimit(physicalMemoryLimitPercentage);
- InitHistory();
+#if NETCOREAPP
+ // The GC.GetGCMemoryInfo API is available only in .NET Core
+ if (_physicalMemoryMode == PhysicalMemoryMode.GCThresholds)
+ {
+ // Set "high" to 100% because we want to use the GC's high memory load threshold exactly.
+ // But set "low" based on the previous size-adjusted "high" to maintain a comfortable - but not too large - buffer below
+ // the limit, allowing the cache to operate efficiently within a safe memory range.
+ _pressureLow = 200 - (2 * _pressureHigh);
+ _pressureHigh = 100;
+ }
+#endif
}
+ protected override int GetCurrentPressure()
+ {
+ int currentPressure = CalculateCurrentPressure();
+
+#if NETCOREAPP
+ if (currentPressure < PressureHigh) // Reset in any mode. No ill effects in Legacy/Standard modes.
+ {
+ // Not above high pressure - reset tracking
+ _lastTrimMemInfo = null;
+ }
+#endif
+
+ return currentPressure;
+ }
+
+#pragma warning disable CA1822 // Mark members as static
+ private int CalculateCurrentPressure()
+ {
+#if NETCOREAPP
+ // Modern GC-based monitoring for Standard and GCThresholds modes
+ if (_physicalMemoryMode != PhysicalMemoryMode.Legacy)
+ {
+ // Get stats from GC without inducing collection
+ GCMemoryInfo memInfo = GC.GetGCMemoryInfo();
+
+ long limit = memInfo.TotalAvailableMemoryBytes;
+ if (_physicalMemoryMode == PhysicalMemoryMode.GCThresholds)
+ {
+ // GCThresholds mode: Use GC's high memory load threshold
+ limit = GetGCThresholdsLimit(memInfo);
+ }
+
+ if (limit > memInfo.MemoryLoadBytes)
+ {
+ int memoryLoad = (int)((float)memInfo.MemoryLoadBytes * 100.0 / (float)limit);
+ return Math.Max(1, memoryLoad);
+ }
+
+ return (memInfo.MemoryLoadBytes > 0) ? 100 : 0;
+ }
+#endif // NETCOREAPP
+
+ // Legacy mode: Platform-specific implementation
+ return LegacyGetCurrentPressure();
+ }
+#pragma warning restore CA1822 // Mark members as static
+
+#if NETCOREAPP
+ // In GCThresholds mode, we want to try and account for heap reductions that may not yet
+ // be reflected in `MemoryLoadBytes` when determining if we're above high pressure. (The GC
+ // may use optimizations that delay uncommitting memory back to the OS even after a trim.)
+ // It's ok to use a possibly lagging `MemoryLoadBytes` outside of this class - it's only used
+ // to determine the monitor/timer interval... which probably should still speed up if the
+ // official `MemoryLoadBytes` is high, even if we think we know better.
+ internal bool IsReallyAboveHighPressure(GCMemoryInfo currentMemInfo)
+ {
+ var baseIsAboveHighPressure = base.IsAboveHighPressure();
+
+ // Use base implementation for non-GCThresholds modes, or when
+ // even the official metric says there's no pressure, or when
+ // we haven't had a previous trim to compare `HeapSizeBytes` against.
+ if ((_physicalMemoryMode != PhysicalMemoryMode.GCThresholds)
+ || !baseIsAboveHighPressure
+ || _lastTrimMemInfo is not GCMemoryInfo lastTrimInfo)
+ {
+ return baseIsAboveHighPressure;
+ }
+
+ // Trim has already happened since entering high pressure - check for adequate heap reduction
+ long heapReduction = lastTrimInfo.HeapSizeBytes - currentMemInfo.HeapSizeBytes;
+
+ // If heap hasn't reduced, we're still under pressure
+ if (heapReduction <= 0)
+ {
+ return true;
+ }
+
+ // If heap hasn't reduced enough to drop below target, we're still under pressure
+ long estimatedMemoryLoad = currentMemInfo.MemoryLoadBytes - heapReduction;
+ long targetMemoryLoad = (long)(((float)_pressureLow / 100.0) * GetGCThresholdsLimit(currentMemInfo));
+
+ // If the estimated load is below target, we're likely not under real pressure anymore
+ return estimatedMemoryLoad >= targetMemoryLoad;
+ }
+#endif // NETCOREAPP
+
internal override int GetPercentToTrim(DateTime lastTrimTime, int lastTrimPercent)
{
int percent = 0;
- if (IsAboveHighPressure())
+ long ticksSinceTrim = DateTime.UtcNow.Subtract(lastTrimTime).Ticks;
+
+#if NETCOREAPP
+ // For GCThresholds mode, track when we trim to help IsAboveHighPressure make better decisions
+ if (_physicalMemoryMode == PhysicalMemoryMode.GCThresholds)
{
- // choose percent such that we don't repeat this for ~5 (TARGET_TOTAL_MEMORY_TRIM_INTERVAL) minutes,
- // but keep the percentage between 10 and 50.
- DateTime utcNow = DateTime.UtcNow;
- long ticksSinceTrim = utcNow.Subtract(lastTrimTime).Ticks;
- if (ticksSinceTrim > 0)
+ var memInfo = GC.GetGCMemoryInfo();
+
+ if (IsReallyAboveHighPressure(memInfo))
{
- percent = Math.Min(50, (int)((lastTrimPercent * TargetTotalMemoryTrimIntervalTicks) / ticksSinceTrim));
- percent = Math.Max(MinTotalMemoryTrimPercent, percent);
+ // Use the standard time-based calculation
+ percent = GetPercentToTrimInternal(ticksSinceTrim, lastTrimPercent);
+
+ // Record that we trimmed so IsAboveHighPressure can use heap size changes
+ _lastTrimMemInfo = memInfo;
}
+ }
-#if PERF
- Debug.WriteLine($"PhysicalMemoryMonitor.GetPercentToTrim: percent={percent:N}, lastTrimPercent={lastTrimPercent:N}, secondsSinceTrim={ticksSinceTrim/TimeSpan.TicksPerSecond:N}{Environment.NewLine}");
-#endif
+ // For other modes, use original time-based calculation
+ else
+#endif // NETCOREAPP
+ if (IsAboveHighPressure())
+ {
+ percent = GetPercentToTrimInternal(ticksSinceTrim, lastTrimPercent);
}
+#if PERF
+ Debug.WriteLine($"PhysicalMemoryMonitor.GetPercentToTrim: percent={percent:N}, lastTrimPercent={lastTrimPercent:N}, secondsSinceTrim={ticksSinceTrim / TimeSpan.TicksPerSecond:N}{Environment.NewLine}");
+#endif
return percent;
}
- internal void SetLimit(int physicalMemoryLimitPercentage)
+ private static int GetPercentToTrimInternal(long ticksSinceTrim, int lastTrimPercent)
{
- if (physicalMemoryLimitPercentage == 0)
+ int percent = 0;
+
+ // Original time-based calculation
+ if (ticksSinceTrim > 0)
{
- // use defaults
- return;
+ percent = Math.Min(50, (int)((lastTrimPercent * TargetTotalMemoryTrimIntervalTicks) / ticksSinceTrim));
+ percent = Math.Max(MinTotalMemoryTrimPercent, percent);
}
- _pressureHigh = Math.Max(3, physicalMemoryLimitPercentage);
- _pressureLow = Math.Max(1, _pressureHigh - 9);
- Dbg.Trace("MemoryCacheStats", $"PhysicalMemoryMonitor.SetLimit: _pressureHigh={_pressureHigh}, _pressureLow={_pressureLow}");
+
+ return percent;
}
}
}
diff --git a/src/libraries/System.Runtime.Caching/tests/System.Runtime.Caching/MemoryCacheTest.cs b/src/libraries/System.Runtime.Caching/tests/System.Runtime.Caching/MemoryCacheTest.cs
index 8d05eab92635eb..8ab644b39cb604 100644
--- a/src/libraries/System.Runtime.Caching/tests/System.Runtime.Caching/MemoryCacheTest.cs
+++ b/src/libraries/System.Runtime.Caching/tests/System.Runtime.Caching/MemoryCacheTest.cs
@@ -178,6 +178,37 @@ public void ConstructorParameters()
mc = new MemoryCache("MyCache", config);
});
+ config.Clear();
+ config.Add("PhysicalMemoryMode", "Standard");
+ // Just make sure it doesn't throw any exception
+ mc = new MemoryCache("MyCache", config);
+
+ config.Clear();
+ config.Add("PhysicalMemoryMode", "Legacy");
+ // Just make sure it doesn't throw any exception
+ mc = new MemoryCache("MyCache", config);
+
+ config.Clear();
+ config.Add("PhysicalMemoryMode", "GCThresholds");
+ // Just make sure it doesn't throw any exception
+ mc = new MemoryCache("MyCache", config);
+
+ config.Clear();
+ config.Add("PhysicalMemoryMode", "NotValidMode");
+ if (IsFullFramework)
+ {
+ // On .NET Framework, this does not throw, because the Framework version of SRC gets loaded,
+ // and it cares nothing for this setting.
+ mc = new MemoryCache("MyCache", config);
+ }
+ else
+ {
+ Assert.Throws(() =>
+ {
+ mc = new MemoryCache("MyCache", config);
+ });
+ }
+
// Just make sure it doesn't throw any exception
config.Clear();
config.Add("UnsupportedSetting", "123");
@@ -239,23 +270,55 @@ public void DefaultInstanceDefaults()
[ConditionalFact(nameof(SupportsPhysicalMemoryMonitor))]
public void ConstructorValues()
{
+ // Testing with auto-calculated physical memory limit percentage across different modes
var config = new NameValueCollection();
config.Add("CacheMemoryLimitMegabytes", "1");
config.Add("pollingInterval", "00:10:00");
-
var mc = new MemoryCache("MyCache", config);
Assert.Equal(1048576, mc.CacheMemoryLimit);
Assert.Equal(TimeSpan.FromMinutes(10), mc.PollingInterval);
+ Assert.True(mc.PhysicalMemoryLimit < 100);
+ Assert.True(mc.PhysicalMemoryLimit >= 95);
+
+ // Also, let's not try to duplicate the default-guessing logic here - but lets do grab the value MC
+ // calculated in this basec case, so we can double check that MC is using the same auto-calc value
+ // for our test environment later
+ var autoCalculatedPhysicalMemoryLimit = mc.PhysicalMemoryLimit;
+
+ config.Add("PhysicalMemoryMode", "Standard");
+ mc = new MemoryCache("MyCache", config);
+ // Full .Net Framework uses the in-box SRC. So none of this 'PhysicalMemoryMode' setting matters. Just make sure it doesn't get in the way.
+ Assert.Equal(autoCalculatedPhysicalMemoryLimit, mc.PhysicalMemoryLimit);
+
+ config.Set("PhysicalMemoryMode", "Legacy");
+ mc = new MemoryCache("MyCache", config);
+ Assert.Equal(autoCalculatedPhysicalMemoryLimit, mc.PhysicalMemoryLimit);
+
+ config.Set("PhysicalMemoryMode", "GCThresholds");
+ mc = new MemoryCache("MyCache", config);
+ Assert.Equal(IsFullFramework ? autoCalculatedPhysicalMemoryLimit : 100, mc.PhysicalMemoryLimit);
+ // Now, verify that auto-calculated physical memory limit percentage does not override manually set values in any mode
config.Clear();
config.Add("PhysicalMemoryLimitPercentage", "10");
config.Add("CacheMemoryLimitMegabytes", "5");
config.Add("PollingInterval", "01:10:00");
-
mc = new MemoryCache("MyCache", config);
Assert.Equal(10, mc.PhysicalMemoryLimit);
Assert.Equal(5242880, mc.CacheMemoryLimit);
Assert.Equal(TimeSpan.FromMinutes(70), mc.PollingInterval);
+
+ config.Add("PhysicalMemoryMode", "Standard");
+ mc = new MemoryCache("MyCache", config);
+ Assert.Equal(10, mc.PhysicalMemoryLimit);
+
+ config.Set("PhysicalMemoryMode", "Legacy");
+ mc = new MemoryCache("MyCache", config);
+ Assert.Equal(10, mc.PhysicalMemoryLimit);
+
+ config.Set("PhysicalMemoryMode", "GCThresholds");
+ mc = new MemoryCache("MyCache", config);
+ Assert.Equal(10, mc.PhysicalMemoryLimit);
}
[Theory, InlineData("true"), InlineData("false"), InlineData(null)]