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)]