diff --git a/tracer/src/Datadog.Trace/RuntimeMetrics/TotalProcessorCount.cs b/tracer/src/Datadog.Trace/RuntimeMetrics/TotalProcessorCount.cs
new file mode 100644
index 000000000000..4a09d696ddf1
--- /dev/null
+++ b/tracer/src/Datadog.Trace/RuntimeMetrics/TotalProcessorCount.cs
@@ -0,0 +1,201 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#if NET6_0_OR_GREATER
+
+#nullable enable
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using Datadog.Trace.Logging;
+using Datadog.Trace.SourceGenerators;
+
+namespace Datadog.Trace.RuntimeMetrics;
+
+internal static class TotalProcessorCount
+{
+ private static readonly Lazy LazyValue = new(GetTotalProcessorCount);
+ private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(TotalProcessorCount));
+
+ ///
+ /// Gets the total number of logical processors on the host machine. Differs from
+ /// (which includes cgroup/container CPU limits
+ /// and process affinity). Mirrors the GC's own GCToOSInterface::GetTotalProcessorCount()
+ ///
+ public static int? Value => LazyValue.Value;
+
+ [TestingAndPrivateOnly]
+ internal static int? GetTotalProcessorCount()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ return WindowsProcessorCount.GetTotalProcessorCount();
+ }
+
+ if (OperatingSystem.IsLinux())
+ {
+ return LinuxProcessorCount.GetTotalProcessorCount();
+ }
+
+ if (OperatingSystem.IsMacOS())
+ {
+ return MacOsProcessorCount.GetTotalProcessorCount();
+ }
+
+ return null;
+ }
+
+ internal static class WindowsProcessorCount
+ {
+ private const ushort AllProcessorGroups = 0xFFFF;
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern int GetActiveProcessorCount(ushort groupNumber);
+
+ ///
+ /// Gets the total number of logical processors on a Windows host via GetActiveProcessorCount,
+ /// counting active processors across all processor groups and ignoring process affinity and Job Object
+ /// CPU limits (the container-CPU-cap mechanism on Windows).
+ ///
+ internal static int? GetTotalProcessorCount()
+ {
+ var result = GetActiveProcessorCount(AllProcessorGroups);
+ if (result > 0)
+ {
+ return result;
+ }
+
+ var error = Marshal.GetLastWin32Error();
+ Log.Warning(
+ "GetActiveProcessorCount failed when getting total machine processor count. ErrorCode={ErrorCode}",
+ property: error);
+ return null;
+ }
+ }
+
+ internal static class LinuxProcessorCount
+ {
+ private const string OnlineCpusPath = "/sys/devices/system/cpu/online";
+
+ ///
+ /// Gets the total number of logical processors on a Linux host by reading the online-CPU range
+ /// reported by the kernel, the same source sysconf(_SC_NPROCESSORS_ONLN) itself reads, and unaffected
+ /// by cgroup CPU quotas. Avoids P/Invoking into libc, which fails to resolve on musl-based images (e.g. Alpine).
+ ///
+ internal static int? GetTotalProcessorCount()
+ {
+ try
+ {
+ var contents = File.ReadAllText(OnlineCpusPath);
+ return TryParseOnlineCpuRanges(contents.AsSpan());
+ }
+ catch (Exception ex)
+ {
+ Log.Warning(ex, "Error reading {Path} to determine total machine processor count", OnlineCpusPath);
+ return null;
+ }
+ }
+
+ // Parses the Linux cpu-list-format (see Documentation/admin-guide/kernel-parameters.txt)
+ // comma-separated list of either a single CPU index ("0") or an inclusive range ("0-7"), e.g. "0-3,8-11".
+ [TestingAndPrivateOnly]
+ internal static int? TryParseOnlineCpuRanges(ReadOnlySpan contents)
+ {
+ var trimmed = contents.Trim();
+ if (trimmed.IsEmpty)
+ {
+ return null;
+ }
+
+ var count = 0;
+ var remaining = trimmed;
+ while (!remaining.IsEmpty)
+ {
+ var commaIndex = remaining.IndexOf(',');
+ var token = commaIndex < 0 ? remaining : remaining[..commaIndex];
+
+ if (!TryParseToken(token, out var tokenCount))
+ {
+ return null;
+ }
+
+ count += tokenCount;
+
+ if (commaIndex < 0)
+ {
+ break;
+ }
+
+ remaining = remaining[(commaIndex + 1)..];
+ if (remaining.IsEmpty)
+ {
+ // trailing comma with no following token
+ return null;
+ }
+ }
+
+ return count > 0 ? count : null;
+
+ static bool TryParseToken(ReadOnlySpan token, out int tokenCount)
+ {
+ tokenCount = 0;
+
+ var dashIndex = token.IndexOf('-');
+ if (dashIndex < 0)
+ {
+ if (!int.TryParse(token, out var single) || single < 0)
+ {
+ return false;
+ }
+
+ tokenCount = 1;
+ return true;
+ }
+
+ var startSpan = token[..dashIndex];
+ var endSpan = token[(dashIndex + 1)..];
+
+ if (!int.TryParse(startSpan, out var start) || start < 0 ||
+ !int.TryParse(endSpan, out var end) || end < start)
+ {
+ return false;
+ }
+
+ tokenCount = end - start + 1;
+ return true;
+ }
+ }
+ }
+
+ internal static class MacOsProcessorCount
+ {
+ private const string LogicalCpuName = "hw.logicalcpu";
+
+ [DllImport("libSystem.dylib", EntryPoint = "sysctlbyname", CharSet = CharSet.Ansi, SetLastError = true)]
+ private static extern int SysCtlByName(string name, out int oldp, ref IntPtr oldlenp, IntPtr newp, IntPtr newlen);
+
+ ///
+ /// Gets the total number of logical processors on a macOS host via sysctlbyname("hw.logicalcpu", ...),
+ /// the standard, stable way to query total logical CPUs on macOS.
+ ///
+ internal static int? GetTotalProcessorCount()
+ {
+ var size = new IntPtr(sizeof(int));
+ var result = SysCtlByName(LogicalCpuName, out var value, ref size, IntPtr.Zero, IntPtr.Zero);
+ if (result == 0 && value > 0)
+ {
+ return value;
+ }
+
+ var error = Marshal.GetLastWin32Error();
+ Log.Warning(
+ "sysctlbyname failed when getting total machine processor count. ErrorCode={ErrorCode}",
+ property: error);
+ return null;
+ }
+ }
+}
+#endif
diff --git a/tracer/test/Datadog.Trace.Tests/RuntimeMetrics/TotalProcessorCountTests.cs b/tracer/test/Datadog.Trace.Tests/RuntimeMetrics/TotalProcessorCountTests.cs
new file mode 100644
index 000000000000..6be728c3650e
--- /dev/null
+++ b/tracer/test/Datadog.Trace.Tests/RuntimeMetrics/TotalProcessorCountTests.cs
@@ -0,0 +1,108 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#if NET6_0_OR_GREATER
+
+#nullable enable
+
+using System;
+using System.Runtime.InteropServices;
+using Datadog.Trace.RuntimeMetrics;
+using Datadog.Trace.TestHelpers;
+using FluentAssertions;
+using Xunit;
+
+namespace Datadog.Trace.Tests.RuntimeMetrics;
+
+public class TotalProcessorCountTests
+{
+ [Fact]
+ public void Value_IsPositive()
+ {
+ TotalProcessorCount.Value.Should().BePositive();
+ }
+
+ [Fact]
+ public void Value_IsStableAcrossCalls()
+ {
+ var first = TotalProcessorCount.Value;
+ var second = TotalProcessorCount.Value;
+
+ second.Should().Be(first);
+ }
+
+ [Fact]
+ public void Resolve_NeverThrows_AndReturnsPositive()
+ {
+ var result = TotalProcessorCount.GetTotalProcessorCount();
+
+ result.Should().BePositive();
+ }
+
+ [SkippableFact]
+ public void GetTotalProcessorCount_ReturnsExpectedValueInCI()
+ {
+ // NOTE: this test will fail locally unless you happen to happen to have 4 logical processors!
+ var result = TotalProcessorCount.GetTotalProcessorCount();
+
+ // All CI machines currently have 4 CPUs, but that won't always be the case, so update this test as appropriate!
+ const int expectedCpus = 4;
+ result.Should().Be(expectedCpus);
+ }
+
+ [SkippableFact]
+ public void GetTotalProcessorCount_OnWindows_SucceedsAndReturnsPositive()
+ {
+ SkipOn.AllExcept(SkipOn.PlatformValue.Windows);
+
+ var result = TotalProcessorCount.WindowsProcessorCount.GetTotalProcessorCount();
+
+ result.Should().BePositive();
+ }
+
+ [SkippableFact]
+ public void Linux_GetTotalProcessorCount_SucceedsAndReturnsPositive()
+ {
+ SkipOn.AllExcept(SkipOn.PlatformValue.Linux);
+
+ var result = TotalProcessorCount.LinuxProcessorCount.GetTotalProcessorCount();
+
+ result.Should().BePositive();
+ }
+
+ [SkippableFact]
+ public void MacOs_GetTotalProcessorCount_SucceedsAndReturnsPositive()
+ {
+ SkipOn.AllExcept(SkipOn.PlatformValue.MacOs);
+
+ var result = TotalProcessorCount.MacOsProcessorCount.GetTotalProcessorCount();
+
+ result.Should().BePositive();
+ }
+
+ [Theory]
+ [InlineData("", null)]
+ [InlineData(" ", null)]
+ [InlineData("\n", null)]
+ [InlineData("0", 1)]
+ [InlineData("0-7", 8)]
+ [InlineData("0-3,8-11", 8)]
+ [InlineData("0,2,4", 3)]
+ [InlineData("0-3,5,7-8", 7)]
+ [InlineData("0-7\n", 8)]
+ [InlineData(" 0-7 ", 8)]
+ [InlineData("0-7,", null)]
+ [InlineData("abc", null)]
+ [InlineData("7-3", null)]
+ [InlineData("-1-3", null)]
+ [InlineData("0--3", null)]
+ public void Linux_TryParseOnlineCpuRanges_ResolvesExpectedValue(string contents, int? expectedCount)
+ {
+ var result = TotalProcessorCount.LinuxProcessorCount.TryParseOnlineCpuRanges(contents.AsSpan());
+
+ result.Should().Be(expectedCount);
+ }
+}
+#endif