Skip to content

Commit 16f8730

Browse files
authored
Simplify initialization of RuntimeMetrics (#105539)
* Simplify initialization of RuntimeMetrics - Trigger the RuntimeMetrics initialization only when actually needed in the MeterListener constructor. - Delete the lock-ordering workaround and wrong comment introduced in #105259. Trigger the RuntimeMetrics initialization only when actually needed should make the lock-ordering workarond unnecessary. * Delete unnecessary static fields * Update src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/RuntimeMetrics.cs
1 parent 432c4a0 commit 16f8730

File tree

3 files changed

+133
-163
lines changed

3 files changed

+133
-163
lines changed

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

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,6 @@ protected void Publish()
8989
return;
9090
}
9191

92-
// MeterListener has a static constructor that creates runtime metrics instruments.
93-
// We need to ensure this static constructor is called before starting to publish the instrument.
94-
// This is necessary because creating runtime metrics instruments will cause re-entry to the Publish method,
95-
// potentially resulting in a deadlock due to the SyncObject lock.
96-
// Sequence of the deadlock:
97-
// 1. An application creates an early instrument (e.g., Counter) before the MeterListener static constructor is executed.
98-
// 2. Instrument.Publish is called and enters the SyncObject lock.
99-
// 3. Within the lock block, MeterListener is called, triggering its static constructor.
100-
// 4. The static constructor creates runtime metrics instruments, causing re-entry to Instrument.Publish and leading to a deadlock.
101-
RuntimeHelpers.RunClassConstructor(typeof(MeterListener).TypeHandle);
102-
10392
List<MeterListener>? allListeners = null;
10493
lock (Instrument.SyncObject)
10594
{

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,17 @@ public sealed class MeterListener : IDisposable
3333
private MeasurementCallback<double> _doubleMeasurementCallback = (instrument, measurement, tags, state) => { /* no-op */ };
3434
private MeasurementCallback<decimal> _decimalMeasurementCallback = (instrument, measurement, tags, state) => { /* no-op */ };
3535

36-
static MeterListener()
36+
/// <summary>
37+
/// Creates a MeterListener object.
38+
/// </summary>
39+
public MeterListener()
3740
{
3841
#if NET9_0_OR_GREATER
3942
// This ensures that the static Meter gets created before any listeners exist.
40-
_ = RuntimeMetrics.IsEnabled();
43+
RuntimeMetrics.EnsureInitialized();
4144
#endif
4245
}
4346

44-
/// <summary>
45-
/// Creates a MeterListener object.
46-
/// </summary>
47-
public MeterListener() { }
48-
4947
/// <summary>
5048
/// Callbacks to get notification when an instrument is published.
5149
/// </summary>

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

Lines changed: 128 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,123 @@ internal static class RuntimeMetrics
2020

2121
private static readonly int s_maxGenerations = Math.Min(GC.GetGCMemoryInfo().GenerationInfo.Length, s_genNames.Length);
2222

23+
private static readonly Counter<long> s_exceptions;
24+
25+
public static void EnsureInitialized()
26+
{
27+
// Dummy method to ensure that the static constructor run and created the meters
28+
}
29+
2330
static RuntimeMetrics()
2431
{
32+
s_meter.CreateObservableCounter(
33+
"dotnet.gc.collections",
34+
GetGarbageCollectionCounts,
35+
unit: "{collection}",
36+
description: "The number of garbage collections that have occurred since the process has started.");
37+
38+
s_meter.CreateObservableUpDownCounter(
39+
"dotnet.process.memory.working_set",
40+
() => Environment.WorkingSet,
41+
unit: "By",
42+
description: "The number of bytes of physical memory mapped to the process context.");
43+
44+
s_meter.CreateObservableCounter(
45+
"dotnet.gc.heap.total_allocated",
46+
() => GC.GetTotalAllocatedBytes(),
47+
unit: "By",
48+
description: "The approximate number of bytes allocated on the managed GC heap since the process has started. The returned value does not include any native allocations.");
49+
50+
s_meter.CreateObservableUpDownCounter(
51+
"dotnet.gc.last_collection.memory.committed_size",
52+
() =>
53+
{
54+
GCMemoryInfo gcInfo = GC.GetGCMemoryInfo();
55+
56+
return gcInfo.Index == 0
57+
? Array.Empty<Measurement<long>>()
58+
: [new(gcInfo.TotalCommittedBytes)];
59+
},
60+
unit: "By",
61+
description: "The amount of committed virtual memory in use by the .NET GC, as observed during the latest garbage collection.");
62+
63+
s_meter.CreateObservableUpDownCounter(
64+
"dotnet.gc.last_collection.heap.size",
65+
GetHeapSizes,
66+
unit: "By",
67+
description: "The managed GC heap size (including fragmentation), as observed during the latest garbage collection.");
68+
69+
s_meter.CreateObservableUpDownCounter(
70+
"dotnet.gc.last_collection.heap.fragmentation.size",
71+
GetHeapFragmentation,
72+
unit: "By",
73+
description: "The heap fragmentation, as observed during the latest garbage collection.");
74+
75+
s_meter.CreateObservableCounter(
76+
"dotnet.gc.pause.time",
77+
() => GC.GetTotalPauseDuration().TotalSeconds,
78+
unit: "s",
79+
description: "The total amount of time paused in GC since the process has started.");
80+
81+
s_meter.CreateObservableCounter(
82+
"dotnet.jit.compiled_il.size",
83+
() => Runtime.JitInfo.GetCompiledILBytes(),
84+
unit: "By",
85+
description: "Count of bytes of intermediate language that have been compiled since the process has started.");
86+
87+
s_meter.CreateObservableCounter(
88+
"dotnet.jit.compiled_methods",
89+
() => Runtime.JitInfo.GetCompiledMethodCount(),
90+
unit: "{method}",
91+
description: "The number of times the JIT compiler (re)compiled methods since the process has started.");
92+
93+
s_meter.CreateObservableCounter(
94+
"dotnet.jit.compilation.time",
95+
() => Runtime.JitInfo.GetCompilationTime().TotalSeconds,
96+
unit: "s",
97+
description: "The number of times the JIT compiler (re)compiled methods since the process has started.");
98+
99+
s_meter.CreateObservableCounter(
100+
"dotnet.monitor.lock_contentions",
101+
() => Monitor.LockContentionCount,
102+
unit: "{contention}",
103+
description: "The number of times there was contention when trying to acquire a monitor lock since the process has started.");
104+
105+
s_meter.CreateObservableCounter(
106+
"dotnet.thread_pool.thread.count",
107+
() => (long)ThreadPool.ThreadCount,
108+
unit: "{thread}",
109+
description: "The number of thread pool threads that currently exist.");
110+
111+
s_meter.CreateObservableCounter(
112+
"dotnet.thread_pool.work_item.count",
113+
() => ThreadPool.CompletedWorkItemCount,
114+
unit: "{work_item}",
115+
description: "The number of work items that the thread pool has completed since the process has started.");
116+
117+
s_meter.CreateObservableCounter(
118+
"dotnet.thread_pool.queue.length",
119+
() => ThreadPool.PendingWorkItemCount,
120+
unit: "{work_item}",
121+
description: "The number of work items that are currently queued to be processed by the thread pool.");
122+
123+
s_meter.CreateObservableUpDownCounter(
124+
"dotnet.timer.count",
125+
() => Timer.ActiveCount,
126+
unit: "{timer}",
127+
description: "The number of timer instances that are currently active. An active timer is registered to tick at some point in the future and has not yet been canceled.");
128+
129+
s_meter.CreateObservableUpDownCounter(
130+
"dotnet.assembly.count",
131+
() => (long)AppDomain.CurrentDomain.GetAssemblies().Length,
132+
unit: "{assembly}",
133+
description: "The number of .NET assemblies that are currently loaded.");
134+
135+
s_exceptions = s_meter.CreateCounter<long>(
136+
"dotnet.exceptions",
137+
unit: "{exception}",
138+
description: "The number of exceptions that have been thrown in managed code.");
139+
25140
AppDomain.CurrentDomain.FirstChanceException += (source, e) =>
26141
{
27142
// Avoid recursion if the listener itself throws an exception while recording the measurement
@@ -31,152 +146,21 @@ static RuntimeMetrics()
31146
s_exceptions.Add(1, new KeyValuePair<string, object?>("error.type", e.Exception.GetType().Name));
32147
t_handlingFirstChanceException = false;
33148
};
34-
}
35149

36-
private static readonly ObservableCounter<long> s_gcCollections = s_meter.CreateObservableCounter(
37-
"dotnet.gc.collections",
38-
GetGarbageCollectionCounts,
39-
unit: "{collection}",
40-
description: "The number of garbage collections that have occurred since the process has started.");
41-
42-
private static readonly ObservableUpDownCounter<long> s_processWorkingSet = s_meter.CreateObservableUpDownCounter(
43-
"dotnet.process.memory.working_set",
44-
() => Environment.WorkingSet,
45-
unit: "By",
46-
description: "The number of bytes of physical memory mapped to the process context.");
47-
48-
private static readonly ObservableCounter<long> s_gcHeapTotalAllocated = s_meter.CreateObservableCounter(
49-
"dotnet.gc.heap.total_allocated",
50-
() => GC.GetTotalAllocatedBytes(),
51-
unit: "By",
52-
description: "The approximate number of bytes allocated on the managed GC heap since the process has started. The returned value does not include any native allocations.");
53-
54-
private static readonly ObservableUpDownCounter<long> s_gcLastCollectionMemoryCommitted = s_meter.CreateObservableUpDownCounter(
55-
"dotnet.gc.last_collection.memory.committed_size",
56-
() =>
150+
s_meter.CreateObservableUpDownCounter(
151+
"dotnet.process.cpu.count",
152+
() => (long)Environment.ProcessorCount,
153+
unit: "{cpu}",
154+
description: "The number of processors available to the process.");
155+
156+
if (!OperatingSystem.IsBrowser() && !OperatingSystem.IsTvOS() && !(OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()))
57157
{
58-
GCMemoryInfo gcInfo = GC.GetGCMemoryInfo();
59-
60-
return gcInfo.Index == 0
61-
? Array.Empty<Measurement<long>>()
62-
: [new(gcInfo.TotalCommittedBytes)];
63-
},
64-
unit: "By",
65-
description: "The amount of committed virtual memory in use by the .NET GC, as observed during the latest garbage collection.");
66-
67-
private static readonly ObservableUpDownCounter<long> s_gcLastCollectionHeapSize = s_meter.CreateObservableUpDownCounter(
68-
"dotnet.gc.last_collection.heap.size",
69-
GetHeapSizes,
70-
unit: "By",
71-
description: "The managed GC heap size (including fragmentation), as observed during the latest garbage collection.");
72-
73-
private static readonly ObservableUpDownCounter<long> s_gcLastCollectionFragmentationSize = s_meter.CreateObservableUpDownCounter(
74-
"dotnet.gc.last_collection.heap.fragmentation.size",
75-
GetHeapFragmentation,
76-
unit: "By",
77-
description: "The heap fragmentation, as observed during the latest garbage collection.");
78-
79-
private static readonly ObservableCounter<double> s_gcPauseTime = s_meter.CreateObservableCounter(
80-
"dotnet.gc.pause.time",
81-
() => GC.GetTotalPauseDuration().TotalSeconds,
82-
unit: "s",
83-
description: "The total amount of time paused in GC since the process has started.");
84-
85-
private static readonly ObservableCounter<long> s_jitCompiledSize = s_meter.CreateObservableCounter(
86-
"dotnet.jit.compiled_il.size",
87-
() => Runtime.JitInfo.GetCompiledILBytes(),
88-
unit: "By",
89-
description: "Count of bytes of intermediate language that have been compiled since the process has started.");
90-
91-
private static readonly ObservableCounter<long> s_jitCompiledMethodCount = s_meter.CreateObservableCounter(
92-
"dotnet.jit.compiled_methods",
93-
() => Runtime.JitInfo.GetCompiledMethodCount(),
94-
unit: "{method}",
95-
description: "The number of times the JIT compiler (re)compiled methods since the process has started.");
96-
97-
private static readonly ObservableCounter<double> s_jitCompilationTime = s_meter.CreateObservableCounter(
98-
"dotnet.jit.compilation.time",
99-
() => Runtime.JitInfo.GetCompilationTime().TotalSeconds,
100-
unit: "s",
101-
description: "The number of times the JIT compiler (re)compiled methods since the process has started.");
102-
103-
private static readonly ObservableCounter<long> s_monitorLockContention = s_meter.CreateObservableCounter(
104-
"dotnet.monitor.lock_contentions",
105-
() => Monitor.LockContentionCount,
106-
unit: "{contention}",
107-
description: "The number of times there was contention when trying to acquire a monitor lock since the process has started.");
108-
109-
private static readonly ObservableCounter<long> s_threadPoolThreadCount = s_meter.CreateObservableCounter(
110-
"dotnet.thread_pool.thread.count",
111-
() => (long)ThreadPool.ThreadCount,
112-
unit: "{thread}",
113-
description: "The number of thread pool threads that currently exist.");
114-
115-
private static readonly ObservableCounter<long> s_threadPoolCompletedWorkItems = s_meter.CreateObservableCounter(
116-
"dotnet.thread_pool.work_item.count",
117-
() => ThreadPool.CompletedWorkItemCount,
118-
unit: "{work_item}",
119-
description: "The number of work items that the thread pool has completed since the process has started.");
120-
121-
private static readonly ObservableCounter<long> s_threadPoolQueueLength = s_meter.CreateObservableCounter(
122-
"dotnet.thread_pool.queue.length",
123-
() => ThreadPool.PendingWorkItemCount,
124-
unit: "{work_item}",
125-
description: "The number of work items that are currently queued to be processed by the thread pool.");
126-
127-
private static readonly ObservableUpDownCounter<long> s_timerCount = s_meter.CreateObservableUpDownCounter(
128-
"dotnet.timer.count",
129-
() => Timer.ActiveCount,
130-
unit: "{timer}",
131-
description: "The number of timer instances that are currently active. An active timer is registered to tick at some point in the future and has not yet been canceled.");
132-
133-
private static readonly ObservableUpDownCounter<long> s_assembliesCount = s_meter.CreateObservableUpDownCounter(
134-
"dotnet.assembly.count",
135-
() => (long)AppDomain.CurrentDomain.GetAssemblies().Length,
136-
unit: "{assembly}",
137-
description: "The number of .NET assemblies that are currently loaded.");
138-
139-
private static readonly Counter<long> s_exceptions = s_meter.CreateCounter<long>(
140-
"dotnet.exceptions",
141-
unit: "{exception}",
142-
description: "The number of exceptions that have been thrown in managed code.");
143-
144-
private static readonly ObservableUpDownCounter<long> s_processCpuCount = s_meter.CreateObservableUpDownCounter(
145-
"dotnet.process.cpu.count",
146-
() => (long)Environment.ProcessorCount,
147-
unit: "{cpu}",
148-
description: "The number of processors available to the process.");
149-
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.");
158-
159-
public static bool IsEnabled()
160-
{
161-
return s_gcCollections.Enabled
162-
|| s_processWorkingSet.Enabled
163-
|| s_gcHeapTotalAllocated.Enabled
164-
|| s_gcLastCollectionMemoryCommitted.Enabled
165-
|| s_gcLastCollectionHeapSize.Enabled
166-
|| s_gcLastCollectionFragmentationSize.Enabled
167-
|| s_gcPauseTime.Enabled
168-
|| s_jitCompiledSize.Enabled
169-
|| s_jitCompiledMethodCount.Enabled
170-
|| s_jitCompilationTime.Enabled
171-
|| s_monitorLockContention.Enabled
172-
|| s_timerCount.Enabled
173-
|| s_threadPoolThreadCount.Enabled
174-
|| s_threadPoolCompletedWorkItems.Enabled
175-
|| s_threadPoolQueueLength.Enabled
176-
|| s_assembliesCount.Enabled
177-
|| s_exceptions.Enabled
178-
|| s_processCpuCount.Enabled
179-
|| s_processCpuTime?.Enabled is true;
158+
s_meter.CreateObservableCounter(
159+
"dotnet.process.cpu.time",
160+
GetCpuTime,
161+
unit: "s",
162+
description: "CPU time used by the process.");
163+
}
180164
}
181165

182166
private static IEnumerable<Measurement<long>> GetGarbageCollectionCounts()
@@ -197,7 +181,6 @@ private static IEnumerable<Measurement<long>> GetGarbageCollectionCounts()
197181
[SupportedOSPlatform("maccatalyst")]
198182
private static IEnumerable<Measurement<double>> GetCpuTime()
199183
{
200-
Debug.Assert(s_processCpuTime is not null);
201184
Debug.Assert(!OperatingSystem.IsBrowser() && !OperatingSystem.IsTvOS() && !(OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()));
202185

203186
Environment.ProcessCpuUsage processCpuUsage = Environment.CpuUsage;

0 commit comments

Comments
 (0)