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