Skip to content

Commit 7544a5e

Browse files
committed
Add GcMemoryLoadCalculator for calculating the actual memory load as a 0-100 percentage, handling differences with GC hard limits
1 parent 207efa7 commit 7544a5e

3 files changed

Lines changed: 634 additions & 0 deletions

File tree

tracer/src/Datadog.Trace/Configuration/PlatformKeys.DotNet.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,18 @@ internal static partial class PlatformKeys
3333
/// Program data folder
3434
/// </summary>
3535
public const string ProgramData = "ProgramData";
36+
37+
/// <summary>
38+
/// Sets the GC's "high memory load" threshold percent (clamped to 99 by the runtime). Parsed as
39+
/// <b>hexadecimal</b> by the runtime (see <c>GCToEEInterface::GetIntConfigValue</c>), and takes precedence
40+
/// over the <c>System.GC.HighMemoryPercent</c> runtimeconfig knob, which is parsed using C-style base
41+
/// detection (<c>0x</c>/<c>0X</c> prefix for hexadecimal, a leading <c>0</c> for octal, otherwise decimal -
42+
/// see <c>Configuration::GetKnobULONGLONGValue</c>).
43+
/// </summary>
44+
public const string DotNetGCHighMemPercent = "DOTNET_GCHighMemPercent";
45+
46+
/// <summary>
47+
/// Legacy alias for <see cref="DotNetGCHighMemPercent"/>, also parsed as hexadecimal.
48+
/// </summary>
49+
public const string ComPlusGCHighMemPercent = "COMPlus_GCHighMemPercent";
3650
}
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
// <copyright file="GcMemoryLoadCalculator.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
#if NET6_0_OR_GREATER
6+
7+
#nullable enable
8+
9+
using System;
10+
using System.Threading;
11+
using Datadog.Trace.Configuration;
12+
using Datadog.Trace.Logging;
13+
using Datadog.Trace.SourceGenerators;
14+
using Datadog.Trace.Util;
15+
16+
namespace Datadog.Trace.RuntimeMetrics;
17+
18+
/// <summary>
19+
/// Recovers the true GC memory-load percentage (0-100) from <see cref="GCMemoryInfo"/>.
20+
/// <see cref="GCMemoryInfo.MemoryLoadBytes"/> and <see cref="GCMemoryInfo.HighMemoryLoadThresholdBytes"/> are both
21+
/// scaled by the GC's <c>total_physical_mem</c>, but <see cref="GCMemoryInfo.TotalAvailableMemoryBytes"/> switches
22+
/// to <c>heap_hard_limit</c> whenever a GC hard limit is in play (e.g. a memory-limited container without explicit
23+
/// GC configuration, where the runtime defaults the limit to 75% of physical memory).
24+
/// See <c>GCHeap::GetMemoryInfo</c> in src/coreclr/gc/gc.cpp.
25+
/// </summary>
26+
internal static class GcMemoryLoadCalculator
27+
{
28+
// gc_heap::compute_memory_settings() only applies its ">= 80GB" branch above this threshold.
29+
// The value here is pre-scaled by the default high-memory-load percentage (90%) so the comparison
30+
// below is a plain integer comparison, not a division.
31+
private const long EightyGiBBytesAt90Percent = 80L * 1024 * 1024 * 1024 * 9 / 10;
32+
33+
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(GcMemoryLoadCalculator));
34+
35+
// high_memory_load_th is fixed for the lifetime of the GC configuration it was resolved from, so the
36+
// configured override (if any) only needs to be read once.
37+
private static readonly Lazy<int?> ConfiguredHighMemoryLoadPercent = new(ReadConfiguredHighMemoryLoadPercent);
38+
39+
private static bool _unableToResolveLogged;
40+
41+
/// <summary>
42+
/// Gets the GC memory load as a 0-100 percentage, or <c>null</c> if it cannot be reliably determined.
43+
/// </summary>
44+
public static double? TryGetMemoryLoadPercentage(in GCMemoryInfo info)
45+
{
46+
// ProcessorCount is incorrect here, we need the number of processors on the machine, not the number visible to the process
47+
return TryCalculate(
48+
info.MemoryLoadBytes,
49+
info.HighMemoryLoadThresholdBytes,
50+
info.TotalAvailableMemoryBytes,
51+
ConfiguredHighMemoryLoadPercent.Value,
52+
Environment.ProcessorCount);
53+
}
54+
55+
[TestingAndPrivateOnly]
56+
internal static double? TryCalculate(long memoryLoadBytes, long highMemoryLoadThresholdBytes, long totalAvailableMemoryBytes, int? configuredHighPercent, int processorCount)
57+
{
58+
if (highMemoryLoadThresholdBytes <= 0 || totalAvailableMemoryBytes <= 0)
59+
{
60+
// HighMemoryLoadThresholdBytes is 0 before the first GC has run, so we can't calculate anything
61+
return null;
62+
}
63+
64+
var highPercent = ResolveHighMemoryLoadThresholdPercent(highMemoryLoadThresholdBytes, configuredHighPercent, processorCount);
65+
66+
// heap_hard_limit (TotalAvailableMemoryBytes) can never exceed total_physical_mem. If the total we'd
67+
// imply from our resolved threshold is smaller than TotalAvailableMemoryBytes, the threshold is wrong -
68+
// bail out rather than publish a skewed value.
69+
var impliedTotalPhysicalMem = highMemoryLoadThresholdBytes * 100.0 / highPercent;
70+
if (impliedTotalPhysicalMem < totalAvailableMemoryBytes * 0.99)
71+
{
72+
if (!Volatile.Read(ref _unableToResolveLogged))
73+
{
74+
Volatile.Write(ref _unableToResolveLogged, true);
75+
Log.Debug<long, long, long, int?, int>(
76+
"Unable to resolve GC memory load percentage (MemoryLoadBytes={MemoryLoadBytes}, HighMemoryLoadThresholdBytes={HighMemoryLoadThresholdBytes}, TotalAvailableMemoryBytes={TotalAvailableMemoryBytes}, ConfiguredHighPercent={ConfiguredHighPercent}, ProcessorCount={ProcessorCount})",
77+
memoryLoadBytes,
78+
highMemoryLoadThresholdBytes,
79+
totalAvailableMemoryBytes,
80+
configuredHighPercent,
81+
processorCount);
82+
}
83+
84+
return null;
85+
}
86+
87+
var memoryLoad = Math.Round(memoryLoadBytes * (double)highPercent / highMemoryLoadThresholdBytes);
88+
return Math.Min(100d, Math.Max(0d, memoryLoad));
89+
}
90+
91+
[TestingAndPrivateOnly]
92+
internal static int ResolveHighMemoryLoadThresholdPercent(long highMemoryLoadThresholdBytes, int? configuredHighPercent, int processorCount)
93+
{
94+
// We need to recreate this flow from the GC: https://github.com/dotnet/runtime/blob/2cc068d0008c898c67578f2868bd5b17a64c6366/src/coreclr/gc/init.cpp#L1488C59-L1519
95+
96+
// An explicit override (from env/AppContext) always wins
97+
if (configuredHighPercent is { } configured)
98+
{
99+
return configured;
100+
}
101+
102+
// Otherwise, the threshold should be the runtime's default formula.
103+
// Since the resolved percentage is always >= 90, the _implied_ total
104+
// here is always >= total_physical_mem, so "implied < 80GiB" implies "total_physical_mem < 80GiB".
105+
if (highMemoryLoadThresholdBytes < EightyGiBBytesAt90Percent)
106+
{
107+
return 90;
108+
}
109+
110+
var availableMemThreshold = Math.Min(10, 3 + (int)(47f / Math.Max(1, processorCount)));
111+
return 100 - availableMemThreshold;
112+
}
113+
114+
[TestingAndPrivateOnly]
115+
internal static int? ParseEnvHighMemPercent(ReadOnlySpan<char> envValue)
116+
{
117+
// CLRConfig reads every environment knob as hex (GCToEEInterface::GetIntConfigValue), and treats
118+
// ERANGE (overflow) as "not specified at all" rather than clamping
119+
if (!TryParseCStyleUnsignedInteger(envValue, numberBase: 16, out var parsed, out var overflowed) || overflowed)
120+
{
121+
return null;
122+
}
123+
124+
return ToGcHighMemPercent(parsed);
125+
}
126+
127+
[TestingAndPrivateOnly]
128+
internal static int? ParseAppContextHighMemPercent(object? appContextValue)
129+
{
130+
// runtimeconfig properties always reach AppContext as strings - anything else was set by user code
131+
// after startup, so the GC never saw it
132+
if (appContextValue is not string stringValue)
133+
{
134+
return null;
135+
}
136+
137+
// Configuration::GetKnobULONGLONGValue uses base 0 (decimal, or hex/octal by prefix) and ignores
138+
// ERANGE, so an out-of-range value saturates to UINT64_MAX and gets clamped to 99 below
139+
TryParseCStyleUnsignedInteger(stringValue.AsSpan(), numberBase: 0, out var parsed, out _);
140+
return ToGcHighMemPercent(parsed);
141+
}
142+
143+
// compute_memory_settings() (see the pinned source reference on EightyGiBBytesAt90Percent above) reads the
144+
// config value into a 32-bit integer, so the high bits are silently dropped, then treats 0 as "not
145+
// configured" and clamps the result to 99.
146+
private static int? ToGcHighMemPercent(ulong configValue)
147+
{
148+
var truncated = (uint)configValue;
149+
return truncated == 0 ? null : (int)Math.Min(99u, truncated);
150+
}
151+
152+
// Emulates the C runtime's strtoull(value, &end, numberBase), which both CLRConfig (base 16) and
153+
// Configuration::GetKnobULONGLONGValue (base 0 - decimal, or hex/octal by prefix) build on. Parsing
154+
// stops at the first invalid character rather than requiring the whole span to match, ERANGE
155+
// (overflow) is reported separately rather than failing the parse, and a leading '-' negates the
156+
// result within the unsigned range rather than being rejected.
157+
private static bool TryParseCStyleUnsignedInteger(ReadOnlySpan<char> value, int numberBase, out ulong result, out bool overflowed)
158+
{
159+
result = 0;
160+
overflowed = false;
161+
162+
var i = 0;
163+
while (i < value.Length && char.IsWhiteSpace(value[i]))
164+
{
165+
i++;
166+
}
167+
168+
var negative = false;
169+
if (i < value.Length && (value[i] == '+' || value[i] == '-'))
170+
{
171+
negative = value[i] == '-';
172+
i++;
173+
}
174+
175+
if (numberBase is 16 or 0 &&
176+
i + 1 < value.Length && value[i] == '0' && (value[i + 1] is 'x' or 'X') &&
177+
i + 2 < value.Length && HexDigitValue(value[i + 2]) >= 0)
178+
{
179+
numberBase = 16;
180+
i += 2;
181+
}
182+
else if (numberBase == 0)
183+
{
184+
numberBase = i < value.Length && value[i] == '0' ? 8 : 10;
185+
}
186+
187+
var digitsConsumed = 0;
188+
for (; i < value.Length; i++)
189+
{
190+
var digit = HexDigitValue(value[i]);
191+
if (digit < 0 || digit >= numberBase)
192+
{
193+
break;
194+
}
195+
196+
digitsConsumed++;
197+
198+
if (overflowed)
199+
{
200+
continue;
201+
}
202+
203+
if (result > (ulong.MaxValue - (ulong)digit) / (ulong)numberBase)
204+
{
205+
overflowed = true;
206+
result = ulong.MaxValue;
207+
continue;
208+
}
209+
210+
result = (result * (ulong)numberBase) + (ulong)digit;
211+
}
212+
213+
if (digitsConsumed == 0)
214+
{
215+
result = 0;
216+
return false;
217+
}
218+
219+
if (negative)
220+
{
221+
result = unchecked(0UL - result);
222+
}
223+
224+
return true;
225+
226+
// Returns the digit's value for 0-9/a-f/A-F, or -1 if the char isn't a hex digit.
227+
static int HexDigitValue(char c) => c switch
228+
{
229+
>= '0' and <= '9' => c - '0',
230+
>= 'a' and <= 'f' => c - 'a' + 10,
231+
>= 'A' and <= 'F' => c - 'A' + 10,
232+
_ => -1,
233+
};
234+
}
235+
236+
private static int? ReadConfiguredHighMemoryLoadPercent()
237+
{
238+
// Read the configs defined here: https://github.com/dotnet/runtime/blob/2cc068d0008c898c67578f2868bd5b17a64c6366/src/coreclr/gc/gcconfig.h#L100
239+
try
240+
{
241+
var envValue = EnvironmentHelpers.GetEnvironmentVariable(PlatformKeys.DotNetGCHighMemPercent)
242+
?? EnvironmentHelpers.GetEnvironmentVariable(PlatformKeys.ComPlusGCHighMemPercent);
243+
244+
// The runtime checks the environment variable first (gcenv.ee.cpp: GetGCHighMemPercent()). If it's
245+
// present at all - even "0", which means "unset" - it wins outright and the runtimeconfig knob below is
246+
// never consulted, so an explicit-but-unset env var can't fall back to a configured runtimeconfig value.
247+
if (!string.IsNullOrEmpty(envValue))
248+
{
249+
return ParseEnvHighMemPercent(envValue.AsSpan());
250+
}
251+
}
252+
catch (Exception ex)
253+
{
254+
Log.Error(ex, "Error reading configured GC high memory load percent");
255+
}
256+
257+
try
258+
{
259+
// The runtimeconfig knob (System.GC.HighMemoryPercent) is only consulted if the environment variable is unset.
260+
return ParseAppContextHighMemPercent(AppContext.GetData("System.GC.HighMemoryPercent"));
261+
}
262+
catch (Exception ex)
263+
{
264+
Log.Debug(ex, "Error reading System.GC.HighMemoryPercent from AppContext");
265+
}
266+
267+
return null;
268+
}
269+
}
270+
#endif

0 commit comments

Comments
 (0)