Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify initialization of RuntimeMetrics #105539

Merged
merged 4 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading