From 1451145e778f133d6299b9d1a16c69b3b56962ea Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 26 Jun 2024 15:44:53 +0100 Subject: [PATCH 01/19] Add runtime metrics --- ...System.Diagnostics.DiagnosticSource.csproj | 1 + .../Diagnostics/Metrics/MeterListener.cs | 5 + .../Diagnostics/Metrics/RuntimeMetrics.cs | 202 ++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/RuntimeMetrics.cs diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj index eafefa6e9fa334..b0179e2d95233e 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj @@ -45,6 +45,7 @@ System.Diagnostics.DiagnosticSource + diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterListener.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterListener.cs index 017074586d583e..8ada8ae3974c44 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterListener.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterListener.cs @@ -33,6 +33,11 @@ public sealed class MeterListener : IDisposable private MeasurementCallback _doubleMeasurementCallback = (instrument, measurement, tags, state) => { /* no-op */ }; private MeasurementCallback _decimalMeasurementCallback = (instrument, measurement, tags, state) => { /* no-op */ }; + static MeterListener() + { + _ = RuntimeMetrics.IsEnabled(); + } + /// /// Creates a MeterListener object. /// 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 new file mode 100644 index 00000000000000..b82b1dc62329ee --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/RuntimeMetrics.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +#if !NETFRAMEWORK && !NETSTANDARD +using System.Threading; +#endif + +namespace System.Diagnostics.Metrics +{ + internal static class RuntimeMetrics + { + private const string MeterName = "System.Runtime"; + + private static readonly Meter s_meter = new(MeterName); + + // These MUST align to the possible attribute values defined in the semantic conventions (TODO: link to the spec) + private static readonly string[] s_genNames = ["gen0", "gen1", "gen2", "loh", "poh"]; +#if !NETFRAMEWORK && !NETSTANDARD + private static readonly int s_maxGenerations = Math.Min(GC.GetGCMemoryInfo().GenerationInfo.Length, s_genNames.Length); +#endif + + static RuntimeMetrics() + { + AppDomain.CurrentDomain.FirstChanceException += (source, e) => + { + s_exceptionCount.Add(1, new KeyValuePair("error.type", e.Exception.GetType().Name)); + }; + } + + // GC Metrics + + private static readonly ObservableCounter s_gcCollectionsCounter = s_meter.CreateObservableCounter( + "dotnet.gc.collections.count", + GetGarbageCollectionCounts, + unit: "{collection}", + description: "Number of garbage collections that have occurred since the process has started."); + + private static readonly ObservableUpDownCounter s_gcObjectsSize = s_meter.CreateObservableUpDownCounter( + "dotnet.gc.objects.size", + () => GC.GetTotalMemory(forceFullCollection: false), + unit: "By", + description: "The number of bytes currently allocated on the managed GC heap. Fragmentation and other GC committed memory pools are excluded."); + +#if !NETFRAMEWORK && !NETSTANDARD + private static readonly ObservableCounter s_gcMemoryTotalAllocated = s_meter.CreateObservableCounter( + "dotnet.gc.memory.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 s_gcMemoryCommited = s_meter.CreateObservableUpDownCounter( + "dotnet.gc.memory.commited", + () => + { + GCMemoryInfo gcInfo = GC.GetGCMemoryInfo(); + + return gcInfo.Index == 0 + ? Array.Empty>() + : [new(GC.GetGCMemoryInfo().TotalCommittedBytes)]; + }, + unit: "By", + description: "The amount of committed virtual memory for the managed GC heap, as observed during the latest garbage collection."); + + private static readonly ObservableUpDownCounter s_gcHeapSize = s_meter.CreateObservableUpDownCounter( + "dotnet.gc.heap.size", + GetHeapSizes, + unit: "By", + description: "The managed GC heap size (including fragmentation), as observed during the latest garbage collection."); + + private static readonly ObservableUpDownCounter s_gcHeapFragmentation = s_meter.CreateObservableUpDownCounter( + "dotnet.gc.heap.fragmentation", + GetHeapFragmentation, + unit: "By", + description: "The heap fragmentation, as observed during the latest garbage collection."); + + private static readonly ObservableCounter 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."); + + // JIT Metrics + + private static readonly ObservableCounter 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 s_jitCompiledMethodCount = s_meter.CreateObservableCounter( + "dotnet.jit.compiled_method.count", + () => 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 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."); + + // Monitor Metrics + + private static readonly ObservableCounter s_monitorLockContention = s_meter.CreateObservableCounter( + "dotnet.monitor.lock_contention.count", + () => Monitor.LockContentionCount, + unit: "s", + description: "The number of times there was contention when trying to acquire a monitor lock since the process has started."); + + // Thread Pool Metrics + + //private static readonly ObservableCounter 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."); + + // TODO + + // Timer Metrics + + private static readonly ObservableUpDownCounter 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."); +#endif + + private static readonly ObservableUpDownCounter s_assemblyCount = s_meter.CreateObservableUpDownCounter( + "dotnet.assemblies.count", + () => (long)AppDomain.CurrentDomain.GetAssemblies().Length, + unit: "{assembly}", + description: "The number of .NET assemblies that are currently loaded."); + + private static readonly Counter s_exceptionCount = s_meter.CreateCounter( + "dotnet.exceptions.count", + unit: "{exception}", + description: "The number of exceptions that have been thrown in managed code."); + + public static bool IsEnabled() + { + return s_gcCollectionsCounter.Enabled + || s_gcObjectsSize.Enabled +#if !NETFRAMEWORK && !NETSTANDARD + || s_gcMemoryTotalAllocated.Enabled + || s_gcMemoryCommited.Enabled + || s_gcHeapSize.Enabled + || s_gcHeapFragmentation.Enabled + || s_gcPauseTime.Enabled + || s_jitCompiledSize.Enabled + || s_jitCompiledMethodCount.Enabled + || s_jitCompilationTime.Enabled + || s_monitorLockContention.Enabled + || s_timerCount.Enabled +#endif + || s_assemblyCount.Enabled + || s_exceptionCount.Enabled; + } + + private static IEnumerable> GetGarbageCollectionCounts() + { + long collectionsFromHigherGeneration = 0; + + for (int gen = 2; gen >= 0; --gen) + { + long collectionsFromThisGeneration = GC.CollectionCount(gen); + yield return new(collectionsFromThisGeneration - collectionsFromHigherGeneration, new KeyValuePair("generation", s_genNames[gen])); + collectionsFromHigherGeneration = collectionsFromThisGeneration; + } + } + +#if !NETFRAMEWORK && !NETSTANDARD + private static IEnumerable> GetHeapSizes() + { + GCMemoryInfo gcInfo = GC.GetGCMemoryInfo(); + + if (gcInfo.Index == 0) + yield break; + + for (int i = 0; i < s_maxGenerations; ++i) + { + yield return new(gcInfo.GenerationInfo[i].SizeAfterBytes, new KeyValuePair("generation", s_genNames[i])); + } + } + + private static IEnumerable> GetHeapFragmentation() + { + GCMemoryInfo gcInfo = GC.GetGCMemoryInfo(); + + if (gcInfo.Index == 0) + yield break; + + for (int i = 0; i < s_maxGenerations; ++i) + { + yield return new(gcInfo.GenerationInfo[i].FragmentationAfterBytes, new KeyValuePair("generation", s_genNames[i])); + } + } +#endif + } +} From 6812fe5883fb7fc1ec5adbbaaf69768d64cba021 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 1 Jul 2024 16:34:35 +0100 Subject: [PATCH 02/19] Add additional metrics --- ...System.Diagnostics.DiagnosticSource.csproj | 3 + .../Diagnostics/Metrics/RuntimeMetrics.cs | 64 +++++++++++++++---- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj index b0179e2d95233e..53fa73acac23c8 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj @@ -130,12 +130,15 @@ System.Diagnostics.DiagnosticSource + + + 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 b82b1dc62329ee..847062843be915 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 @@ -111,13 +111,23 @@ static RuntimeMetrics() // Thread Pool Metrics - //private static readonly ObservableCounter 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."); - - // TODO + private static readonly ObservableCounter 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 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 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."); // Timer Metrics @@ -139,6 +149,20 @@ static RuntimeMetrics() unit: "{exception}", description: "The number of exceptions that have been thrown in managed code."); + // CPU Metrics + + private static readonly ObservableUpDownCounter s_cpuCount = s_meter.CreateObservableUpDownCounter( + "dotnet.cpu.count", + () => (long)Environment.ProcessorCount, + unit: "{cpu}", + description: "The number of processors available to the process."); + + private static readonly ObservableCounter s_cpuTime = s_meter.CreateObservableCounter( + "dotnet.cpu.time", + GetCpuTime, + unit: "s", + description: "The number of work items that are currently queued to be processed by the thread pool."); + public static bool IsEnabled() { return s_gcCollectionsCounter.Enabled @@ -154,9 +178,14 @@ public static bool IsEnabled() || s_jitCompilationTime.Enabled || s_monitorLockContention.Enabled || s_timerCount.Enabled + || s_threadPoolThreadCount.Enabled + || s_threadPoolCompletedWorkItems.Enabled + || s_threadPoolQueueLength.Enabled #endif || s_assemblyCount.Enabled - || s_exceptionCount.Enabled; + || s_exceptionCount.Enabled + || s_cpuCount.Enabled + || s_cpuTime.Enabled; } private static IEnumerable> GetGarbageCollectionCounts() @@ -166,11 +195,24 @@ private static IEnumerable> GetGarbageCollectionCounts() for (int gen = 2; gen >= 0; --gen) { long collectionsFromThisGeneration = GC.CollectionCount(gen); - yield return new(collectionsFromThisGeneration - collectionsFromHigherGeneration, new KeyValuePair("generation", s_genNames[gen])); + yield return new(collectionsFromThisGeneration - collectionsFromHigherGeneration, new KeyValuePair("gc.heap.generation", s_genNames[gen])); collectionsFromHigherGeneration = collectionsFromThisGeneration; } } + private static IEnumerable> GetCpuTime() + { +#if !NETFRAMEWORK && !NETSTANDARD + if (OperatingSystem.IsBrowser() || OperatingSystem.IsTvOS() || OperatingSystem.IsIOS()) + yield break; +#endif + + Process process = Process.GetCurrentProcess(); + + yield return new(process.UserProcessorTime.TotalSeconds, [new KeyValuePair("cpu.mode", "user")]); + yield return new(process.PrivilegedProcessorTime.TotalSeconds, [new KeyValuePair("cpu.mode", "system")]); + } + #if !NETFRAMEWORK && !NETSTANDARD private static IEnumerable> GetHeapSizes() { @@ -181,7 +223,7 @@ private static IEnumerable> GetHeapSizes() for (int i = 0; i < s_maxGenerations; ++i) { - yield return new(gcInfo.GenerationInfo[i].SizeAfterBytes, new KeyValuePair("generation", s_genNames[i])); + yield return new(gcInfo.GenerationInfo[i].SizeAfterBytes, new KeyValuePair("gc.heap.generation", s_genNames[i])); } } @@ -194,7 +236,7 @@ private static IEnumerable> GetHeapFragmentation() for (int i = 0; i < s_maxGenerations; ++i) { - yield return new(gcInfo.GenerationInfo[i].FragmentationAfterBytes, new KeyValuePair("generation", s_genNames[i])); + yield return new(gcInfo.GenerationInfo[i].FragmentationAfterBytes, new KeyValuePair("gc.heap.generation", s_genNames[i])); } } #endif From 1ca6ff91ae983e5ec50d7f82a8029403361a8ed9 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 3 Jul 2024 14:05:19 +0100 Subject: [PATCH 03/19] Add an initial test --- .../tests/RuntimeMetricsTests.cs | 102 ++++++++++++++++++ ....Diagnostics.DiagnosticSource.Tests.csproj | 1 + 2 files changed, 103 insertions(+) create mode 100644 src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs new file mode 100644 index 00000000000000..9974561a8851b3 --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.Diagnostics.Metrics.Tests +{ + public class RuntimeMetricsTests + { + [Fact] + public async Task GcCollectionsCount() + { + using InstrumentRecorder instrumentRecorder = new("dotnet.gc.collections.count"); + using CancellationTokenSource cts = new(1000); + + for (var gen = 0; gen <= GC.MaxGeneration; gen++) + { + GC.Collect(gen, GCCollectionMode.Forced); + } + + instrumentRecorder.RecordObservableInstruments(); + + (bool success, IReadOnlyList> measurements) = await WaitForMeasurements(instrumentRecorder, 3, cts.Token); + + Assert.True(success, "Expected to receive at least 3 measurements."); + + int count = 0; + for (int i = GC.MaxGeneration; i >= 0; i--) + { + Measurement measurement = measurements[count++]; + Assert.True(measurement.Value > 0, $"Gen {i} count should be greater than zero."); + + var tags = measurement.Tags.ToArray(); + + Assert.Equal(1, tags.Length); + VerifyTag(tags, "gc.heap.generation", $"gen{i}"); + } + } + + private static async Task<(bool, IReadOnlyList>)> WaitForMeasurements(InstrumentRecorder instrumentRecorder, + int expected, CancellationToken cancellationToken) where T : struct + { + IReadOnlyList> measurements; + while ((measurements = instrumentRecorder.GetMeasurements()).Count < 3) + { + if (cancellationToken.IsCancellationRequested) + { + return (false, measurements); + } + + await Task.Delay(50, cancellationToken); + } + + return (true, measurements); + } + + private static void VerifyTag(KeyValuePair[] tags, string name, T value) + { + if (value is null) + { + Assert.DoesNotContain(tags, t => t.Key == name); + } + else + { + Assert.Equal(value, (T)tags.Single(t => t.Key == name).Value); + } + } + + protected sealed class InstrumentRecorder : IDisposable where T : struct + { + private readonly MeterListener _meterListener = new(); + private readonly ConcurrentQueue> _values = new(); + + public InstrumentRecorder(string instrumentName) + { + _meterListener.InstrumentPublished = (instrument, listener) => + { + if (instrument.Meter.Name == "System.Runtime" && instrument.Name == instrumentName) + { + listener.EnableMeasurementEvents(instrument); + } + }; + _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded); + _meterListener.Start(); + } + + private void OnMeasurementRecorded(Instrument instrument, T measurement, ReadOnlySpan> tags, object? state) => + _values.Enqueue(new Measurement(measurement, tags)); + + public IReadOnlyList> GetMeasurements() => _values.ToArray(); + + public void RecordObservableInstruments() => _meterListener.RecordObservableInstruments(); + + public void Dispose() => _meterListener.Dispose(); + } + } +} diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj b/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj index 446c67800c4a07..6ba91d498fc957 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj @@ -35,6 +35,7 @@ + From 6a61279f201a6a514a133db8178f5b1ed6379013 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 3 Jul 2024 14:11:52 +0100 Subject: [PATCH 04/19] Use GC.MaxGeneration --- .../src/System/Diagnostics/Metrics/RuntimeMetrics.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 847062843be915..14334b6811a0b9 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 @@ -58,7 +58,7 @@ static RuntimeMetrics() return gcInfo.Index == 0 ? Array.Empty>() - : [new(GC.GetGCMemoryInfo().TotalCommittedBytes)]; + : [new(gcInfo.TotalCommittedBytes)]; }, unit: "By", description: "The amount of committed virtual memory for the managed GC heap, as observed during the latest garbage collection."); @@ -192,7 +192,7 @@ private static IEnumerable> GetGarbageCollectionCounts() { long collectionsFromHigherGeneration = 0; - for (int gen = 2; gen >= 0; --gen) + for (int gen = GC.MaxGeneration; gen >= 0; --gen) { long collectionsFromThisGeneration = GC.CollectionCount(gen); yield return new(collectionsFromThisGeneration - collectionsFromHigherGeneration, new KeyValuePair("gc.heap.generation", s_genNames[gen])); From 98f6b05004a048df67f902d66415a7eed7da52e3 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 3 Jul 2024 14:35:40 +0100 Subject: [PATCH 05/19] Make test less brittle --- .../tests/RuntimeMetricsTests.cs | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs index 9974561a8851b3..ff1018724a2b76 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs @@ -18,6 +18,9 @@ public async Task GcCollectionsCount() using InstrumentRecorder instrumentRecorder = new("dotnet.gc.collections.count"); using CancellationTokenSource cts = new(1000); + var token = cts.Token; + token.Register(() => Assert.Fail("Timed out waiting for measurements.")); + for (var gen = 0; gen <= GC.MaxGeneration; gen++) { GC.Collect(gen, GCCollectionMode.Forced); @@ -25,20 +28,49 @@ public async Task GcCollectionsCount() instrumentRecorder.RecordObservableInstruments(); - (bool success, IReadOnlyList> measurements) = await WaitForMeasurements(instrumentRecorder, 3, cts.Token); + (bool success, IReadOnlyList> measurements) = await WaitForMeasurements(instrumentRecorder, GC.MaxGeneration + 1, token); - Assert.True(success, "Expected to receive at least 3 measurements."); + Assert.True(success, "Expected to receive at least 1 measurement per generation."); - int count = 0; - for (int i = GC.MaxGeneration; i >= 0; i--) + bool[] foundGenerations = new bool[GC.MaxGeneration + 1]; + for (int i = 0; i < GC.MaxGeneration + 1; i++) { - Measurement measurement = measurements[count++]; - Assert.True(measurement.Value > 0, $"Gen {i} count should be greater than zero."); + foundGenerations[i] = false; + } + foreach (Measurement measurement in measurements.Where(m => m.Value >= 1)) + { var tags = measurement.Tags.ToArray(); - Assert.Equal(1, tags.Length); - VerifyTag(tags, "gc.heap.generation", $"gen{i}"); + var tag = tags.SingleOrDefault(k => k.Key == "gc.heap.generation"); + + if (tag.Key is not null) + { + Assert.True(tag.Value is string, "Expected generation tag to be a string."); + + string tagValue = (string)tag.Value; + + switch (tagValue) + { + case "gen0": + foundGenerations[0] = true; + break; + case "gen1": + foundGenerations[1] = true; + break; + case "gen2": + foundGenerations[2] = true; + break; + default: + Assert.Fail("Unexpected generation tag value."); + break; + } + } + } + + foreach (var found in foundGenerations) + { + Assert.True(found, "Expected to find a measurement for each generation (0, 1 and 2)."); } } From 101e8aac911359f2a695f40af3a74f9c9bbab5b1 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 10 Jul 2024 16:04:30 +0100 Subject: [PATCH 06/19] PR feedback and additional tests --- .../Diagnostics/Metrics/MeterListener.cs | 1 + .../Diagnostics/Metrics/RuntimeMetrics.cs | 26 +- .../tests/RuntimeMetricsTests.cs | 236 +++++++++++++++--- 3 files changed, 220 insertions(+), 43 deletions(-) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterListener.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterListener.cs index 8ada8ae3974c44..52de821c9f4e58 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterListener.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterListener.cs @@ -35,6 +35,7 @@ public sealed class MeterListener : IDisposable static MeterListener() { + // This ensures that the static Meter gets created before any listeners exist. _ = RuntimeMetrics.IsEnabled(); } 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 14334b6811a0b9..5be46508c23e90 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,8 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Runtime.CompilerServices; -#if !NETFRAMEWORK && !NETSTANDARD +#if NET using System.Threading; #endif @@ -11,13 +10,15 @@ namespace System.Diagnostics.Metrics { internal static class RuntimeMetrics { + [ThreadStatic] private static bool t_handlingFirstChanceException; + private const string MeterName = "System.Runtime"; private static readonly Meter s_meter = new(MeterName); // These MUST align to the possible attribute values defined in the semantic conventions (TODO: link to the spec) private static readonly string[] s_genNames = ["gen0", "gen1", "gen2", "loh", "poh"]; -#if !NETFRAMEWORK && !NETSTANDARD +#if NET private static readonly int s_maxGenerations = Math.Min(GC.GetGCMemoryInfo().GenerationInfo.Length, s_genNames.Length); #endif @@ -25,7 +26,10 @@ static RuntimeMetrics() { AppDomain.CurrentDomain.FirstChanceException += (source, e) => { + if (t_handlingFirstChanceException) return; + t_handlingFirstChanceException = true; s_exceptionCount.Add(1, new KeyValuePair("error.type", e.Exception.GetType().Name)); + t_handlingFirstChanceException = false; }; } @@ -43,7 +47,7 @@ static RuntimeMetrics() unit: "By", description: "The number of bytes currently allocated on the managed GC heap. Fragmentation and other GC committed memory pools are excluded."); -#if !NETFRAMEWORK && !NETSTANDARD +#if NET private static readonly ObservableCounter s_gcMemoryTotalAllocated = s_meter.CreateObservableCounter( "dotnet.gc.memory.total_allocated", () => GC.GetTotalAllocatedBytes(), @@ -51,7 +55,7 @@ static RuntimeMetrics() 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 s_gcMemoryCommited = s_meter.CreateObservableUpDownCounter( - "dotnet.gc.memory.commited", + "dotnet.gc.memory.committed", () => { GCMemoryInfo gcInfo = GC.GetGCMemoryInfo(); @@ -61,7 +65,7 @@ static RuntimeMetrics() : [new(gcInfo.TotalCommittedBytes)]; }, unit: "By", - description: "The amount of committed virtual memory for the managed GC heap, as observed during the latest garbage collection."); + description: "The amount of committed virtual memory in use by the .NET GC, as observed during the latest garbage collection."); private static readonly ObservableUpDownCounter s_gcHeapSize = s_meter.CreateObservableUpDownCounter( "dotnet.gc.heap.size", @@ -106,7 +110,7 @@ static RuntimeMetrics() private static readonly ObservableCounter s_monitorLockContention = s_meter.CreateObservableCounter( "dotnet.monitor.lock_contention.count", () => Monitor.LockContentionCount, - unit: "s", + unit: "{contention}", description: "The number of times there was contention when trying to acquire a monitor lock since the process has started."); // Thread Pool Metrics @@ -161,13 +165,13 @@ static RuntimeMetrics() "dotnet.cpu.time", GetCpuTime, unit: "s", - description: "The number of work items that are currently queued to be processed by the thread pool."); + description: "CPU time used by the process as reported by the CLR."); public static bool IsEnabled() { return s_gcCollectionsCounter.Enabled || s_gcObjectsSize.Enabled -#if !NETFRAMEWORK && !NETSTANDARD +#if NET || s_gcMemoryTotalAllocated.Enabled || s_gcMemoryCommited.Enabled || s_gcHeapSize.Enabled @@ -202,7 +206,7 @@ private static IEnumerable> GetGarbageCollectionCounts() private static IEnumerable> GetCpuTime() { -#if !NETFRAMEWORK && !NETSTANDARD +#if NET if (OperatingSystem.IsBrowser() || OperatingSystem.IsTvOS() || OperatingSystem.IsIOS()) yield break; #endif @@ -213,7 +217,7 @@ private static IEnumerable> GetCpuTime() yield return new(process.PrivilegedProcessorTime.TotalSeconds, [new KeyValuePair("cpu.mode", "system")]); } -#if !NETFRAMEWORK && !NETSTANDARD +#if NET private static IEnumerable> GetHeapSizes() { GCMemoryInfo gcInfo = GC.GetGCMemoryInfo(); diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs index ff1018724a2b76..f4a65148256cc5 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs @@ -5,21 +5,26 @@ using System.Collections.Generic; using System.Linq; using System.Threading; -using System.Threading.Tasks; using Xunit; namespace System.Diagnostics.Metrics.Tests { public class RuntimeMetricsTests { + private const string GreaterThanZeroMessage = "Expected value to be greater than zero."; + private const string GreaterThanOrEqualToZeroMessage = "Expected value to be greater than or equal to zero."; + + private static readonly string[] s_genNames = ["gen0", "gen1", "gen2", "loh", "poh"]; + + private static readonly Action s_forceGc = () => GC.Collect(0, GCCollectionMode.Forced); + private static readonly Func s_longGreaterThanZero = v => v > 0 ? (true, null) : (false, GreaterThanZeroMessage); + private static readonly Func s_longGreaterThanOrEqualToZero = v => v >= 0 ? (true, null) : (false, GreaterThanOrEqualToZeroMessage); + private static readonly Func s_doubleGreaterThanZero = v => v > 0 ? (true, null) : (false, GreaterThanZeroMessage); + [Fact] - public async Task GcCollectionsCount() + public void GcCollectionsCount() { using InstrumentRecorder instrumentRecorder = new("dotnet.gc.collections.count"); - using CancellationTokenSource cts = new(1000); - - var token = cts.Token; - token.Register(() => Assert.Fail("Timed out waiting for measurements.")); for (var gen = 0; gen <= GC.MaxGeneration; gen++) { @@ -28,20 +33,15 @@ public async Task GcCollectionsCount() instrumentRecorder.RecordObservableInstruments(); - (bool success, IReadOnlyList> measurements) = await WaitForMeasurements(instrumentRecorder, GC.MaxGeneration + 1, token); - - Assert.True(success, "Expected to receive at least 1 measurement per generation."); - bool[] foundGenerations = new bool[GC.MaxGeneration + 1]; for (int i = 0; i < GC.MaxGeneration + 1; i++) { foundGenerations[i] = false; } - foreach (Measurement measurement in measurements.Where(m => m.Value >= 1)) + foreach (Measurement measurement in instrumentRecorder.GetMeasurements().Where(m => m.Value >= 1)) { var tags = measurement.Tags.ToArray(); - var tag = tags.SingleOrDefault(k => k.Key == "gc.heap.generation"); if (tag.Key is not null) @@ -62,53 +62,204 @@ public async Task GcCollectionsCount() foundGenerations[2] = true; break; default: - Assert.Fail("Unexpected generation tag value."); + Assert.Fail($"Unexpected generation tag value '{tagValue}'."); break; } } } - foreach (var found in foundGenerations) + for (int i = 0; i < foundGenerations.Length; i++) { - Assert.True(found, "Expected to find a measurement for each generation (0, 1 and 2)."); + var generation = i switch + { + 0 => "gen0", + 1 => "gen1", + 2 => "gen2", + _ => throw new InvalidOperationException("Unexpected generation.") + }; + + Assert.True(foundGenerations[i], $"Expected to find a measurement for '{generation}'."); } } - private static async Task<(bool, IReadOnlyList>)> WaitForMeasurements(InstrumentRecorder instrumentRecorder, - int expected, CancellationToken cancellationToken) where T : struct + [Fact] + public void CpuTime() { - IReadOnlyList> measurements; - while ((measurements = instrumentRecorder.GetMeasurements()).Count < 3) + using InstrumentRecorder instrumentRecorder = new("dotnet.cpu.time"); + + instrumentRecorder.RecordObservableInstruments(); + + bool[] foundCpuModes = [false, false]; + + foreach (Measurement measurement in instrumentRecorder.GetMeasurements().Where(m => m.Value >= 0)) { - if (cancellationToken.IsCancellationRequested) + var tags = measurement.Tags.ToArray(); + var tag = tags.SingleOrDefault(k => k.Key == "cpu.mode"); + + if (tag.Key is not null) { - return (false, measurements); + 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; + } } + } - await Task.Delay(50, cancellationToken); + 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."); } + } - return (true, measurements); + [Fact] + public void ExceptionsCount() + { + // We inject an exception into the MeterListener callback here, so we can test that we don't recursively record exceptions. + using InstrumentRecorder instrumentRecorder = new("dotnet.exceptions.count", injectException: true); + + try + { + throw new Exception(); + } + catch + { + // Ignore the exception. + } + + var measurements = instrumentRecorder.GetMeasurements(); + + Assert.Single(measurements); + + try + { + throw new Exception(); + } + catch + { + // Ignore the exception. + } + + measurements = instrumentRecorder.GetMeasurements(); + + Assert.Equal(2, measurements.Count); + } + + [Theory] + [MemberData(nameof(LongMeasurements))] + public void ValidateMeasurements(string metricName, Func? valueAssertion, Action? beforeRecord) + where T : struct + { + ValidateSingleMeasurement(metricName, valueAssertion, beforeRecord); } - private static void VerifyTag(KeyValuePair[] tags, string name, T value) + public static IEnumerable LongMeasurements => new List { - if (value is null) + new object[] { "dotnet.gc.objects.size", s_longGreaterThanZero, null }, + new object[] { "dotnet.assemblies.count", s_longGreaterThanZero, null }, + new object[] { "dotnet.cpu.count", s_longGreaterThanZero, null }, +#if NET + new object[] { "dotnet.gc.memory.total_allocated", s_longGreaterThanZero, null }, + new object[] { "dotnet.gc.memory.committed", s_longGreaterThanZero, s_forceGc }, + new object[] { "dotnet.gc.pause.time", s_doubleGreaterThanZero, s_forceGc }, + new object[] { "dotnet.jit.compiled_il.size", s_longGreaterThanZero, null }, + new object[] { "dotnet.jit.compiled_method.count", s_longGreaterThanZero, null }, + new object[] { "dotnet.jit.compilation.time", s_doubleGreaterThanZero, null }, + new object[] { "dotnet.monitor.lock_contention.count", s_longGreaterThanOrEqualToZero, null }, + new object[] { "dotnet.thread_pool.thread.count", s_longGreaterThanZero, null }, + new object[] { "dotnet.thread_pool.work_item.count", s_longGreaterThanOrEqualToZero, null }, + new object[] { "dotnet.thread_pool.queue.length", s_longGreaterThanOrEqualToZero, null }, + new object[] { "dotnet.timer.count", s_longGreaterThanOrEqualToZero, null }, +#endif + }; + +#if NET + [Fact] + public void HeapSize() => EnsureAllHeapTags("dotnet.gc.heap.size"); + + [Fact] + public void FragmentationSize() => EnsureAllHeapTags("dotnet.gc.heap.fragmentation"); + + private void EnsureAllHeapTags(string metricName) + { + using InstrumentRecorder instrumentRecorder = new(metricName); + + for (var gen = 0; gen <= GC.MaxGeneration; gen++) + { + GC.Collect(gen, GCCollectionMode.Forced); + } + + instrumentRecorder.RecordObservableInstruments(); + + bool[] foundGenerations = new bool[s_genNames.Length]; + for (int i = 0; i < 5; i++) + { + foundGenerations[i] = false; + } + + foreach (Measurement measurement in instrumentRecorder.GetMeasurements()) + { + var tags = measurement.Tags.ToArray(); + var tag = tags.SingleOrDefault(k => k.Key == "gc.heap.generation"); + + if (tag.Key is not null) + { + Assert.True(tag.Value is string, "Expected generation tag to be a string."); + + string tagValue = (string)tag.Value; + + var index = Array.FindIndex(s_genNames, x => x == tagValue); + + if (index == -1) + Assert.Fail($"Unexpected generation tag value '{tagValue}'."); + + foundGenerations[index] = true; + } + } + + for (int i = 0; i < foundGenerations.Length; i++) { - Assert.DoesNotContain(tags, t => t.Key == name); + Assert.True(foundGenerations[i], $"Expected to find a measurement for '{s_genNames[i]}'."); } - else + } +#endif + + private static void ValidateSingleMeasurement(string metricName, Func? valueAssertion = null, Action? beforeRecord = null) + where T : struct + { + using InstrumentRecorder instrumentRecorder = new(metricName); + + beforeRecord?.Invoke(); + instrumentRecorder.RecordObservableInstruments(); + var measurements = instrumentRecorder.GetMeasurements(); + Assert.Single(measurements); + + if (valueAssertion is not null) { - Assert.Equal(value, (T)tags.Single(t => t.Key == name).Value); + var (isExpected, message) = valueAssertion(measurements[0].Value); + Assert.True(isExpected, message); } } - protected sealed class InstrumentRecorder : IDisposable where T : struct + private sealed class InstrumentRecorder : IDisposable where T : struct { private readonly MeterListener _meterListener = new(); private readonly ConcurrentQueue> _values = new(); + private readonly bool _injectException; - public InstrumentRecorder(string instrumentName) + public InstrumentRecorder(string instrumentName, bool injectException = false) { _meterListener.InstrumentPublished = (instrument, listener) => { @@ -119,12 +270,33 @@ public InstrumentRecorder(string instrumentName) }; _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded); _meterListener.Start(); + _injectException = injectException; } - private void OnMeasurementRecorded(Instrument instrument, T measurement, ReadOnlySpan> tags, object? state) => + private void OnMeasurementRecorded(Instrument instrument, T measurement, ReadOnlySpan> tags, object? state) + { + if (_injectException) + { + try + { + throw new Exception(); + } + catch + { + // Ignore the exception. + } + } + _values.Enqueue(new Measurement(measurement, tags)); + } - public IReadOnlyList> GetMeasurements() => _values.ToArray(); + public IReadOnlyList> GetMeasurements() + { + // Wait enough time for all the measurements to be enqueued via the + // OnMeasurementRecorded callback. 50ms seems to be sufficient. + Thread.Sleep(50); + return _values.ToArray(); + } public void RecordObservableInstruments() => _meterListener.RecordObservableInstruments(); From 34685d93a0739990fd4cba520f00d4d7aea3bef0 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Thu, 11 Jul 2024 07:25:34 +0100 Subject: [PATCH 07/19] Add code comment --- .../src/System/Diagnostics/Metrics/RuntimeMetrics.cs | 2 ++ 1 file changed, 2 insertions(+) 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 5be46508c23e90..5acb1e0528fe90 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 @@ -26,6 +26,8 @@ static RuntimeMetrics() { AppDomain.CurrentDomain.FirstChanceException += (source, e) => { + // Avoid recursion if the listener itself throws an exception while recording the measurement + // in its `OnMeasurementRecorded` callback. if (t_handlingFirstChanceException) return; t_handlingFirstChanceException = true; s_exceptionCount.Add(1, new KeyValuePair("error.type", e.Exception.GetType().Name)); From 9cd630364c8b1ddcacc2189a0b95659c2218167d Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Thu, 11 Jul 2024 08:04:22 +0100 Subject: [PATCH 08/19] Update runtime metrics to align with latest semcon changes --- .../Diagnostics/Metrics/RuntimeMetrics.cs | 34 ++++++------------- .../tests/RuntimeMetricsTests.cs | 21 ++++++------ 2 files changed, 21 insertions(+), 34 deletions(-) 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 5acb1e0528fe90..ad92785b610273 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 @@ -35,8 +35,6 @@ static RuntimeMetrics() }; } - // GC Metrics - private static readonly ObservableCounter s_gcCollectionsCounter = s_meter.CreateObservableCounter( "dotnet.gc.collections.count", GetGarbageCollectionCounts, @@ -44,20 +42,20 @@ static RuntimeMetrics() description: "Number of garbage collections that have occurred since the process has started."); private static readonly ObservableUpDownCounter s_gcObjectsSize = s_meter.CreateObservableUpDownCounter( - "dotnet.gc.objects.size", - () => GC.GetTotalMemory(forceFullCollection: false), + "dotnet.process.memory.working_set", + () => Environment.WorkingSet, unit: "By", - description: "The number of bytes currently allocated on the managed GC heap. Fragmentation and other GC committed memory pools are excluded."); + description: "The number of bytes of physical memory mapped to the process context."); #if NET private static readonly ObservableCounter s_gcMemoryTotalAllocated = s_meter.CreateObservableCounter( - "dotnet.gc.memory.total_allocated", + "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 s_gcMemoryCommited = s_meter.CreateObservableUpDownCounter( - "dotnet.gc.memory.committed", + "dotnet.gc.last_collection.memory.committed_size", () => { GCMemoryInfo gcInfo = GC.GetGCMemoryInfo(); @@ -70,13 +68,13 @@ static RuntimeMetrics() description: "The amount of committed virtual memory in use by the .NET GC, as observed during the latest garbage collection."); private static readonly ObservableUpDownCounter s_gcHeapSize = s_meter.CreateObservableUpDownCounter( - "dotnet.gc.heap.size", + "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 s_gcHeapFragmentation = s_meter.CreateObservableUpDownCounter( - "dotnet.gc.heap.fragmentation", + "dotnet.gc.last_collection.heap.fragmentation.size", GetHeapFragmentation, unit: "By", description: "The heap fragmentation, as observed during the latest garbage collection."); @@ -87,8 +85,6 @@ static RuntimeMetrics() unit: "s", description: "The total amount of time paused in GC since the process has started."); - // JIT Metrics - private static readonly ObservableCounter s_jitCompiledSize = s_meter.CreateObservableCounter( "dotnet.jit.compiled_il.size", () => Runtime.JitInfo.GetCompiledILBytes(), @@ -107,16 +103,12 @@ static RuntimeMetrics() unit: "s", description: "The number of times the JIT compiler (re)compiled methods since the process has started."); - // Monitor Metrics - private static readonly ObservableCounter s_monitorLockContention = s_meter.CreateObservableCounter( "dotnet.monitor.lock_contention.count", () => Monitor.LockContentionCount, unit: "{contention}", description: "The number of times there was contention when trying to acquire a monitor lock since the process has started."); - // Thread Pool Metrics - private static readonly ObservableCounter s_threadPoolThreadCount = s_meter.CreateObservableCounter( "dotnet.thread_pool.thread.count", () => (long)ThreadPool.ThreadCount, @@ -135,8 +127,6 @@ static RuntimeMetrics() unit: "{work_item}", description: "The number of work items that are currently queued to be processed by the thread pool."); - // Timer Metrics - private static readonly ObservableUpDownCounter s_timerCount = s_meter.CreateObservableUpDownCounter( "dotnet.timer.count", () => Timer.ActiveCount, @@ -151,20 +141,18 @@ static RuntimeMetrics() description: "The number of .NET assemblies that are currently loaded."); private static readonly Counter s_exceptionCount = s_meter.CreateCounter( - "dotnet.exceptions.count", + "dotnet.exceptions", unit: "{exception}", description: "The number of exceptions that have been thrown in managed code."); - // CPU Metrics - private static readonly ObservableUpDownCounter s_cpuCount = s_meter.CreateObservableUpDownCounter( - "dotnet.cpu.count", + "dotnet.process.cpu.count", () => (long)Environment.ProcessorCount, unit: "{cpu}", description: "The number of processors available to the process."); - private static readonly ObservableCounter s_cpuTime = s_meter.CreateObservableCounter( - "dotnet.cpu.time", + private static readonly ObservableCounter s_cpuTime = s_meter.CreateObservableCounter( + "dotnet.process.cpu.time", GetCpuTime, unit: "s", description: "CPU time used by the process as reported by the CLR."); diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs index f4a65148256cc5..3d0a25fd64f17f 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs @@ -85,7 +85,7 @@ public void GcCollectionsCount() [Fact] public void CpuTime() { - using InstrumentRecorder instrumentRecorder = new("dotnet.cpu.time"); + using InstrumentRecorder instrumentRecorder = new("dotnet.process.cpu.time"); instrumentRecorder.RecordObservableInstruments(); @@ -128,7 +128,7 @@ public void CpuTime() public void ExceptionsCount() { // We inject an exception into the MeterListener callback here, so we can test that we don't recursively record exceptions. - using InstrumentRecorder instrumentRecorder = new("dotnet.exceptions.count", injectException: true); + using InstrumentRecorder instrumentRecorder = new("dotnet.exceptions", injectException: true); try { @@ -167,12 +167,12 @@ public void ValidateMeasurements(string metricName, Func? public static IEnumerable LongMeasurements => new List { - new object[] { "dotnet.gc.objects.size", s_longGreaterThanZero, null }, + new object[] { "dotnet.process.memory.working_set", s_longGreaterThanZero, null }, new object[] { "dotnet.assemblies.count", s_longGreaterThanZero, null }, - new object[] { "dotnet.cpu.count", s_longGreaterThanZero, null }, + new object[] { "dotnet.process.cpu.count", s_longGreaterThanZero, null }, #if NET - new object[] { "dotnet.gc.memory.total_allocated", s_longGreaterThanZero, null }, - new object[] { "dotnet.gc.memory.committed", s_longGreaterThanZero, s_forceGc }, + new object[] { "dotnet.gc.heap.total_allocated", s_longGreaterThanZero, null }, + new object[] { "dotnet.gc.last_collection.memory.committed_size", s_longGreaterThanZero, s_forceGc }, new object[] { "dotnet.gc.pause.time", s_doubleGreaterThanZero, s_forceGc }, new object[] { "dotnet.jit.compiled_il.size", s_longGreaterThanZero, null }, new object[] { "dotnet.jit.compiled_method.count", s_longGreaterThanZero, null }, @@ -186,11 +186,10 @@ public void ValidateMeasurements(string metricName, Func? }; #if NET - [Fact] - public void HeapSize() => EnsureAllHeapTags("dotnet.gc.heap.size"); - - [Fact] - public void FragmentationSize() => EnsureAllHeapTags("dotnet.gc.heap.fragmentation"); + [Theory] + [InlineData("dotnet.gc.last_collection.heap.size")] + [InlineData("dotnet.gc.last_collection.heap.fragmentation.size")] + public void HeapTags(string metricName) => EnsureAllHeapTags(metricName); private void EnsureAllHeapTags(string metricName) { From 72940375a0b110e1ab5c298a4538508b789d2e97 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Thu, 11 Jul 2024 08:27:38 +0100 Subject: [PATCH 09/19] Fix/update field names --- .../Diagnostics/Metrics/RuntimeMetrics.cs | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) 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 ad92785b610273..5edd6271a0d2dc 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 @@ -30,31 +30,31 @@ static RuntimeMetrics() // in its `OnMeasurementRecorded` callback. if (t_handlingFirstChanceException) return; t_handlingFirstChanceException = true; - s_exceptionCount.Add(1, new KeyValuePair("error.type", e.Exception.GetType().Name)); + s_exceptions.Add(1, new KeyValuePair("error.type", e.Exception.GetType().Name)); t_handlingFirstChanceException = false; }; } - private static readonly ObservableCounter s_gcCollectionsCounter = s_meter.CreateObservableCounter( + private static readonly ObservableCounter s_gcCollections = s_meter.CreateObservableCounter( "dotnet.gc.collections.count", GetGarbageCollectionCounts, unit: "{collection}", description: "Number of garbage collections that have occurred since the process has started."); - private static readonly ObservableUpDownCounter s_gcObjectsSize = s_meter.CreateObservableUpDownCounter( + private static readonly ObservableUpDownCounter 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."); #if NET - private static readonly ObservableCounter s_gcMemoryTotalAllocated = s_meter.CreateObservableCounter( + private static readonly ObservableCounter 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 s_gcMemoryCommited = s_meter.CreateObservableUpDownCounter( + private static readonly ObservableUpDownCounter s_gcLastCollectionMemoryCommitted = s_meter.CreateObservableUpDownCounter( "dotnet.gc.last_collection.memory.committed_size", () => { @@ -67,13 +67,13 @@ static RuntimeMetrics() 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 s_gcHeapSize = s_meter.CreateObservableUpDownCounter( + private static readonly ObservableUpDownCounter 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 s_gcHeapFragmentation = s_meter.CreateObservableUpDownCounter( + private static readonly ObservableUpDownCounter s_gcLastCollectionFragmentationSize = s_meter.CreateObservableUpDownCounter( "dotnet.gc.last_collection.heap.fragmentation.size", GetHeapFragmentation, unit: "By", @@ -134,24 +134,24 @@ static RuntimeMetrics() 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."); #endif - private static readonly ObservableUpDownCounter s_assemblyCount = s_meter.CreateObservableUpDownCounter( + private static readonly ObservableUpDownCounter s_assembliesCount = s_meter.CreateObservableUpDownCounter( "dotnet.assemblies.count", () => (long)AppDomain.CurrentDomain.GetAssemblies().Length, unit: "{assembly}", description: "The number of .NET assemblies that are currently loaded."); - private static readonly Counter s_exceptionCount = s_meter.CreateCounter( + private static readonly Counter s_exceptions = s_meter.CreateCounter( "dotnet.exceptions", unit: "{exception}", description: "The number of exceptions that have been thrown in managed code."); - private static readonly ObservableUpDownCounter s_cpuCount = s_meter.CreateObservableUpDownCounter( + private static readonly ObservableUpDownCounter 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 s_cpuTime = s_meter.CreateObservableCounter( + private static readonly ObservableCounter s_processCpuTime = s_meter.CreateObservableCounter( "dotnet.process.cpu.time", GetCpuTime, unit: "s", @@ -159,13 +159,13 @@ static RuntimeMetrics() public static bool IsEnabled() { - return s_gcCollectionsCounter.Enabled - || s_gcObjectsSize.Enabled + return s_gcCollections.Enabled + || s_processWorkingSet.Enabled #if NET - || s_gcMemoryTotalAllocated.Enabled - || s_gcMemoryCommited.Enabled - || s_gcHeapSize.Enabled - || s_gcHeapFragmentation.Enabled + || s_gcHeapTotalAllocated.Enabled + || s_gcLastCollectionMemoryCommitted.Enabled + || s_gcLastCollectionHeapSize.Enabled + || s_gcLastCollectionFragmentationSize.Enabled || s_gcPauseTime.Enabled || s_jitCompiledSize.Enabled || s_jitCompiledMethodCount.Enabled @@ -176,10 +176,10 @@ public static bool IsEnabled() || s_threadPoolCompletedWorkItems.Enabled || s_threadPoolQueueLength.Enabled #endif - || s_assemblyCount.Enabled - || s_exceptionCount.Enabled - || s_cpuCount.Enabled - || s_cpuTime.Enabled; + || s_assembliesCount.Enabled + || s_exceptions.Enabled + || s_processCpuCount.Enabled + || s_processCpuTime.Enabled; } private static IEnumerable> GetGarbageCollectionCounts() From 6162a29d7fe1382cf423d2727debc31b3fbc42a8 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 17 Jul 2024 07:19:45 +0100 Subject: [PATCH 10/19] PR feedback and naming updates - Limits the new meter to .NET 9+ per the review feedback. - Comment out dotnet.process.cpu.time until blocking PR for a new API is merged. - Renames several counters to align with latest spec. --- ...System.Diagnostics.DiagnosticSource.csproj | 6 +- .../Diagnostics/Metrics/MeterListener.cs | 2 + .../Diagnostics/Metrics/RuntimeMetrics.cs | 55 ++++------ .../tests/RuntimeMetricsTests.cs | 101 +++++++++--------- ....Diagnostics.DiagnosticSource.Tests.csproj | 4 +- 5 files changed, 83 insertions(+), 85 deletions(-) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj index 53fa73acac23c8..990dce63229c3d 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj @@ -45,7 +45,6 @@ System.Diagnostics.DiagnosticSource - @@ -127,6 +126,10 @@ System.Diagnostics.DiagnosticSource + + + + @@ -138,7 +141,6 @@ System.Diagnostics.DiagnosticSource - diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterListener.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterListener.cs index 52de821c9f4e58..deef123f52a41e 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterListener.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterListener.cs @@ -35,8 +35,10 @@ public sealed class MeterListener : IDisposable static MeterListener() { +#if NET9_0_OR_GREATER // This ensures that the static Meter gets created before any listeners exist. _ = RuntimeMetrics.IsEnabled(); +#endif } /// 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 5edd6271a0d2dc..1ac12630a201f1 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,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -#if NET using System.Threading; -#endif namespace System.Diagnostics.Metrics { @@ -18,9 +16,8 @@ internal static class RuntimeMetrics // These MUST align to the possible attribute values defined in the semantic conventions (TODO: link to the spec) private static readonly string[] s_genNames = ["gen0", "gen1", "gen2", "loh", "poh"]; -#if NET + private static readonly int s_maxGenerations = Math.Min(GC.GetGCMemoryInfo().GenerationInfo.Length, s_genNames.Length); -#endif static RuntimeMetrics() { @@ -36,10 +33,10 @@ static RuntimeMetrics() } private static readonly ObservableCounter s_gcCollections = s_meter.CreateObservableCounter( - "dotnet.gc.collections.count", + "dotnet.gc.collections", GetGarbageCollectionCounts, unit: "{collection}", - description: "Number of garbage collections that have occurred since the process has started."); + description: "The number of garbage collections that have occurred since the process has started."); private static readonly ObservableUpDownCounter s_processWorkingSet = s_meter.CreateObservableUpDownCounter( "dotnet.process.memory.working_set", @@ -47,7 +44,6 @@ static RuntimeMetrics() unit: "By", description: "The number of bytes of physical memory mapped to the process context."); -#if NET private static readonly ObservableCounter s_gcHeapTotalAllocated = s_meter.CreateObservableCounter( "dotnet.gc.heap.total_allocated", () => GC.GetTotalAllocatedBytes(), @@ -92,7 +88,7 @@ static RuntimeMetrics() description: "Count of bytes of intermediate language that have been compiled since the process has started."); private static readonly ObservableCounter s_jitCompiledMethodCount = s_meter.CreateObservableCounter( - "dotnet.jit.compiled_method.count", + "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."); @@ -104,7 +100,7 @@ static RuntimeMetrics() description: "The number of times the JIT compiler (re)compiled methods since the process has started."); private static readonly ObservableCounter s_monitorLockContention = s_meter.CreateObservableCounter( - "dotnet.monitor.lock_contention.count", + "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."); @@ -132,10 +128,9 @@ static RuntimeMetrics() () => 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."); -#endif private static readonly ObservableUpDownCounter s_assembliesCount = s_meter.CreateObservableUpDownCounter( - "dotnet.assemblies.count", + "dotnet.assembly.count", () => (long)AppDomain.CurrentDomain.GetAssemblies().Length, unit: "{assembly}", description: "The number of .NET assemblies that are currently loaded."); @@ -151,17 +146,17 @@ static RuntimeMetrics() unit: "{cpu}", description: "The number of processors available to the process."); - 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."); + // 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."); public static bool IsEnabled() { return s_gcCollections.Enabled || s_processWorkingSet.Enabled -#if NET || s_gcHeapTotalAllocated.Enabled || s_gcLastCollectionMemoryCommitted.Enabled || s_gcLastCollectionHeapSize.Enabled @@ -175,11 +170,10 @@ public static bool IsEnabled() || s_threadPoolThreadCount.Enabled || s_threadPoolCompletedWorkItems.Enabled || s_threadPoolQueueLength.Enabled -#endif || s_assembliesCount.Enabled || s_exceptions.Enabled - || s_processCpuCount.Enabled - || s_processCpuTime.Enabled; + || s_processCpuCount.Enabled; + //|| s_processCpuTime.Enabled; } private static IEnumerable> GetGarbageCollectionCounts() @@ -194,20 +188,18 @@ private static IEnumerable> GetGarbageCollectionCounts() } } - private static IEnumerable> GetCpuTime() - { -#if NET - if (OperatingSystem.IsBrowser() || OperatingSystem.IsTvOS() || OperatingSystem.IsIOS()) - yield break; -#endif + // 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; - Process process = Process.GetCurrentProcess(); + // ProcessCpuUsage processCpuUsage = Environment.CpuUsage; - yield return new(process.UserProcessorTime.TotalSeconds, [new KeyValuePair("cpu.mode", "user")]); - yield return new(process.PrivilegedProcessorTime.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")]); + //} -#if NET private static IEnumerable> GetHeapSizes() { GCMemoryInfo gcInfo = GC.GetGCMemoryInfo(); @@ -233,6 +225,5 @@ private static IEnumerable> GetHeapFragmentation() yield return new(gcInfo.GenerationInfo[i].FragmentationAfterBytes, new KeyValuePair("gc.heap.generation", s_genNames[i])); } } -#endif } } diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs index 3d0a25fd64f17f..e0b582a916bc27 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs @@ -24,7 +24,7 @@ public class RuntimeMetricsTests [Fact] public void GcCollectionsCount() { - using InstrumentRecorder instrumentRecorder = new("dotnet.gc.collections.count"); + using InstrumentRecorder instrumentRecorder = new("dotnet.gc.collections"); for (var gen = 0; gen <= GC.MaxGeneration; gen++) { @@ -39,7 +39,11 @@ public void GcCollectionsCount() foundGenerations[i] = false; } - foreach (Measurement measurement in instrumentRecorder.GetMeasurements().Where(m => m.Value >= 1)) + var measurements = instrumentRecorder.GetMeasurements(); + + Assert.True(measurements.Count >= GC.MaxGeneration + 1, "Expected to find at least one measurement for each generation."); + + foreach (Measurement measurement in measurements.Where(m => m.Value >= 1)) { var tags = measurement.Tags.ToArray(); var tag = tags.SingleOrDefault(k => k.Key == "gc.heap.generation"); @@ -82,47 +86,48 @@ public void GcCollectionsCount() } } - [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."); - } - } + // 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."); + // } + //} [Fact] public void ExceptionsCount() @@ -168,24 +173,21 @@ public void ValidateMeasurements(string metricName, Func? public static IEnumerable LongMeasurements => new List { new object[] { "dotnet.process.memory.working_set", s_longGreaterThanZero, null }, - new object[] { "dotnet.assemblies.count", s_longGreaterThanZero, null }, + new object[] { "dotnet.assembly.count", s_longGreaterThanZero, null }, new object[] { "dotnet.process.cpu.count", s_longGreaterThanZero, null }, -#if NET new object[] { "dotnet.gc.heap.total_allocated", s_longGreaterThanZero, null }, new object[] { "dotnet.gc.last_collection.memory.committed_size", s_longGreaterThanZero, s_forceGc }, new object[] { "dotnet.gc.pause.time", s_doubleGreaterThanZero, s_forceGc }, new object[] { "dotnet.jit.compiled_il.size", s_longGreaterThanZero, null }, - new object[] { "dotnet.jit.compiled_method.count", s_longGreaterThanZero, null }, + new object[] { "dotnet.jit.compiled_methods", s_longGreaterThanZero, null }, new object[] { "dotnet.jit.compilation.time", s_doubleGreaterThanZero, null }, - new object[] { "dotnet.monitor.lock_contention.count", s_longGreaterThanOrEqualToZero, null }, + new object[] { "dotnet.monitor.lock_contentions", s_longGreaterThanOrEqualToZero, null }, new object[] { "dotnet.thread_pool.thread.count", s_longGreaterThanZero, null }, new object[] { "dotnet.thread_pool.work_item.count", s_longGreaterThanOrEqualToZero, null }, new object[] { "dotnet.thread_pool.queue.length", s_longGreaterThanOrEqualToZero, null }, new object[] { "dotnet.timer.count", s_longGreaterThanOrEqualToZero, null }, -#endif }; -#if NET [Theory] [InlineData("dotnet.gc.last_collection.heap.size")] [InlineData("dotnet.gc.last_collection.heap.fragmentation.size")] @@ -233,7 +235,6 @@ private void EnsureAllHeapTags(string metricName) Assert.True(foundGenerations[i], $"Expected to find a measurement for '{s_genNames[i]}'."); } } -#endif private static void ValidateSingleMeasurement(string metricName, Func? valueAssertion = null, Action? beforeRecord = null) where T : struct diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj b/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj index 6ba91d498fc957..7aa8ba5fe6be71 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj @@ -35,7 +35,6 @@ - @@ -44,6 +43,9 @@ + + + From 298b7fab15f24b9eff50dbaff74cb86045d2ce5f Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 17 Jul 2024 11:00:18 +0100 Subject: [PATCH 11/19] Fix exceptions count test --- .../tests/RuntimeMetricsTests.cs | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs index e0b582a916bc27..569d7cd8b3e4fe 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs @@ -137,7 +137,7 @@ public void ExceptionsCount() try { - throw new Exception(); + throw new RuntimeMeterException(); } catch { @@ -146,11 +146,11 @@ public void ExceptionsCount() var measurements = instrumentRecorder.GetMeasurements(); - Assert.Single(measurements); + AssertExceptions(measurements, 1); try { - throw new Exception(); + throw new RuntimeMeterException(); } catch { @@ -159,7 +159,40 @@ public void ExceptionsCount() measurements = instrumentRecorder.GetMeasurements(); - Assert.Equal(2, measurements.Count); + AssertExceptions(measurements, 2); + + static void AssertExceptions(IReadOnlyList> measurements, int expectedCount) + { + int foundExpectedExceptions = 0; + int foundUnexpectedExceptions = 0; + + foreach (Measurement measurement in measurements) + { + var tags = measurement.Tags.ToArray(); + var tag = tags.Single(k => k.Key == "error.type"); + + Assert.NotNull(tag.Key); + Assert.NotNull(tag.Value); + + if (tag.Value is not string tagValue) + { + Assert.Fail("Expected error type tag to be a string."); + return; + } + + if (tagValue == nameof(RuntimeMeterException)) + { + foundExpectedExceptions++; + } + else if (tagValue == nameof(InstrumentRecorderException)) + { + foundUnexpectedExceptions++; + } + } + + Assert.Equal(expectedCount, foundExpectedExceptions); + Assert.Equal(0, foundUnexpectedExceptions); + } } [Theory] @@ -210,7 +243,11 @@ private void EnsureAllHeapTags(string metricName) foundGenerations[i] = false; } - foreach (Measurement measurement in instrumentRecorder.GetMeasurements()) + var measurements = instrumentRecorder.GetMeasurements(); + + Assert.True(measurements.Count >= GC.MaxGeneration + 1, "Expected to find at least one measurement for each generation."); + + foreach (Measurement measurement in measurements) { var tags = measurement.Tags.ToArray(); var tag = tags.SingleOrDefault(k => k.Key == "gc.heap.generation"); @@ -253,6 +290,10 @@ private static void ValidateSingleMeasurement(string metricName, Func : IDisposable where T : struct { private readonly MeterListener _meterListener = new(); @@ -279,7 +320,7 @@ private void OnMeasurementRecorded(Instrument instrument, T measurement, ReadOnl { try { - throw new Exception(); + throw new InstrumentRecorderException(); } catch { From 024d2d0208cffb12fd3aa81fde5248620cb41fa6 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 17 Jul 2024 11:56:41 +0100 Subject: [PATCH 12/19] Add some test logging --- .../tests/RuntimeMetricsTests.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs index 569d7cd8b3e4fe..fd28dabe3c13fd 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs @@ -6,10 +6,11 @@ using System.Linq; using System.Threading; using Xunit; +using Xunit.Abstractions; namespace System.Diagnostics.Metrics.Tests { - public class RuntimeMetricsTests + public class RuntimeMetricsTests(ITestOutputHelper output) { private const string GreaterThanZeroMessage = "Expected value to be greater than zero."; private const string GreaterThanOrEqualToZeroMessage = "Expected value to be greater than or equal to zero."; @@ -21,9 +22,14 @@ public class RuntimeMetricsTests private static readonly Func s_longGreaterThanOrEqualToZero = v => v >= 0 ? (true, null) : (false, GreaterThanOrEqualToZeroMessage); private static readonly Func s_doubleGreaterThanZero = v => v > 0 ? (true, null) : (false, GreaterThanZeroMessage); + private readonly ITestOutputHelper _output = output; + [Fact] public void GcCollectionsCount() { + var pause = GC.GetTotalPauseDuration(); + _output.WriteLine($"GC pause time: {pause.TotalSeconds} [{pause.Ticks}]"); + using InstrumentRecorder instrumentRecorder = new("dotnet.gc.collections"); for (var gen = 0; gen <= GC.MaxGeneration; gen++) @@ -245,6 +251,11 @@ private void EnsureAllHeapTags(string metricName) var measurements = instrumentRecorder.GetMeasurements(); + var gcInfo = GC.GetGCMemoryInfo(); + + _output.WriteLine($"GenerationInfo.Length: {gcInfo.GenerationInfo.Length}"); + _output.WriteLine($"Count of measurements: {measurements.Count}"); + Assert.True(measurements.Count >= GC.MaxGeneration + 1, "Expected to find at least one measurement for each generation."); foreach (Measurement measurement in measurements) @@ -258,6 +269,8 @@ private void EnsureAllHeapTags(string metricName) string tagValue = (string)tag.Value; + _output.WriteLine($"Found tag value: {tagValue}"); + var index = Array.FindIndex(s_genNames, x => x == tagValue); if (index == -1) From 845739611982a1b71e5f303a808adac3f33de628 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 17 Jul 2024 12:09:26 +0100 Subject: [PATCH 13/19] Fix MetricsTests Ensure that each test only observes the expected Meter --- .../tests/MetricsTests.cs | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs index 3ed61757282500..161c9431df5dd1 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs @@ -184,7 +184,11 @@ public void ListeningToInstrumentsPublishingTest() // Listener is not enabled yet Assert.Equal(0, instrumentsEncountered); - listener.InstrumentPublished = (instruments, theListener) => instrumentsEncountered++; + listener.InstrumentPublished = (theInstrument, theListener) => + { + if (theInstrument.Meter.Name == meter.Name) + instrumentsEncountered++; + }; // Listener still not started yet Assert.Equal(0, instrumentsEncountered); @@ -891,7 +895,11 @@ public void MeterDisposalsTest() Gauge gauge = meter7.CreateGauge("Gauge"); using MeterListener listener = new MeterListener(); - listener.InstrumentPublished = (theInstrument, theListener) => theListener.EnableMeasurementEvents(theInstrument, theInstrument); + listener.InstrumentPublished = (theInstrument, theListener) => + { + if (theInstrument.Meter.Name.StartsWith("MeterDisposalsTest", StringComparison.Ordinal)) + theListener.EnableMeasurementEvents(theInstrument, theInstrument); + }; int count = 0; @@ -970,7 +978,11 @@ public void ListenerDisposalsTest() int completedMeasurements = 0; MeterListener listener = new MeterListener(); - listener.InstrumentPublished = (theInstrument, theListener) => theListener.EnableMeasurementEvents(theInstrument, theInstrument); + listener.InstrumentPublished = (theInstrument, theListener) => + { + if (theInstrument.Meter.Name == meter.Name) + theListener.EnableMeasurementEvents(theInstrument, theInstrument); + }; listener.MeasurementsCompleted = (theInstrument, state) => completedMeasurements++; int count = 0; @@ -1036,7 +1048,11 @@ public void ListenerWithoutMeasurementsCompletedDisposalsTest() ObservableUpDownCounter observableUpDownCounter = meter.CreateObservableUpDownCounter("ObservableUpDownCounter", () => new Measurement(-5.7f, new KeyValuePair[] { new KeyValuePair("Key", "value")})); MeterListener listener = new MeterListener(); - listener.InstrumentPublished = (theInstrument, theListener) => theListener.EnableMeasurementEvents(theInstrument, theInstrument); + listener.InstrumentPublished = (theInstrument, theListener) => + { + if (theInstrument.Meter.Name == meter.Name) + theListener.EnableMeasurementEvents(theInstrument, theInstrument); + }; int count = 0; @@ -1178,7 +1194,11 @@ public void EnableListeningMultipleTimesWithDifferentState() MeterListener listener = new MeterListener(); string lastState = "1"; - listener.InstrumentPublished = (theInstrument, theListener) => theListener.EnableMeasurementEvents(theInstrument, lastState); + listener.InstrumentPublished = (theInstrument, theListener) => + { + if (theInstrument.Meter.Name == meter.Name) + theListener.EnableMeasurementEvents(theInstrument, lastState); + }; int completedCount = 0; listener.MeasurementsCompleted = (theInstrument, state) => { Assert.Equal(lastState, state); completedCount++; }; listener.Start(); From 7708d0a9af3dcec879ec86c9d4b432fd5ebbe17a Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 17 Jul 2024 13:04:08 +0100 Subject: [PATCH 14/19] More test logging and extended wait for metrics --- .../tests/RuntimeMetricsTests.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs index fd28dabe3c13fd..cf679f23a32082 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs @@ -18,9 +18,18 @@ public class RuntimeMetricsTests(ITestOutputHelper output) private static readonly string[] s_genNames = ["gen0", "gen1", "gen2", "loh", "poh"]; private static readonly Action s_forceGc = () => GC.Collect(0, GCCollectionMode.Forced); - private static readonly Func s_longGreaterThanZero = v => v > 0 ? (true, null) : (false, GreaterThanZeroMessage); - private static readonly Func s_longGreaterThanOrEqualToZero = v => v >= 0 ? (true, null) : (false, GreaterThanOrEqualToZeroMessage); - private static readonly Func s_doubleGreaterThanZero = v => v > 0 ? (true, null) : (false, GreaterThanZeroMessage); + + private static readonly Func s_longGreaterThanZero = v => v > 0 + ? (true, null) + : (false, $"{GreaterThanZeroMessage} Actual value was: {v}."); + + private static readonly Func s_longGreaterThanOrEqualToZero = v => v >= 0 + ? (true, null) + : (false, $"{GreaterThanOrEqualToZeroMessage} Actual value was: {v}."); + + private static readonly Func s_doubleGreaterThanZero = v => v > 0 + ? (true, null) + : (false, $"{GreaterThanZeroMessage} Actual value was: {v}."); private readonly ITestOutputHelper _output = output; @@ -253,8 +262,10 @@ private void EnsureAllHeapTags(string metricName) var gcInfo = GC.GetGCMemoryInfo(); - _output.WriteLine($"GenerationInfo.Length: {gcInfo.GenerationInfo.Length}"); + _output.WriteLine($"GCMemoryInfo.GenerationInfo.Length: {gcInfo.GenerationInfo.Length}"); _output.WriteLine($"Count of measurements: {measurements.Count}"); + _output.WriteLine($"GenerationInfo.TotalCommittedBytes: {gcInfo.TotalCommittedBytes}"); + _output.WriteLine($"GC.MaxGeneration: {GC.MaxGeneration}"); Assert.True(measurements.Count >= GC.MaxGeneration + 1, "Expected to find at least one measurement for each generation."); @@ -348,7 +359,7 @@ public IReadOnlyList> GetMeasurements() { // Wait enough time for all the measurements to be enqueued via the // OnMeasurementRecorded callback. 50ms seems to be sufficient. - Thread.Sleep(50); + Thread.Sleep(100); return _values.ToArray(); } From fef38e1964392559b6a9c2b06ecd1df21c411b89 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 17 Jul 2024 14:39:00 +0100 Subject: [PATCH 15/19] More logging and a test fix --- .../tests/RuntimeMetricsTests.cs | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs index cf679f23a32082..7c2e799c097699 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs @@ -17,7 +17,15 @@ public class RuntimeMetricsTests(ITestOutputHelper output) private static readonly string[] s_genNames = ["gen0", "gen1", "gen2", "loh", "poh"]; - private static readonly Action s_forceGc = () => GC.Collect(0, GCCollectionMode.Forced); + private static readonly Func s_forceGc = () => + { + for (var gen = 0; gen <= GC.MaxGeneration; gen++) + { + GC.Collect(gen, GCCollectionMode.Forced); + } + + return GC.GetGCMemoryInfo().Index > 0; + }; private static readonly Func s_longGreaterThanZero = v => v > 0 ? (true, null) @@ -31,14 +39,15 @@ public class RuntimeMetricsTests(ITestOutputHelper output) ? (true, null) : (false, $"{GreaterThanZeroMessage} Actual value was: {v}."); + private static readonly Func s_doubleGreaterThanOrEqualToZero = v => v >= 0 + ? (true, null) + : (false, $"{GreaterThanOrEqualToZeroMessage} Actual value was: {v}."); + private readonly ITestOutputHelper _output = output; [Fact] public void GcCollectionsCount() { - var pause = GC.GetTotalPauseDuration(); - _output.WriteLine($"GC pause time: {pause.TotalSeconds} [{pause.Ticks}]"); - using InstrumentRecorder instrumentRecorder = new("dotnet.gc.collections"); for (var gen = 0; gen <= GC.MaxGeneration; gen++) @@ -212,7 +221,7 @@ static void AssertExceptions(IReadOnlyList> measurements, int [Theory] [MemberData(nameof(LongMeasurements))] - public void ValidateMeasurements(string metricName, Func? valueAssertion, Action? beforeRecord) + public void ValidateMeasurements(string metricName, Func? valueAssertion, Func? beforeRecord) where T : struct { ValidateSingleMeasurement(metricName, valueAssertion, beforeRecord); @@ -225,7 +234,7 @@ public void ValidateMeasurements(string metricName, Func? new object[] { "dotnet.process.cpu.count", s_longGreaterThanZero, null }, new object[] { "dotnet.gc.heap.total_allocated", s_longGreaterThanZero, null }, new object[] { "dotnet.gc.last_collection.memory.committed_size", s_longGreaterThanZero, s_forceGc }, - new object[] { "dotnet.gc.pause.time", s_doubleGreaterThanZero, s_forceGc }, + new object[] { "dotnet.gc.pause.time", s_doubleGreaterThanOrEqualToZero, s_forceGc }, // may be zero if no GC has occurred new object[] { "dotnet.jit.compiled_il.size", s_longGreaterThanZero, null }, new object[] { "dotnet.jit.compiled_methods", s_longGreaterThanZero, null }, new object[] { "dotnet.jit.compilation.time", s_doubleGreaterThanZero, null }, @@ -266,8 +275,11 @@ private void EnsureAllHeapTags(string metricName) _output.WriteLine($"Count of measurements: {measurements.Count}"); _output.WriteLine($"GenerationInfo.TotalCommittedBytes: {gcInfo.TotalCommittedBytes}"); _output.WriteLine($"GC.MaxGeneration: {GC.MaxGeneration}"); + _output.WriteLine($"GCMemoryInfo.Index: {gcInfo.Index}"); - Assert.True(measurements.Count >= GC.MaxGeneration + 1, "Expected to find at least one measurement for each generation."); + var gensExpected = GC.MaxGeneration + 1; + Assert.True(measurements.Count >= gensExpected, $"Expected to find at least one measurement for each generation ({gensExpected}) " + + $"but received {measurements.Count} measurements."); foreach (Measurement measurement in measurements) { @@ -297,12 +309,16 @@ private void EnsureAllHeapTags(string metricName) } } - private static void ValidateSingleMeasurement(string metricName, Func? valueAssertion = null, Action? beforeRecord = null) + private static void ValidateSingleMeasurement(string metricName, Func? valueAssertion = null, Func? beforeRecord = null) where T : struct { using InstrumentRecorder instrumentRecorder = new(metricName); - beforeRecord?.Invoke(); + var shouldContinue = beforeRecord?.Invoke() ?? true; + + if (!shouldContinue) + return; + instrumentRecorder.RecordObservableInstruments(); var measurements = instrumentRecorder.GetMeasurements(); Assert.Single(measurements); From a5c348d4b07e06f6cd02033ba37825e5615b958e Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 17 Jul 2024 14:40:25 +0100 Subject: [PATCH 16/19] Remove System.Diagnostics.Process --- .../src/System.Diagnostics.DiagnosticSource.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj index 990dce63229c3d..666ef37b22c9e3 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj @@ -133,7 +133,6 @@ System.Diagnostics.DiagnosticSource - From e3662c945bda8d82e7f74ec46d8bc01903c09408 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 17 Jul 2024 15:43:44 +0100 Subject: [PATCH 17/19] Update HeapTags test to assert correctly if no GC has run --- .../tests/RuntimeMetricsTests.cs | 92 +++++++++---------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs index 7c2e799c097699..8e20e0da78d540 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs @@ -39,7 +39,7 @@ public class RuntimeMetricsTests(ITestOutputHelper output) ? (true, null) : (false, $"{GreaterThanZeroMessage} Actual value was: {v}."); - private static readonly Func s_doubleGreaterThanOrEqualToZero = v => v >= 0 + private static readonly Func s_doubleGreaterThanOrEqualToZero = v => v >= 0 ? (true, null) : (false, $"{GreaterThanOrEqualToZeroMessage} Actual value was: {v}."); @@ -65,7 +65,9 @@ public void GcCollectionsCount() var measurements = instrumentRecorder.GetMeasurements(); - Assert.True(measurements.Count >= GC.MaxGeneration + 1, "Expected to find at least one measurement for each generation."); + var gensExpected = GC.MaxGeneration + 1; + Assert.True(measurements.Count >= gensExpected, $"Expected to find at least one measurement for each generation ({gensExpected}) " + + $"but received {measurements.Count} measurements."); foreach (Measurement measurement in measurements.Where(m => m.Value >= 1)) { @@ -219,14 +221,6 @@ static void AssertExceptions(IReadOnlyList> measurements, int } } - [Theory] - [MemberData(nameof(LongMeasurements))] - public void ValidateMeasurements(string metricName, Func? valueAssertion, Func? beforeRecord) - where T : struct - { - ValidateSingleMeasurement(metricName, valueAssertion, beforeRecord); - } - public static IEnumerable LongMeasurements => new List { new object[] { "dotnet.process.memory.working_set", s_longGreaterThanZero, null }, @@ -245,6 +239,35 @@ public void ValidateMeasurements(string metricName, Func? new object[] { "dotnet.timer.count", s_longGreaterThanOrEqualToZero, null }, }; + [Theory] + [MemberData(nameof(LongMeasurements))] + public void ValidateMeasurements(string metricName, Func? valueAssertion, Func? beforeRecord) + where T : struct + { + ValidateSingleMeasurement(metricName, valueAssertion, beforeRecord); + } + + private static void ValidateSingleMeasurement(string metricName, Func? valueAssertion = null, Func? beforeRecord = null) + where T : struct + { + using InstrumentRecorder instrumentRecorder = new(metricName); + + var shouldContinue = beforeRecord?.Invoke() ?? true; + + if (!shouldContinue) + return; + + instrumentRecorder.RecordObservableInstruments(); + var measurements = instrumentRecorder.GetMeasurements(); + Assert.Single(measurements); + + if (valueAssertion is not null) + { + var (isExpected, message) = valueAssertion(measurements[0].Value); + Assert.True(isExpected, message); + } + } + [Theory] [InlineData("dotnet.gc.last_collection.heap.size")] [InlineData("dotnet.gc.last_collection.heap.fragmentation.size")] @@ -260,6 +283,14 @@ private void EnsureAllHeapTags(string metricName) } instrumentRecorder.RecordObservableInstruments(); + var measurements = instrumentRecorder.GetMeasurements(); + + if (GC.GetGCMemoryInfo().Index == 0) + { + // No GC has occurred which can be the case on some platforms. + Assert.Empty(measurements); + return; + } bool[] foundGenerations = new bool[s_genNames.Length]; for (int i = 0; i < 5; i++) @@ -267,16 +298,6 @@ private void EnsureAllHeapTags(string metricName) foundGenerations[i] = false; } - var measurements = instrumentRecorder.GetMeasurements(); - - var gcInfo = GC.GetGCMemoryInfo(); - - _output.WriteLine($"GCMemoryInfo.GenerationInfo.Length: {gcInfo.GenerationInfo.Length}"); - _output.WriteLine($"Count of measurements: {measurements.Count}"); - _output.WriteLine($"GenerationInfo.TotalCommittedBytes: {gcInfo.TotalCommittedBytes}"); - _output.WriteLine($"GC.MaxGeneration: {GC.MaxGeneration}"); - _output.WriteLine($"GCMemoryInfo.Index: {gcInfo.Index}"); - var gensExpected = GC.MaxGeneration + 1; Assert.True(measurements.Count >= gensExpected, $"Expected to find at least one measurement for each generation ({gensExpected}) " + $"but received {measurements.Count} measurements."); @@ -292,8 +313,6 @@ private void EnsureAllHeapTags(string metricName) string tagValue = (string)tag.Value; - _output.WriteLine($"Found tag value: {tagValue}"); - var index = Array.FindIndex(s_genNames, x => x == tagValue); if (index == -1) @@ -309,27 +328,6 @@ private void EnsureAllHeapTags(string metricName) } } - private static void ValidateSingleMeasurement(string metricName, Func? valueAssertion = null, Func? beforeRecord = null) - where T : struct - { - using InstrumentRecorder instrumentRecorder = new(metricName); - - var shouldContinue = beforeRecord?.Invoke() ?? true; - - if (!shouldContinue) - return; - - instrumentRecorder.RecordObservableInstruments(); - var measurements = instrumentRecorder.GetMeasurements(); - Assert.Single(measurements); - - if (valueAssertion is not null) - { - var (isExpected, message) = valueAssertion(measurements[0].Value); - Assert.True(isExpected, message); - } - } - private sealed class RuntimeMeterException() : Exception { } private sealed class InstrumentRecorderException() : Exception { } @@ -342,6 +340,7 @@ private sealed class InstrumentRecorder : IDisposable where T : struct public InstrumentRecorder(string instrumentName, bool injectException = false) { + _injectException = injectException; _meterListener.InstrumentPublished = (instrument, listener) => { if (instrument.Meter.Name == "System.Runtime" && instrument.Name == instrumentName) @@ -351,11 +350,12 @@ public InstrumentRecorder(string instrumentName, bool injectException = false) }; _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded); _meterListener.Start(); - _injectException = injectException; } private void OnMeasurementRecorded(Instrument instrument, T measurement, ReadOnlySpan> tags, object? state) { + _values.Enqueue(new Measurement(measurement, tags)); + if (_injectException) { try @@ -367,14 +367,12 @@ private void OnMeasurementRecorded(Instrument instrument, T measurement, ReadOnl // Ignore the exception. } } - - _values.Enqueue(new Measurement(measurement, tags)); } public IReadOnlyList> GetMeasurements() { // Wait enough time for all the measurements to be enqueued via the - // OnMeasurementRecorded callback. 50ms seems to be sufficient. + // OnMeasurementRecorded callback. This value seems to be sufficient. Thread.Sleep(100); return _values.ToArray(); } From 24af2813401d463cd8a3342b5e3e028568a84116 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Wed, 17 Jul 2024 15:58:54 +0100 Subject: [PATCH 18/19] Only include System.Threading.ThreadPool reference for net9.0 --- .../src/System.Diagnostics.DiagnosticSource.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj index 666ef37b22c9e3..43fe7a2de45d77 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj @@ -128,6 +128,7 @@ System.Diagnostics.DiagnosticSource + @@ -139,7 +140,6 @@ System.Diagnostics.DiagnosticSource - From 5105ee8c6312435f4b8bb7035bf54bcfc623a71c Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Thu, 18 Jul 2024 05:45:33 +0100 Subject: [PATCH 19/19] Skip tests on browser --- .../tests/RuntimeMetricsTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs index 8e20e0da78d540..f5554720f4551e 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs @@ -45,7 +45,7 @@ public class RuntimeMetricsTests(ITestOutputHelper output) private readonly ITestOutputHelper _output = output; - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] public void GcCollectionsCount() { using InstrumentRecorder instrumentRecorder = new("dotnet.gc.collections"); @@ -155,7 +155,7 @@ public void GcCollectionsCount() // } //} - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] public void ExceptionsCount() { // We inject an exception into the MeterListener callback here, so we can test that we don't recursively record exceptions. @@ -239,7 +239,7 @@ static void AssertExceptions(IReadOnlyList> measurements, int new object[] { "dotnet.timer.count", s_longGreaterThanOrEqualToZero, null }, }; - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] [MemberData(nameof(LongMeasurements))] public void ValidateMeasurements(string metricName, Func? valueAssertion, Func? beforeRecord) where T : struct @@ -268,7 +268,7 @@ private static void ValidateSingleMeasurement(string metricName, Func EnsureAllHeapTags(metricName);