using System.Diagnostics; using System.Globalization; using System.Runtime.InteropServices; namespace XFEToolBox.Server.Core.Utilities; /// /// Samples the CPU utilization of the entire operating system. Process CPU is /// used only as a fallback on platforms where system counters are unavailable. /// public static class SystemCpuUsageSampler { private static readonly TimeSpan DefaultInterval = TimeSpan.FromMilliseconds(300); public static async Task SampleAsync( TimeSpan? interval = null, CancellationToken cancellationToken = default) { var sampleInterval = interval ?? DefaultInterval; if (sampleInterval < TimeSpan.FromMilliseconds(50)) throw new ArgumentOutOfRangeException(nameof(interval), "CPU 采样间隔不能小于 50ms。"); using var process = Process.GetCurrentProcess(); var processTimeBefore = process.TotalProcessorTime; var sampleStarted = Stopwatch.GetTimestamp(); var hasSystemSnapshot = TryReadSystemSnapshot(out var systemBefore); await Task.Delay(sampleInterval, cancellationToken); if (hasSystemSnapshot && TryReadSystemSnapshot(out var systemAfter) && TryCalculateUsage(systemBefore, systemAfter, out var systemUsage)) return systemUsage; process.Refresh(); var elapsedSeconds = Stopwatch.GetElapsedTime(sampleStarted).TotalSeconds; if (elapsedSeconds <= 0) return 0; var processUsage = (process.TotalProcessorTime - processTimeBefore).TotalSeconds / elapsedSeconds / Math.Max(1, Environment.ProcessorCount) * 100; return Math.Clamp(processUsage, 0, 100); } private static bool TryReadSystemSnapshot(out CpuSnapshot snapshot) { if (OperatingSystem.IsWindows()) return TryReadWindowsSnapshot(out snapshot); if (OperatingSystem.IsLinux()) return TryReadLinuxSnapshot(out snapshot); snapshot = default; return false; } private static bool TryReadWindowsSnapshot(out CpuSnapshot snapshot) { if (!GetSystemTimes(out var idle, out var kernel, out var user)) { snapshot = default; return false; } // Kernel time includes idle time on Windows. snapshot = new CpuSnapshot( checked(kernel.ToUInt64() + user.ToUInt64()), idle.ToUInt64()); return true; } private static bool TryReadLinuxSnapshot(out CpuSnapshot snapshot) { try { var cpuLine = File.ReadLines("/proc/stat") .FirstOrDefault(line => line.StartsWith("cpu ", StringComparison.Ordinal)); if (cpuLine is null) { snapshot = default; return false; } var values = cpuLine.Split(' ', StringSplitOptions.RemoveEmptyEntries) .Skip(1) .Select(value => ulong.Parse(value, CultureInfo.InvariantCulture)) .ToArray(); if (values.Length < 4) { snapshot = default; return false; } var total = values.Aggregate(0UL, static (sum, value) => checked(sum + value)); var idle = checked(values[3] + (values.Length > 4 ? values[4] : 0)); snapshot = new CpuSnapshot(total, idle); return true; } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or FormatException or OverflowException) { snapshot = default; return false; } } private static bool TryCalculateUsage(CpuSnapshot before, CpuSnapshot after, out double usage) { if (after.TotalTicks <= before.TotalTicks || after.IdleTicks < before.IdleTicks) { usage = 0; return false; } var totalDelta = after.TotalTicks - before.TotalTicks; var idleDelta = Math.Min(totalDelta, after.IdleTicks - before.IdleTicks); usage = Math.Clamp((totalDelta - idleDelta) * 100d / totalDelta, 0, 100); return double.IsFinite(usage); } [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GetSystemTimes( out FileTime idleTime, out FileTime kernelTime, out FileTime userTime); [StructLayout(LayoutKind.Sequential)] private readonly struct FileTime { private readonly uint lowDateTime; private readonly uint highDateTime; public ulong ToUInt64() => ((ulong)highDateTime << 32) | lowDateTime; } private readonly record struct CpuSnapshot(ulong TotalTicks, ulong IdleTicks); }