diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/RuntimeMetrics.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/RuntimeMetrics.cs index 1ac12630a201f1..2857c9538ed04f 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/RuntimeMetrics.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/RuntimeMetrics.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Runtime.Versioning; using System.Threading; namespace System.Diagnostics.Metrics @@ -146,12 +147,14 @@ static RuntimeMetrics() unit: "{cpu}", description: "The number of processors available to the process."); - // TODO - Uncomment once an implementation for https://github.com/dotnet/runtime/issues/104844 is available. - //private static readonly ObservableCounter s_processCpuTime = s_meter.CreateObservableCounter( - // "dotnet.process.cpu.time", - // GetCpuTime, - // unit: "s", - // description: "CPU time used by the process as reported by the CLR."); + private static readonly ObservableCounter? s_processCpuTime = + OperatingSystem.IsBrowser() || OperatingSystem.IsTvOS() || (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()) ? + null : + s_meter.CreateObservableCounter( + "dotnet.process.cpu.time", + GetCpuTime, + unit: "s", + description: "CPU time used by the process."); public static bool IsEnabled() { @@ -172,8 +175,8 @@ public static bool IsEnabled() || s_threadPoolQueueLength.Enabled || s_assembliesCount.Enabled || s_exceptions.Enabled - || s_processCpuCount.Enabled; - //|| s_processCpuTime.Enabled; + || s_processCpuCount.Enabled + || s_processCpuTime?.Enabled is true; } private static IEnumerable> GetGarbageCollectionCounts() @@ -188,17 +191,20 @@ private static IEnumerable> GetGarbageCollectionCounts() } } - // TODO - Uncomment once an implementation for https://github.com/dotnet/runtime/issues/104844 is available. - //private static IEnumerable> GetCpuTime() - //{ - // if (OperatingSystem.IsBrowser() || OperatingSystem.IsTvOS() || OperatingSystem.IsIOS()) - // yield break; + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [UnsupportedOSPlatform("browser")] + private static IEnumerable> GetCpuTime() + { + Debug.Assert(s_processCpuTime is not null); + Debug.Assert(!OperatingSystem.IsBrowser() && !OperatingSystem.IsTvOS() && !(OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst())); - // ProcessCpuUsage processCpuUsage = Environment.CpuUsage; + Environment.ProcessCpuUsage processCpuUsage = Environment.CpuUsage; - // yield return new(processCpuUsage.UserTime.TotalSeconds, [new KeyValuePair("cpu.mode", "user")]); - // yield return new(processCpuUsage.PrivilegedTime.TotalSeconds, [new KeyValuePair("cpu.mode", "system")]); - //} + yield return new(processCpuUsage.UserTime.TotalSeconds, [new KeyValuePair("cpu.mode", "user")]); + yield return new(processCpuUsage.PrivilegedTime.TotalSeconds, [new KeyValuePair("cpu.mode", "system")]); + } private static IEnumerable> GetHeapSizes() { diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs index f5554720f4551e..c4d38f82303122 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs @@ -112,48 +112,47 @@ public void GcCollectionsCount() } } - // TODO - Uncomment once an implementation for https://github.com/dotnet/runtime/issues/104844 is available. - //[Fact] - //public void CpuTime() - //{ - // using InstrumentRecorder instrumentRecorder = new("dotnet.process.cpu.time"); - - // instrumentRecorder.RecordObservableInstruments(); - - // bool[] foundCpuModes = [false, false]; - - // foreach (Measurement measurement in instrumentRecorder.GetMeasurements().Where(m => m.Value >= 0)) - // { - // var tags = measurement.Tags.ToArray(); - // var tag = tags.SingleOrDefault(k => k.Key == "cpu.mode"); - - // if (tag.Key is not null) - // { - // Assert.True(tag.Value is string, "Expected CPU mode tag to be a string."); - - // string tagValue = (string)tag.Value; - - // switch (tagValue) - // { - // case "user": - // foundCpuModes[0] = true; - // break; - // case "system": - // foundCpuModes[1] = true; - // break; - // default: - // Assert.Fail($"Unexpected CPU mode tag value '{tagValue}'."); - // break; - // } - // } - // } - - // for (int i = 0; i < foundCpuModes.Length; i++) - // { - // var mode = i == 0 ? "user" : "system"; - // Assert.True(foundCpuModes[i], $"Expected to find a measurement for '{mode}' CPU mode."); - // } - //} + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public void CpuTime() + { + using InstrumentRecorder instrumentRecorder = new("dotnet.process.cpu.time"); + + instrumentRecorder.RecordObservableInstruments(); + + bool[] foundCpuModes = [false, false]; + + foreach (Measurement measurement in instrumentRecorder.GetMeasurements().Where(m => m.Value >= 0)) + { + var tags = measurement.Tags.ToArray(); + var tag = tags.SingleOrDefault(k => k.Key == "cpu.mode"); + + if (tag.Key is not null) + { + Assert.True(tag.Value is string, "Expected CPU mode tag to be a string."); + + string tagValue = (string)tag.Value; + + switch (tagValue) + { + case "user": + foundCpuModes[0] = true; + break; + case "system": + foundCpuModes[1] = true; + break; + default: + Assert.Fail($"Unexpected CPU mode tag value '{tagValue}'."); + break; + } + } + } + + for (int i = 0; i < foundCpuModes.Length; i++) + { + var mode = i == 0 ? "user" : "system"; + Assert.True(foundCpuModes[i], $"Expected to find a measurement for '{mode}' CPU mode."); + } + } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] public void ExceptionsCount() diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.FreeBSD.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.FreeBSD.cs index ab2c652ed2cc97..a597bb2f6565ea 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.FreeBSD.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.FreeBSD.cs @@ -34,6 +34,11 @@ public TimeSpan TotalProcessorTime { get { + if (IsCurrentProcess) + { + return Environment.CpuUsage.TotalTime; + } + EnsureState(State.HaveNonExitedId); Interop.Process.proc_stats stat = Interop.Process.GetThreadInfo(_processId, 0); return Process.TicksToTimeSpan(stat.userTime + stat.systemTime); @@ -51,6 +56,11 @@ public TimeSpan UserProcessorTime { get { + if (IsCurrentProcess) + { + return Environment.CpuUsage.UserTime; + } + EnsureState(State.HaveNonExitedId); Interop.Process.proc_stats stat = Interop.Process.GetThreadInfo(_processId, 0); @@ -66,6 +76,11 @@ public TimeSpan PrivilegedProcessorTime { get { + if (IsCurrentProcess) + { + return Environment.CpuUsage.PrivilegedTime; + } + EnsureState(State.HaveNonExitedId); Interop.Process.proc_stats stat = Interop.Process.GetThreadInfo(_processId, 0); diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs index dd1b40fd9a570c..cf5ceb58e831cc 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs @@ -53,10 +53,7 @@ public static Process[] GetProcessesByName(string? processName, string machineNa [SupportedOSPlatform("maccatalyst")] public TimeSpan PrivilegedProcessorTime { - get - { - return TicksToTimeSpan(GetStat().stime); - } + get => IsCurrentProcess ? Environment.CpuUsage.PrivilegedTime : TicksToTimeSpan(GetStat().stime); } /// Gets the time the associated process was started. @@ -132,6 +129,11 @@ public TimeSpan TotalProcessorTime { get { + if (IsCurrentProcess) + { + return Environment.CpuUsage.TotalTime; + } + Interop.procfs.ParsedStat stat = GetStat(); return TicksToTimeSpan(stat.utime + stat.stime); } @@ -146,10 +148,7 @@ public TimeSpan TotalProcessorTime [SupportedOSPlatform("maccatalyst")] public TimeSpan UserProcessorTime { - get - { - return TicksToTimeSpan(GetStat().utime); - } + get => IsCurrentProcess ? Environment.CpuUsage.UserTime : TicksToTimeSpan(GetStat().utime); } partial void EnsureHandleCountPopulated() diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs index 07f55780d82de9..b84938c65e8eea 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs @@ -22,6 +22,11 @@ public TimeSpan PrivilegedProcessorTime { get { + if (IsCurrentProcess) + { + return Environment.CpuUsage.PrivilegedTime; + } + EnsureState(State.HaveNonExitedId); Interop.libproc.rusage_info_v3 info = Interop.libproc.proc_pid_rusage(_processId); return MapTime(info.ri_system_time); @@ -64,6 +69,11 @@ public TimeSpan TotalProcessorTime { get { + if (IsCurrentProcess) + { + return Environment.CpuUsage.TotalTime; + } + EnsureState(State.HaveNonExitedId); Interop.libproc.rusage_info_v3 info = Interop.libproc.proc_pid_rusage(_processId); return MapTime(info.ri_system_time + info.ri_user_time); @@ -81,6 +91,11 @@ public TimeSpan UserProcessorTime { get { + if (IsCurrentProcess) + { + return Environment.CpuUsage.UserTime; + } + EnsureState(State.HaveNonExitedId); Interop.libproc.rusage_info_v3 info = Interop.libproc.proc_pid_rusage(_processId); return MapTime(info.ri_user_time); diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs index 4407fc81e821e6..06a2bd51d6d402 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs @@ -232,7 +232,7 @@ private DateTime ExitTimeCore [SupportedOSPlatform("maccatalyst")] public TimeSpan PrivilegedProcessorTime { - get { return GetProcessTimes().PrivilegedProcessorTime; } + get => IsCurrentProcess ? Environment.CpuUsage.PrivilegedTime : GetProcessTimes().PrivilegedProcessorTime; } /// Gets the time the associated process was started. @@ -251,7 +251,7 @@ internal DateTime StartTimeCore [SupportedOSPlatform("maccatalyst")] public TimeSpan TotalProcessorTime { - get { return GetProcessTimes().TotalProcessorTime; } + get => IsCurrentProcess ? Environment.CpuUsage.TotalTime : GetProcessTimes().TotalProcessorTime; } /// @@ -263,7 +263,7 @@ public TimeSpan TotalProcessorTime [SupportedOSPlatform("maccatalyst")] public TimeSpan UserProcessorTime { - get { return GetProcessTimes().UserProcessorTime; } + get => IsCurrentProcess ? Environment.CpuUsage.UserTime : GetProcessTimes().UserProcessorTime; } /// diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index cc8a0ae13811d3..4218596c129644 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -1106,6 +1106,8 @@ public static Process[] GetProcesses(string machineName) return processes; } + private bool IsCurrentProcess => _processId == Environment.ProcessId; + /// /// /// Returns a new diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.UnixOrBrowser.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.UnixOrBrowser.cs index 3f04333e6b3932..eefd1120cb5710 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.UnixOrBrowser.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.UnixOrBrowser.cs @@ -5,6 +5,7 @@ using System.IO; using System.Reflection; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Text; using System.Threading; @@ -69,5 +70,28 @@ private static int CheckedSysConf(Interop.Sys.SysConfName name) } return (int)result; } + + /// + /// Get the CPU usage, including the process time spent running the application code, the process time spent running the operating system code, + /// and the total time spent running both the application and operating system code. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [UnsupportedOSPlatform("browser")] + public static ProcessCpuUsage CpuUsage + { + get + { + Interop.Sys.ProcessCpuInformation cpuInfo = default; + Interop.Sys.GetCpuUtilization(ref cpuInfo); + + // Division by 100 is to convert the nanoseconds to 100-nanoseconds to match .NET time units (100-nanoseconds). + ulong userTime100Nanoseconds = Math.Min(cpuInfo.lastRecordedUserTime / 100, (ulong)long.MaxValue); + ulong kernelTime100Nanoseconds = Math.Min(cpuInfo.lastRecordedKernelTime / 100, (ulong)long.MaxValue); + + return new ProcessCpuUsage { UserTime = new TimeSpan((long)userTime100Nanoseconds), PrivilegedTime = new TimeSpan((long)kernelTime100Nanoseconds) }; + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs index 82f7271c780fc8..2d4e82dca88748 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs @@ -6,6 +6,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Text; using Microsoft.Win32.SafeHandles; @@ -358,5 +359,20 @@ private static unsafe string[] SegmentCommandLine(char* cmdLine) return arrayBuilder.ToArray(); } + + /// + /// Get the CPU usage, including the process time spent running the application code, the process time spent running the operating system code, + /// and the total time spent running both the application and operating system code. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [UnsupportedOSPlatform("browser")] + public static ProcessCpuUsage CpuUsage + { + get => Interop.Kernel32.GetProcessTimes(Interop.Kernel32.GetCurrentProcess(), out _, out _, out long procKernelTime, out long procUserTime) ? + new ProcessCpuUsage { UserTime = new TimeSpan(procUserTime), PrivilegedTime = new TimeSpan(procKernelTime) } : + new ProcessCpuUsage { UserTime = TimeSpan.Zero, PrivilegedTime = TimeSpan.Zero }; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.cs index aaefefd0d22d4a..654c1d1aaf8ba5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.cs @@ -10,6 +10,31 @@ namespace System { public static partial class Environment { + /// + /// Represents the CPU usage statistics of a process. + /// + /// + /// The CPU usage statistics include information about the time spent by the process in the application code (user mode) and the operating system code (kernel mode), + /// as well as the total time spent by the process in both user mode and kernel mode. + /// + public readonly struct ProcessCpuUsage + { + /// + /// Gets the amount of time the associated process has spent running code inside the application portion of the process (not the operating system code). + /// + public TimeSpan UserTime { get; internal init; } + + /// + /// Gets the amount of time the process has spent running code inside the operating system code. + /// + public TimeSpan PrivilegedTime { get; internal init; } + + /// + /// Gets the amount of time the process has spent utilizing the CPU including the process time spent in the application code and the process time spent in the operating system code. + /// + public TimeSpan TotalTime => UserTime + PrivilegedTime; + } + public static int ProcessorCount { get; } = GetProcessorCount(); /// diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index b88630ba4c592d..db49bbfb30de25 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -2666,6 +2666,13 @@ protected Enum() { } } public static partial class Environment { + public readonly struct ProcessCpuUsage + { + public System.TimeSpan UserTime { get { throw null; } } + public System.TimeSpan PrivilegedTime { get { throw null; } } + public System.TimeSpan TotalTime { get { throw null; } } + } + public static string CommandLine { get { throw null; } } public static string CurrentDirectory { get { throw null; } set { } } public static int CurrentManagedThreadId { get { throw null; } } @@ -2679,6 +2686,11 @@ public static partial class Environment public static System.OperatingSystem OSVersion { get { throw null; } } public static int ProcessId { get { throw null; } } public static int ProcessorCount { get { throw null; } } + [System.Runtime.Versioning.UnsupportedOSPlatform("ios")] + [System.Runtime.Versioning.UnsupportedOSPlatform("tvos")] + [System.Runtime.Versioning.SupportedOSPlatform("maccatalyst")] + [System.Runtime.Versioning.UnsupportedOSPlatform("browser")] + public static ProcessCpuUsage CpuUsage { get { throw null; } } public static string? ProcessPath { get { throw null; } } public static string StackTrace { get { throw null; } } public static string SystemDirectory { get { throw null; } } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/EnvironmentTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/EnvironmentTests.cs index fd47fb81eb7424..b746973b6189ee 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/EnvironmentTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/EnvironmentTests.cs @@ -574,6 +574,43 @@ public void GetLogicalDrives_Windows_MatchesExpectedLetters() } } + [Fact] + public void TestCpuUsage() + { + if ((OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()) || PlatformDetection.IstvOS || PlatformDetection.IsBrowser) + { + // Environment should return 0 for all values + Environment.ProcessCpuUsage usage = Environment.CpuUsage; + Assert.Equal(TimeSpan.Zero, usage.UserTime); + Assert.Equal(TimeSpan.Zero, usage.PrivilegedTime); + Assert.Equal(TimeSpan.Zero, usage.TotalTime); + } + else + { + Process currentProcess = Process.GetCurrentProcess(); + + TimeSpan userTime = currentProcess.UserProcessorTime; + TimeSpan privilegedTime = currentProcess.PrivilegedProcessorTime; + TimeSpan totalTime = currentProcess.TotalProcessorTime; + + Environment.ProcessCpuUsage usage = Environment.CpuUsage; + Assert.True(usage.UserTime.TotalMilliseconds >= 0); + Assert.True(usage.PrivilegedTime.TotalMilliseconds >= 0); + Assert.True(usage.TotalTime.TotalMilliseconds >= 0); + Assert.Equal(usage.TotalTime, usage.UserTime + usage.PrivilegedTime); + + Assert.True(usage.UserTime >= userTime); + Assert.True(usage.PrivilegedTime >= privilegedTime); + Assert.True(usage.TotalTime >= totalTime); + + TimeSpan delta = TimeSpan.FromMinutes(1); + + Assert.True(usage.UserTime - userTime < delta); + Assert.True(usage.PrivilegedTime - privilegedTime < delta); + Assert.True(usage.TotalTime - totalTime < delta); + } + } + [DllImport("kernel32.dll", SetLastError = true)] internal static extern int GetLogicalDrives();