Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ System.Runtime.Caching.ObjectCache</PackageDescription>
</ItemGroup>

<ItemGroup Condition="'$(TargetPlatformIdentifier)' != 'windows'">
<Compile Include="System\Runtime\Caching\MemoryMonitor.Unix.cs" />
<Compile Include="System\Runtime\Caching\PhysicalMemoryMonitor.Unix.cs" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<PhysicalMemoryMode>(rawValue, true, out PhysicalMemoryMode mode))
{
throw new ArgumentException(null, ConfigUtil.PhysicalMemoryMode);
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ArgumentException is thrown with a null message, making it unhelpful to users. It should follow the pattern of other ConfigUtil methods and use a formatted resource string like RH.Format(SR.Value_must_be_valid_enum, valueName, rawValue) to provide clear feedback about the invalid value.

Suggested change
throw new ArgumentException(null, ConfigUtil.PhysicalMemoryMode);
throw new ArgumentException(RH.Format(SR.Value_must_be_valid_enum, ConfigUtil.PhysicalMemoryMode, rawValue), ConfigUtil.PhysicalMemoryMode);

Copilot uses AI. Check for mistakes.
}

return mode;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// Defines the physical memory monitoring modes for the cache.
/// </summary>
internal enum PhysicalMemoryMode
{
/// <summary>
/// Legacy mode - uses platform-specific memory detection with GC-induced stats on non-Windows.
/// </summary>
Legacy = 0,

/// <summary>
/// Standard mode - uses GCMemoryInfo without inducing GC collections.
/// </summary>
Standard = 1,

/// <summary>
/// GC thresholds mode - uses GCMemoryInfo.HighMemoryLoadThresholdBytes instead of percentage of total memory.
/// </summary>
GCThresholds = 2
}

#if NETCOREAPP
[UnsupportedOSPlatform("browser")]
#endif
Expand All @@ -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),
Expand All @@ -46,6 +75,7 @@ internal sealed class MemoryCacheElement : ConfigurationElement
{
s_propName,
s_propPhysicalMemoryLimitPercentage,
s_propPhysicalMemoryMode,
s_propCacheMemoryLimitMegabytes,
s_propPollingInterval
};
Expand Down Expand Up @@ -82,6 +112,10 @@ public string Name
}
}

/// <summary>
/// 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).
/// </summary>
[ConfigurationProperty("physicalMemoryLimitPercentage", DefaultValue = (int)0)]
[IntegerValidator(MinValue = 0, MaxValue = 100)]
public int PhysicalMemoryLimitPercentage
Expand All @@ -96,6 +130,22 @@ public int PhysicalMemoryLimitPercentage
}
}

/// <summary>
/// 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
/// </summary>
[ConfigurationProperty("physicalMemoryMode", DefaultValue = "Legacy")]
internal PhysicalMemoryMode PhysicalMemoryMode
{
get
{
return (PhysicalMemoryMode)base["physicalMemoryMode"];
}
}

[ConfigurationProperty("cacheMemoryLimitMegabytes", DefaultValue = (int)0)]
[IntegerValidator(MinValue = 0)]
public int CacheMemoryLimitMegabytes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ namespace System.Runtime.Caching.Configuration
<system.runtime.caching>
<memoryCaches>
<namedCaches>
<add name="Default" physicalMemoryPercentage="0" pollingInterval="00:02:00"/>
<add name="Foo" physicalMemoryPercentage="0" pollingInterval="00:02:00"/>
<add name="Bar" physicalMemoryPercentage="0" pollingInterval="00:02:00"/>
<add name="Default" physicalMemoryPercentage="0" physicalMemoryMode="Legacy" pollingInterval="00:02:00"/>
<add name="Foo" physicalMemoryPercentage="0" physicalMemoryMode="Standard" pollingInterval="00:02:00"/>
<add name="Bar" physicalMemoryPercentage="0" physicalMemoryMode="GCThresholds" pollingInterval="00:02:00"/>
</namedCaches>
</memoryCaches>
</system.caching>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -145,13 +147,21 @@ private void InitializeConfiguration(NameValueCollection config)
_configPollingInterval = ConfigUtil.DefaultPollingTimeMilliseconds;
_configCacheMemoryLimitMegabytes = 0;
_configPhysicalMemoryLimitPercentage = 0;
_configPhysicalMemoryMode = PhysicalMemoryMode.Legacy;
}

if (config != null)
{
_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)
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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)
Expand All @@ -368,7 +386,8 @@ internal void UpdateConfig(NameValueCollection config)
}

if (cacheMemoryLimitMegabytes == _configCacheMemoryLimitMegabytes
&& physicalMemoryLimitPercentage == _configPhysicalMemoryLimitPercentage)
&& physicalMemoryLimitPercentage == _configPhysicalMemoryLimitPercentage
&& physicalMemoryMode == _configPhysicalMemoryMode)
{
return;
}
Expand All @@ -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;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +5 to +7
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused using directives. The following using directives are not needed and should be removed:

  • System.Collections.Specialized
  • System.Security
  • System.Runtime.InteropServices

Only System is actually used in this file.

Suggested change
using System.Collections.Specialized;
using System.Security;
using System.Runtime.InteropServices;

Copilot uses AI. Check for mistakes.


Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra blank line. There's an unnecessary blank line after the using directives. Per .editorconfig standards, there should be only one blank line between the using statements and the namespace declaration.

Suggested change

Copilot uses AI. Check for mistakes.
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.
Comment on lines +23 to +28
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent product name: ".Net Core" should be ".NET Core" (with capital letters for both parts). This appears twice in the comments on lines 23 and 28.

Copilot uses AI. Check for mistakes.
// So we don't need to worry about it here.
//s_totalVirtual = default(long);
}
#pragma warning restore CA1810
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading