Skip to content

Commit 207efa7

Browse files
committed
Add a TotalProcessorCount API that returns the expected value
1 parent 233a16e commit 207efa7

2 files changed

Lines changed: 309 additions & 0 deletions

File tree

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// <copyright file="TotalProcessorCount.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+
6+
#if NET6_0_OR_GREATER
7+
8+
#nullable enable
9+
10+
using System;
11+
using System.IO;
12+
using System.Runtime.InteropServices;
13+
using Datadog.Trace.Logging;
14+
using Datadog.Trace.SourceGenerators;
15+
16+
namespace Datadog.Trace.RuntimeMetrics;
17+
18+
internal static class TotalProcessorCount
19+
{
20+
private static readonly Lazy<int?> LazyValue = new(GetTotalProcessorCount);
21+
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(TotalProcessorCount));
22+
23+
/// <summary>
24+
/// Gets the total number of logical processors on the host machine. Differs from
25+
/// <see cref="Environment.ProcessorCount"/> (which includes cgroup/container CPU limits
26+
/// and process affinity). Mirrors the GC's own GCToOSInterface::GetTotalProcessorCount()
27+
/// </summary>
28+
public static int? Value => LazyValue.Value;
29+
30+
[TestingAndPrivateOnly]
31+
internal static int? GetTotalProcessorCount()
32+
{
33+
if (OperatingSystem.IsWindows())
34+
{
35+
return WindowsProcessorCount.GetTotalProcessorCount();
36+
}
37+
38+
if (OperatingSystem.IsLinux())
39+
{
40+
return LinuxProcessorCount.GetTotalProcessorCount();
41+
}
42+
43+
if (OperatingSystem.IsMacOS())
44+
{
45+
return MacOsProcessorCount.GetTotalProcessorCount();
46+
}
47+
48+
return null;
49+
}
50+
51+
internal static class WindowsProcessorCount
52+
{
53+
private const ushort AllProcessorGroups = 0xFFFF;
54+
55+
[DllImport("kernel32.dll", SetLastError = true)]
56+
private static extern int GetActiveProcessorCount(ushort groupNumber);
57+
58+
/// <summary>
59+
/// Gets the total number of logical processors on a Windows host via <c>GetActiveProcessorCount</c>,
60+
/// counting active processors across all processor groups and ignoring process affinity and Job Object
61+
/// CPU limits (the container-CPU-cap mechanism on Windows).
62+
/// </summary>
63+
internal static int? GetTotalProcessorCount()
64+
{
65+
var result = GetActiveProcessorCount(AllProcessorGroups);
66+
if (result > 0)
67+
{
68+
return result;
69+
}
70+
71+
var error = Marshal.GetLastWin32Error();
72+
Log.Warning(
73+
"GetActiveProcessorCount failed when getting total machine processor count. ErrorCode={ErrorCode}",
74+
property: error);
75+
return null;
76+
}
77+
}
78+
79+
internal static class LinuxProcessorCount
80+
{
81+
private const string OnlineCpusPath = "/sys/devices/system/cpu/online";
82+
83+
/// <summary>
84+
/// Gets the total number of logical processors on a Linux host by reading the online-CPU range
85+
/// reported by the kernel, the same source <c>sysconf(_SC_NPROCESSORS_ONLN)</c> itself reads, and unaffected
86+
/// by cgroup CPU quotas. Avoids P/Invoking into libc, which fails to resolve on musl-based images (e.g. Alpine).
87+
/// </summary>
88+
internal static int? GetTotalProcessorCount()
89+
{
90+
try
91+
{
92+
var contents = File.ReadAllText(OnlineCpusPath);
93+
return TryParseOnlineCpuRanges(contents.AsSpan());
94+
}
95+
catch (Exception ex)
96+
{
97+
Log.Warning(ex, "Error reading {Path} to determine total machine processor count", OnlineCpusPath);
98+
return null;
99+
}
100+
}
101+
102+
// Parses the Linux cpu-list-format (see Documentation/admin-guide/kernel-parameters.txt)
103+
// comma-separated list of either a single CPU index ("0") or an inclusive range ("0-7"), e.g. "0-3,8-11".
104+
[TestingAndPrivateOnly]
105+
internal static int? TryParseOnlineCpuRanges(ReadOnlySpan<char> contents)
106+
{
107+
var trimmed = contents.Trim();
108+
if (trimmed.IsEmpty)
109+
{
110+
return null;
111+
}
112+
113+
var count = 0;
114+
var remaining = trimmed;
115+
while (!remaining.IsEmpty)
116+
{
117+
var commaIndex = remaining.IndexOf(',');
118+
var token = commaIndex < 0 ? remaining : remaining[..commaIndex];
119+
120+
if (!TryParseToken(token, out var tokenCount))
121+
{
122+
return null;
123+
}
124+
125+
count += tokenCount;
126+
127+
if (commaIndex < 0)
128+
{
129+
break;
130+
}
131+
132+
remaining = remaining[(commaIndex + 1)..];
133+
if (remaining.IsEmpty)
134+
{
135+
// trailing comma with no following token
136+
return null;
137+
}
138+
}
139+
140+
return count > 0 ? count : null;
141+
142+
static bool TryParseToken(ReadOnlySpan<char> token, out int tokenCount)
143+
{
144+
tokenCount = 0;
145+
146+
var dashIndex = token.IndexOf('-');
147+
if (dashIndex < 0)
148+
{
149+
if (!int.TryParse(token, out var single) || single < 0)
150+
{
151+
return false;
152+
}
153+
154+
tokenCount = 1;
155+
return true;
156+
}
157+
158+
var startSpan = token[..dashIndex];
159+
var endSpan = token[(dashIndex + 1)..];
160+
161+
if (!int.TryParse(startSpan, out var start) || start < 0 ||
162+
!int.TryParse(endSpan, out var end) || end < start)
163+
{
164+
return false;
165+
}
166+
167+
tokenCount = end - start + 1;
168+
return true;
169+
}
170+
}
171+
}
172+
173+
internal static class MacOsProcessorCount
174+
{
175+
private const string LogicalCpuName = "hw.logicalcpu";
176+
177+
[DllImport("libSystem.dylib", EntryPoint = "sysctlbyname", CharSet = CharSet.Ansi, SetLastError = true)]
178+
private static extern int SysCtlByName(string name, out int oldp, ref IntPtr oldlenp, IntPtr newp, IntPtr newlen);
179+
180+
/// <summary>
181+
/// Gets the total number of logical processors on a macOS host via <c>sysctlbyname("hw.logicalcpu", ...)</c>,
182+
/// the standard, stable way to query total logical CPUs on macOS.
183+
/// </summary>
184+
internal static int? GetTotalProcessorCount()
185+
{
186+
var size = new IntPtr(sizeof(int));
187+
var result = SysCtlByName(LogicalCpuName, out var value, ref size, IntPtr.Zero, IntPtr.Zero);
188+
if (result == 0 && value > 0)
189+
{
190+
return value;
191+
}
192+
193+
var error = Marshal.GetLastWin32Error();
194+
Log.Warning(
195+
"sysctlbyname failed when getting total machine processor count. ErrorCode={ErrorCode}",
196+
property: error);
197+
return null;
198+
}
199+
}
200+
}
201+
#endif
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// <copyright file="TotalProcessorCountTests.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+
6+
#if NET6_0_OR_GREATER
7+
8+
#nullable enable
9+
10+
using System;
11+
using System.Runtime.InteropServices;
12+
using Datadog.Trace.RuntimeMetrics;
13+
using Datadog.Trace.TestHelpers;
14+
using FluentAssertions;
15+
using Xunit;
16+
17+
namespace Datadog.Trace.Tests.RuntimeMetrics;
18+
19+
public class TotalProcessorCountTests
20+
{
21+
[Fact]
22+
public void Value_IsPositive()
23+
{
24+
TotalProcessorCount.Value.Should().BePositive();
25+
}
26+
27+
[Fact]
28+
public void Value_IsStableAcrossCalls()
29+
{
30+
var first = TotalProcessorCount.Value;
31+
var second = TotalProcessorCount.Value;
32+
33+
second.Should().Be(first);
34+
}
35+
36+
[Fact]
37+
public void Resolve_NeverThrows_AndReturnsPositive()
38+
{
39+
var result = TotalProcessorCount.GetTotalProcessorCount();
40+
41+
result.Should().BePositive();
42+
}
43+
44+
[SkippableFact]
45+
public void GetTotalProcessorCount_ReturnsExpectedValueInCI()
46+
{
47+
// NOTE: this test will fail locally unless you happen to happen to have 4 logical processors!
48+
var result = TotalProcessorCount.GetTotalProcessorCount();
49+
50+
// All CI machines currently have 4 CPUs, but that won't always be the case, so update this test as appropriate!
51+
const int expectedCpus = 4;
52+
result.Should().Be(expectedCpus);
53+
}
54+
55+
[SkippableFact]
56+
public void GetTotalProcessorCount_OnWindows_SucceedsAndReturnsPositive()
57+
{
58+
SkipOn.AllExcept(SkipOn.PlatformValue.Windows);
59+
60+
var result = TotalProcessorCount.WindowsProcessorCount.GetTotalProcessorCount();
61+
62+
result.Should().BePositive();
63+
}
64+
65+
[SkippableFact]
66+
public void Linux_GetTotalProcessorCount_SucceedsAndReturnsPositive()
67+
{
68+
SkipOn.AllExcept(SkipOn.PlatformValue.Linux);
69+
70+
var result = TotalProcessorCount.LinuxProcessorCount.GetTotalProcessorCount();
71+
72+
result.Should().BePositive();
73+
}
74+
75+
[SkippableFact]
76+
public void MacOs_GetTotalProcessorCount_SucceedsAndReturnsPositive()
77+
{
78+
SkipOn.AllExcept(SkipOn.PlatformValue.MacOs);
79+
80+
var result = TotalProcessorCount.MacOsProcessorCount.GetTotalProcessorCount();
81+
82+
result.Should().BePositive();
83+
}
84+
85+
[Theory]
86+
[InlineData("", null)]
87+
[InlineData(" ", null)]
88+
[InlineData("\n", null)]
89+
[InlineData("0", 1)]
90+
[InlineData("0-7", 8)]
91+
[InlineData("0-3,8-11", 8)]
92+
[InlineData("0,2,4", 3)]
93+
[InlineData("0-3,5,7-8", 7)]
94+
[InlineData("0-7\n", 8)]
95+
[InlineData(" 0-7 ", 8)]
96+
[InlineData("0-7,", null)]
97+
[InlineData("abc", null)]
98+
[InlineData("7-3", null)]
99+
[InlineData("-1-3", null)]
100+
[InlineData("0--3", null)]
101+
public void Linux_TryParseOnlineCpuRanges_ResolvesExpectedValue(string contents, int? expectedCount)
102+
{
103+
var result = TotalProcessorCount.LinuxProcessorCount.TryParseOnlineCpuRanges(contents.AsSpan());
104+
105+
result.Should().Be(expectedCount);
106+
}
107+
}
108+
#endif

0 commit comments

Comments
 (0)