Skip to content

Commit

Permalink
Simplify initialization of RuntimeMetrics (#105539)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jkotas authored Jul 26, 2024
1 parent 432c4a0 commit 16f8730
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 163 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,6 @@ protected void Publish()
return;
}

// MeterListener has a static constructor that creates runtime metrics instruments.
// We need to ensure this static constructor is called before starting to publish the instrument.
// This is necessary because creating runtime metrics instruments will cause re-entry to the Publish method,
// potentially resulting in a deadlock due to the SyncObject lock.
// Sequence of the deadlock:
// 1. An application creates an early instrument (e.g., Counter) before the MeterListener static constructor is executed.
// 2. Instrument.Publish is called and enters the SyncObject lock.
// 3. Within the lock block, MeterListener is called, triggering its static constructor.
// 4. The static constructor creates runtime metrics instruments, causing re-entry to Instrument.Publish and leading to a deadlock.
RuntimeHelpers.RunClassConstructor(typeof(MeterListener).TypeHandle);

List<MeterListener>? allListeners = null;
lock (Instrument.SyncObject)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,17 @@ public sealed class MeterListener : IDisposable
private MeasurementCallback<double> _doubleMeasurementCallback = (instrument, measurement, tags, state) => { /* no-op */ };
private MeasurementCallback<decimal> _decimalMeasurementCallback = (instrument, measurement, tags, state) => { /* no-op */ };

static MeterListener()
/// <summary>
/// Creates a MeterListener object.
/// </summary>
public MeterListener()
{
#if NET9_0_OR_GREATER
// This ensures that the static Meter gets created before any listeners exist.
_ = RuntimeMetrics.IsEnabled();
RuntimeMetrics.EnsureInitialized();
#endif
}

/// <summary>
/// Creates a MeterListener object.
/// </summary>
public MeterListener() { }

/// <summary>
/// Callbacks to get notification when an instrument is published.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,123 @@ internal static class RuntimeMetrics

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

private static readonly Counter<long> s_exceptions;

public static void EnsureInitialized()
{
// Dummy method to ensure that the static constructor run and created the meters
}

static RuntimeMetrics()
{
s_meter.CreateObservableCounter(
"dotnet.gc.collections",
GetGarbageCollectionCounts,
unit: "{collection}",
description: "The number of garbage collections that have occurred since the process has started.");

s_meter.CreateObservableUpDownCounter(
"dotnet.process.memory.working_set",
() => Environment.WorkingSet,
unit: "By",
description: "The number of bytes of physical memory mapped to the process context.");

s_meter.CreateObservableCounter(
"dotnet.gc.heap.total_allocated",
() => GC.GetTotalAllocatedBytes(),
unit: "By",
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.");

s_meter.CreateObservableUpDownCounter(
"dotnet.gc.last_collection.memory.committed_size",
() =>
{
GCMemoryInfo gcInfo = GC.GetGCMemoryInfo();

return gcInfo.Index == 0
? Array.Empty<Measurement<long>>()
: [new(gcInfo.TotalCommittedBytes)];
},
unit: "By",
description: "The amount of committed virtual memory in use by the .NET GC, as observed during the latest garbage collection.");

s_meter.CreateObservableUpDownCounter(
"dotnet.gc.last_collection.heap.size",
GetHeapSizes,
unit: "By",
description: "The managed GC heap size (including fragmentation), as observed during the latest garbage collection.");

s_meter.CreateObservableUpDownCounter(
"dotnet.gc.last_collection.heap.fragmentation.size",
GetHeapFragmentation,
unit: "By",
description: "The heap fragmentation, as observed during the latest garbage collection.");

s_meter.CreateObservableCounter(
"dotnet.gc.pause.time",
() => GC.GetTotalPauseDuration().TotalSeconds,
unit: "s",
description: "The total amount of time paused in GC since the process has started.");

s_meter.CreateObservableCounter(
"dotnet.jit.compiled_il.size",
() => Runtime.JitInfo.GetCompiledILBytes(),
unit: "By",
description: "Count of bytes of intermediate language that have been compiled since the process has started.");

s_meter.CreateObservableCounter(
"dotnet.jit.compiled_methods",
() => Runtime.JitInfo.GetCompiledMethodCount(),
unit: "{method}",
description: "The number of times the JIT compiler (re)compiled methods since the process has started.");

s_meter.CreateObservableCounter(
"dotnet.jit.compilation.time",
() => Runtime.JitInfo.GetCompilationTime().TotalSeconds,
unit: "s",
description: "The number of times the JIT compiler (re)compiled methods since the process has started.");

s_meter.CreateObservableCounter(
"dotnet.monitor.lock_contentions",
() => Monitor.LockContentionCount,
unit: "{contention}",
description: "The number of times there was contention when trying to acquire a monitor lock since the process has started.");

s_meter.CreateObservableCounter(
"dotnet.thread_pool.thread.count",
() => (long)ThreadPool.ThreadCount,
unit: "{thread}",
description: "The number of thread pool threads that currently exist.");

s_meter.CreateObservableCounter(
"dotnet.thread_pool.work_item.count",
() => ThreadPool.CompletedWorkItemCount,
unit: "{work_item}",
description: "The number of work items that the thread pool has completed since the process has started.");

s_meter.CreateObservableCounter(
"dotnet.thread_pool.queue.length",
() => ThreadPool.PendingWorkItemCount,
unit: "{work_item}",
description: "The number of work items that are currently queued to be processed by the thread pool.");

s_meter.CreateObservableUpDownCounter(
"dotnet.timer.count",
() => Timer.ActiveCount,
unit: "{timer}",
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.");

s_meter.CreateObservableUpDownCounter(
"dotnet.assembly.count",
() => (long)AppDomain.CurrentDomain.GetAssemblies().Length,
unit: "{assembly}",
description: "The number of .NET assemblies that are currently loaded.");

s_exceptions = s_meter.CreateCounter<long>(
"dotnet.exceptions",
unit: "{exception}",
description: "The number of exceptions that have been thrown in managed code.");

AppDomain.CurrentDomain.FirstChanceException += (source, e) =>
{
// Avoid recursion if the listener itself throws an exception while recording the measurement
Expand All @@ -31,152 +146,21 @@ static RuntimeMetrics()
s_exceptions.Add(1, new KeyValuePair<string, object?>("error.type", e.Exception.GetType().Name));
t_handlingFirstChanceException = false;
};
}

private static readonly ObservableCounter<long> s_gcCollections = s_meter.CreateObservableCounter(
"dotnet.gc.collections",
GetGarbageCollectionCounts,
unit: "{collection}",
description: "The number of garbage collections that have occurred since the process has started.");

private static readonly ObservableUpDownCounter<long> s_processWorkingSet = s_meter.CreateObservableUpDownCounter(
"dotnet.process.memory.working_set",
() => Environment.WorkingSet,
unit: "By",
description: "The number of bytes of physical memory mapped to the process context.");

private static readonly ObservableCounter<long> s_gcHeapTotalAllocated = s_meter.CreateObservableCounter(
"dotnet.gc.heap.total_allocated",
() => GC.GetTotalAllocatedBytes(),
unit: "By",
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.");

private static readonly ObservableUpDownCounter<long> s_gcLastCollectionMemoryCommitted = s_meter.CreateObservableUpDownCounter(
"dotnet.gc.last_collection.memory.committed_size",
() =>
s_meter.CreateObservableUpDownCounter(
"dotnet.process.cpu.count",
() => (long)Environment.ProcessorCount,
unit: "{cpu}",
description: "The number of processors available to the process.");

if (!OperatingSystem.IsBrowser() && !OperatingSystem.IsTvOS() && !(OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()))
{
GCMemoryInfo gcInfo = GC.GetGCMemoryInfo();

return gcInfo.Index == 0
? Array.Empty<Measurement<long>>()
: [new(gcInfo.TotalCommittedBytes)];
},
unit: "By",
description: "The amount of committed virtual memory in use by the .NET GC, as observed during the latest garbage collection.");

private static readonly ObservableUpDownCounter<long> s_gcLastCollectionHeapSize = s_meter.CreateObservableUpDownCounter(
"dotnet.gc.last_collection.heap.size",
GetHeapSizes,
unit: "By",
description: "The managed GC heap size (including fragmentation), as observed during the latest garbage collection.");

private static readonly ObservableUpDownCounter<long> s_gcLastCollectionFragmentationSize = s_meter.CreateObservableUpDownCounter(
"dotnet.gc.last_collection.heap.fragmentation.size",
GetHeapFragmentation,
unit: "By",
description: "The heap fragmentation, as observed during the latest garbage collection.");

private static readonly ObservableCounter<double> s_gcPauseTime = s_meter.CreateObservableCounter(
"dotnet.gc.pause.time",
() => GC.GetTotalPauseDuration().TotalSeconds,
unit: "s",
description: "The total amount of time paused in GC since the process has started.");

private static readonly ObservableCounter<long> s_jitCompiledSize = s_meter.CreateObservableCounter(
"dotnet.jit.compiled_il.size",
() => Runtime.JitInfo.GetCompiledILBytes(),
unit: "By",
description: "Count of bytes of intermediate language that have been compiled since the process has started.");

private static readonly ObservableCounter<long> s_jitCompiledMethodCount = s_meter.CreateObservableCounter(
"dotnet.jit.compiled_methods",
() => Runtime.JitInfo.GetCompiledMethodCount(),
unit: "{method}",
description: "The number of times the JIT compiler (re)compiled methods since the process has started.");

private static readonly ObservableCounter<double> s_jitCompilationTime = s_meter.CreateObservableCounter(
"dotnet.jit.compilation.time",
() => Runtime.JitInfo.GetCompilationTime().TotalSeconds,
unit: "s",
description: "The number of times the JIT compiler (re)compiled methods since the process has started.");

private static readonly ObservableCounter<long> s_monitorLockContention = s_meter.CreateObservableCounter(
"dotnet.monitor.lock_contentions",
() => Monitor.LockContentionCount,
unit: "{contention}",
description: "The number of times there was contention when trying to acquire a monitor lock since the process has started.");

private static readonly ObservableCounter<long> s_threadPoolThreadCount = s_meter.CreateObservableCounter(
"dotnet.thread_pool.thread.count",
() => (long)ThreadPool.ThreadCount,
unit: "{thread}",
description: "The number of thread pool threads that currently exist.");

private static readonly ObservableCounter<long> s_threadPoolCompletedWorkItems = s_meter.CreateObservableCounter(
"dotnet.thread_pool.work_item.count",
() => ThreadPool.CompletedWorkItemCount,
unit: "{work_item}",
description: "The number of work items that the thread pool has completed since the process has started.");

private static readonly ObservableCounter<long> s_threadPoolQueueLength = s_meter.CreateObservableCounter(
"dotnet.thread_pool.queue.length",
() => ThreadPool.PendingWorkItemCount,
unit: "{work_item}",
description: "The number of work items that are currently queued to be processed by the thread pool.");

private static readonly ObservableUpDownCounter<long> s_timerCount = s_meter.CreateObservableUpDownCounter(
"dotnet.timer.count",
() => Timer.ActiveCount,
unit: "{timer}",
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.");

private static readonly ObservableUpDownCounter<long> s_assembliesCount = s_meter.CreateObservableUpDownCounter(
"dotnet.assembly.count",
() => (long)AppDomain.CurrentDomain.GetAssemblies().Length,
unit: "{assembly}",
description: "The number of .NET assemblies that are currently loaded.");

private static readonly Counter<long> s_exceptions = s_meter.CreateCounter<long>(
"dotnet.exceptions",
unit: "{exception}",
description: "The number of exceptions that have been thrown in managed code.");

private static readonly ObservableUpDownCounter<long> s_processCpuCount = s_meter.CreateObservableUpDownCounter(
"dotnet.process.cpu.count",
() => (long)Environment.ProcessorCount,
unit: "{cpu}",
description: "The number of processors available to the process.");

private static readonly ObservableCounter<double>? 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()
{
return s_gcCollections.Enabled
|| s_processWorkingSet.Enabled
|| s_gcHeapTotalAllocated.Enabled
|| s_gcLastCollectionMemoryCommitted.Enabled
|| s_gcLastCollectionHeapSize.Enabled
|| s_gcLastCollectionFragmentationSize.Enabled
|| s_gcPauseTime.Enabled
|| s_jitCompiledSize.Enabled
|| s_jitCompiledMethodCount.Enabled
|| s_jitCompilationTime.Enabled
|| s_monitorLockContention.Enabled
|| s_timerCount.Enabled
|| s_threadPoolThreadCount.Enabled
|| s_threadPoolCompletedWorkItems.Enabled
|| s_threadPoolQueueLength.Enabled
|| s_assembliesCount.Enabled
|| s_exceptions.Enabled
|| s_processCpuCount.Enabled
|| s_processCpuTime?.Enabled is true;
s_meter.CreateObservableCounter(
"dotnet.process.cpu.time",
GetCpuTime,
unit: "s",
description: "CPU time used by the process.");
}
}

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

Environment.ProcessCpuUsage processCpuUsage = Environment.CpuUsage;
Expand Down

0 comments on commit 16f8730

Please sign in to comment.