Skip to content

Commit 7069cb3

Browse files
authored
Introducing Environment.CpuUsage (#105152)
* Introducing Environment.CpuUsage * Check Unix builds * increment * update 1 * enable browser test * revert to use init. * Updates * Fix OSX build failures * Fix MACCATALYST builds * Address different feedback * Addressing more feedback * cleanup * revert the resources file change * addressing the feedback * Update the test * kittle tweaks * ref fix * minore comment fix. * revert un-intended change * Fix the test with with the browser * simplify some code
1 parent d285c82 commit 7069cb3

File tree

12 files changed

+220
-70
lines changed

12 files changed

+220
-70
lines changed

src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/RuntimeMetrics.cs

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Generic;
5+
using System.Runtime.Versioning;
56
using System.Threading;
67

78
namespace System.Diagnostics.Metrics
@@ -146,12 +147,14 @@ static RuntimeMetrics()
146147
unit: "{cpu}",
147148
description: "The number of processors available to the process.");
148149

149-
// TODO - Uncomment once an implementation for https://github.com/dotnet/runtime/issues/104844 is available.
150-
//private static readonly ObservableCounter<double> s_processCpuTime = s_meter.CreateObservableCounter(
151-
// "dotnet.process.cpu.time",
152-
// GetCpuTime,
153-
// unit: "s",
154-
// description: "CPU time used by the process as reported by the CLR.");
150+
private static readonly ObservableCounter<double>? s_processCpuTime =
151+
OperatingSystem.IsBrowser() || OperatingSystem.IsTvOS() || (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()) ?
152+
null :
153+
s_meter.CreateObservableCounter(
154+
"dotnet.process.cpu.time",
155+
GetCpuTime,
156+
unit: "s",
157+
description: "CPU time used by the process.");
155158

156159
public static bool IsEnabled()
157160
{
@@ -172,8 +175,8 @@ public static bool IsEnabled()
172175
|| s_threadPoolQueueLength.Enabled
173176
|| s_assembliesCount.Enabled
174177
|| s_exceptions.Enabled
175-
|| s_processCpuCount.Enabled;
176-
//|| s_processCpuTime.Enabled;
178+
|| s_processCpuCount.Enabled
179+
|| s_processCpuTime?.Enabled is true;
177180
}
178181

179182
private static IEnumerable<Measurement<long>> GetGarbageCollectionCounts()
@@ -188,17 +191,20 @@ private static IEnumerable<Measurement<long>> GetGarbageCollectionCounts()
188191
}
189192
}
190193

191-
// TODO - Uncomment once an implementation for https://github.com/dotnet/runtime/issues/104844 is available.
192-
//private static IEnumerable<Measurement<double>> GetCpuTime()
193-
//{
194-
// if (OperatingSystem.IsBrowser() || OperatingSystem.IsTvOS() || OperatingSystem.IsIOS())
195-
// yield break;
194+
[SupportedOSPlatform("maccatalyst")]
195+
[UnsupportedOSPlatform("ios")]
196+
[UnsupportedOSPlatform("tvos")]
197+
[UnsupportedOSPlatform("browser")]
198+
private static IEnumerable<Measurement<double>> GetCpuTime()
199+
{
200+
Debug.Assert(s_processCpuTime is not null);
201+
Debug.Assert(!OperatingSystem.IsBrowser() && !OperatingSystem.IsTvOS() && !(OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()));
196202

197-
// ProcessCpuUsage processCpuUsage = Environment.CpuUsage;
203+
Environment.ProcessCpuUsage processCpuUsage = Environment.CpuUsage;
198204

199-
// yield return new(processCpuUsage.UserTime.TotalSeconds, [new KeyValuePair<string, object?>("cpu.mode", "user")]);
200-
// yield return new(processCpuUsage.PrivilegedTime.TotalSeconds, [new KeyValuePair<string, object?>("cpu.mode", "system")]);
201-
//}
205+
yield return new(processCpuUsage.UserTime.TotalSeconds, [new KeyValuePair<string, object?>("cpu.mode", "user")]);
206+
yield return new(processCpuUsage.PrivilegedTime.TotalSeconds, [new KeyValuePair<string, object?>("cpu.mode", "system")]);
207+
}
202208

203209
private static IEnumerable<Measurement<long>> GetHeapSizes()
204210
{

src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -112,48 +112,47 @@ public void GcCollectionsCount()
112112
}
113113
}
114114

115-
// TODO - Uncomment once an implementation for https://github.com/dotnet/runtime/issues/104844 is available.
116-
//[Fact]
117-
//public void CpuTime()
118-
//{
119-
// using InstrumentRecorder<double> instrumentRecorder = new("dotnet.process.cpu.time");
120-
121-
// instrumentRecorder.RecordObservableInstruments();
122-
123-
// bool[] foundCpuModes = [false, false];
124-
125-
// foreach (Measurement<double> measurement in instrumentRecorder.GetMeasurements().Where(m => m.Value >= 0))
126-
// {
127-
// var tags = measurement.Tags.ToArray();
128-
// var tag = tags.SingleOrDefault(k => k.Key == "cpu.mode");
129-
130-
// if (tag.Key is not null)
131-
// {
132-
// Assert.True(tag.Value is string, "Expected CPU mode tag to be a string.");
133-
134-
// string tagValue = (string)tag.Value;
135-
136-
// switch (tagValue)
137-
// {
138-
// case "user":
139-
// foundCpuModes[0] = true;
140-
// break;
141-
// case "system":
142-
// foundCpuModes[1] = true;
143-
// break;
144-
// default:
145-
// Assert.Fail($"Unexpected CPU mode tag value '{tagValue}'.");
146-
// break;
147-
// }
148-
// }
149-
// }
150-
151-
// for (int i = 0; i < foundCpuModes.Length; i++)
152-
// {
153-
// var mode = i == 0 ? "user" : "system";
154-
// Assert.True(foundCpuModes[i], $"Expected to find a measurement for '{mode}' CPU mode.");
155-
// }
156-
//}
115+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
116+
public void CpuTime()
117+
{
118+
using InstrumentRecorder<double> instrumentRecorder = new("dotnet.process.cpu.time");
119+
120+
instrumentRecorder.RecordObservableInstruments();
121+
122+
bool[] foundCpuModes = [false, false];
123+
124+
foreach (Measurement<double> measurement in instrumentRecorder.GetMeasurements().Where(m => m.Value >= 0))
125+
{
126+
var tags = measurement.Tags.ToArray();
127+
var tag = tags.SingleOrDefault(k => k.Key == "cpu.mode");
128+
129+
if (tag.Key is not null)
130+
{
131+
Assert.True(tag.Value is string, "Expected CPU mode tag to be a string.");
132+
133+
string tagValue = (string)tag.Value;
134+
135+
switch (tagValue)
136+
{
137+
case "user":
138+
foundCpuModes[0] = true;
139+
break;
140+
case "system":
141+
foundCpuModes[1] = true;
142+
break;
143+
default:
144+
Assert.Fail($"Unexpected CPU mode tag value '{tagValue}'.");
145+
break;
146+
}
147+
}
148+
}
149+
150+
for (int i = 0; i < foundCpuModes.Length; i++)
151+
{
152+
var mode = i == 0 ? "user" : "system";
153+
Assert.True(foundCpuModes[i], $"Expected to find a measurement for '{mode}' CPU mode.");
154+
}
155+
}
157156

158157
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
159158
public void ExceptionsCount()

src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.FreeBSD.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ public TimeSpan TotalProcessorTime
3434
{
3535
get
3636
{
37+
if (IsCurrentProcess)
38+
{
39+
return Environment.CpuUsage.TotalTime;
40+
}
41+
3742
EnsureState(State.HaveNonExitedId);
3843
Interop.Process.proc_stats stat = Interop.Process.GetThreadInfo(_processId, 0);
3944
return Process.TicksToTimeSpan(stat.userTime + stat.systemTime);
@@ -51,6 +56,11 @@ public TimeSpan UserProcessorTime
5156
{
5257
get
5358
{
59+
if (IsCurrentProcess)
60+
{
61+
return Environment.CpuUsage.UserTime;
62+
}
63+
5464
EnsureState(State.HaveNonExitedId);
5565

5666
Interop.Process.proc_stats stat = Interop.Process.GetThreadInfo(_processId, 0);
@@ -66,6 +76,11 @@ public TimeSpan PrivilegedProcessorTime
6676
{
6777
get
6878
{
79+
if (IsCurrentProcess)
80+
{
81+
return Environment.CpuUsage.PrivilegedTime;
82+
}
83+
6984
EnsureState(State.HaveNonExitedId);
7085

7186
Interop.Process.proc_stats stat = Interop.Process.GetThreadInfo(_processId, 0);

src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,7 @@ public static Process[] GetProcessesByName(string? processName, string machineNa
5353
[SupportedOSPlatform("maccatalyst")]
5454
public TimeSpan PrivilegedProcessorTime
5555
{
56-
get
57-
{
58-
return TicksToTimeSpan(GetStat().stime);
59-
}
56+
get => IsCurrentProcess ? Environment.CpuUsage.PrivilegedTime : TicksToTimeSpan(GetStat().stime);
6057
}
6158

6259
/// <summary>Gets the time the associated process was started.</summary>
@@ -132,6 +129,11 @@ public TimeSpan TotalProcessorTime
132129
{
133130
get
134131
{
132+
if (IsCurrentProcess)
133+
{
134+
return Environment.CpuUsage.TotalTime;
135+
}
136+
135137
Interop.procfs.ParsedStat stat = GetStat();
136138
return TicksToTimeSpan(stat.utime + stat.stime);
137139
}
@@ -146,10 +148,7 @@ public TimeSpan TotalProcessorTime
146148
[SupportedOSPlatform("maccatalyst")]
147149
public TimeSpan UserProcessorTime
148150
{
149-
get
150-
{
151-
return TicksToTimeSpan(GetStat().utime);
152-
}
151+
get => IsCurrentProcess ? Environment.CpuUsage.UserTime : TicksToTimeSpan(GetStat().utime);
153152
}
154153

155154
partial void EnsureHandleCountPopulated()

src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ public TimeSpan PrivilegedProcessorTime
2222
{
2323
get
2424
{
25+
if (IsCurrentProcess)
26+
{
27+
return Environment.CpuUsage.PrivilegedTime;
28+
}
29+
2530
EnsureState(State.HaveNonExitedId);
2631
Interop.libproc.rusage_info_v3 info = Interop.libproc.proc_pid_rusage(_processId);
2732
return MapTime(info.ri_system_time);
@@ -64,6 +69,11 @@ public TimeSpan TotalProcessorTime
6469
{
6570
get
6671
{
72+
if (IsCurrentProcess)
73+
{
74+
return Environment.CpuUsage.TotalTime;
75+
}
76+
6777
EnsureState(State.HaveNonExitedId);
6878
Interop.libproc.rusage_info_v3 info = Interop.libproc.proc_pid_rusage(_processId);
6979
return MapTime(info.ri_system_time + info.ri_user_time);
@@ -81,6 +91,11 @@ public TimeSpan UserProcessorTime
8191
{
8292
get
8393
{
94+
if (IsCurrentProcess)
95+
{
96+
return Environment.CpuUsage.UserTime;
97+
}
98+
8499
EnsureState(State.HaveNonExitedId);
85100
Interop.libproc.rusage_info_v3 info = Interop.libproc.proc_pid_rusage(_processId);
86101
return MapTime(info.ri_user_time);

src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ private DateTime ExitTimeCore
232232
[SupportedOSPlatform("maccatalyst")]
233233
public TimeSpan PrivilegedProcessorTime
234234
{
235-
get { return GetProcessTimes().PrivilegedProcessorTime; }
235+
get => IsCurrentProcess ? Environment.CpuUsage.PrivilegedTime : GetProcessTimes().PrivilegedProcessorTime;
236236
}
237237

238238
/// <summary>Gets the time the associated process was started.</summary>
@@ -251,7 +251,7 @@ internal DateTime StartTimeCore
251251
[SupportedOSPlatform("maccatalyst")]
252252
public TimeSpan TotalProcessorTime
253253
{
254-
get { return GetProcessTimes().TotalProcessorTime; }
254+
get => IsCurrentProcess ? Environment.CpuUsage.TotalTime : GetProcessTimes().TotalProcessorTime;
255255
}
256256

257257
/// <summary>
@@ -263,7 +263,7 @@ public TimeSpan TotalProcessorTime
263263
[SupportedOSPlatform("maccatalyst")]
264264
public TimeSpan UserProcessorTime
265265
{
266-
get { return GetProcessTimes().UserProcessorTime; }
266+
get => IsCurrentProcess ? Environment.CpuUsage.UserTime : GetProcessTimes().UserProcessorTime;
267267
}
268268

269269
/// <summary>

src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,8 @@ public static Process[] GetProcesses(string machineName)
11061106
return processes;
11071107
}
11081108

1109+
private bool IsCurrentProcess => _processId == Environment.ProcessId;
1110+
11091111
/// <devdoc>
11101112
/// <para>
11111113
/// Returns a new <see cref='System.Diagnostics.Process'/>

src/libraries/System.Private.CoreLib/src/System/Environment.UnixOrBrowser.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.IO;
66
using System.Reflection;
77
using System.Runtime.InteropServices;
8+
using System.Runtime.Versioning;
89
using System.Text;
910
using System.Threading;
1011

@@ -69,5 +70,28 @@ private static int CheckedSysConf(Interop.Sys.SysConfName name)
6970
}
7071
return (int)result;
7172
}
73+
74+
/// <summary>
75+
/// Get the CPU usage, including the process time spent running the application code, the process time spent running the operating system code,
76+
/// and the total time spent running both the application and operating system code.
77+
/// </summary>
78+
[SupportedOSPlatform("maccatalyst")]
79+
[UnsupportedOSPlatform("ios")]
80+
[UnsupportedOSPlatform("tvos")]
81+
[UnsupportedOSPlatform("browser")]
82+
public static ProcessCpuUsage CpuUsage
83+
{
84+
get
85+
{
86+
Interop.Sys.ProcessCpuInformation cpuInfo = default;
87+
Interop.Sys.GetCpuUtilization(ref cpuInfo);
88+
89+
// Division by 100 is to convert the nanoseconds to 100-nanoseconds to match .NET time units (100-nanoseconds).
90+
ulong userTime100Nanoseconds = Math.Min(cpuInfo.lastRecordedUserTime / 100, (ulong)long.MaxValue);
91+
ulong kernelTime100Nanoseconds = Math.Min(cpuInfo.lastRecordedKernelTime / 100, (ulong)long.MaxValue);
92+
93+
return new ProcessCpuUsage { UserTime = new TimeSpan((long)userTime100Nanoseconds), PrivilegedTime = new TimeSpan((long)kernelTime100Nanoseconds) };
94+
}
95+
}
7296
}
7397
}

src/libraries/System.Private.CoreLib/src/System/Environment.Windows.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.IO;
77
using System.Runtime.CompilerServices;
88
using System.Runtime.InteropServices;
9+
using System.Runtime.Versioning;
910
using System.Text;
1011
using Microsoft.Win32.SafeHandles;
1112

@@ -358,5 +359,20 @@ private static unsafe string[] SegmentCommandLine(char* cmdLine)
358359

359360
return arrayBuilder.ToArray();
360361
}
362+
363+
/// <summary>
364+
/// Get the CPU usage, including the process time spent running the application code, the process time spent running the operating system code,
365+
/// and the total time spent running both the application and operating system code.
366+
/// </summary>
367+
[SupportedOSPlatform("maccatalyst")]
368+
[UnsupportedOSPlatform("ios")]
369+
[UnsupportedOSPlatform("tvos")]
370+
[UnsupportedOSPlatform("browser")]
371+
public static ProcessCpuUsage CpuUsage
372+
{
373+
get => Interop.Kernel32.GetProcessTimes(Interop.Kernel32.GetCurrentProcess(), out _, out _, out long procKernelTime, out long procUserTime) ?
374+
new ProcessCpuUsage { UserTime = new TimeSpan(procUserTime), PrivilegedTime = new TimeSpan(procKernelTime) } :
375+
new ProcessCpuUsage { UserTime = TimeSpan.Zero, PrivilegedTime = TimeSpan.Zero };
376+
}
361377
}
362378
}

0 commit comments

Comments
 (0)