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 eafefa6e9fa33..43fe7a2de45d7 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj @@ -126,6 +126,11 @@ 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 017074586d583..deef123f52a41 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,14 @@ 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() + { +#if NET9_0_OR_GREATER + // This ensures that the static Meter gets created before any listeners exist. + _ = RuntimeMetrics.IsEnabled(); +#endif + } + /// /// 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 0000000000000..1ac12630a201f --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/RuntimeMetrics.cs @@ -0,0 +1,229 @@ +// 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.Threading; + +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"]; + + private static readonly int s_maxGenerations = Math.Min(GC.GetGCMemoryInfo().GenerationInfo.Length, s_genNames.Length); + + 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_exceptions.Add(1, new KeyValuePair("error.type", e.Exception.GetType().Name)); + t_handlingFirstChanceException = false; + }; + } + + private static readonly ObservableCounter s_gcCollections = s_meter.CreateObservableCounter( + "dotnet.gc.collections", + GetGarbageCollectionCounts, + unit: "{collection}", + description: "The number of garbage collections that have occurred since the process has started."); + + private static readonly ObservableUpDownCounter s_processWorkingSet = s_meter.CreateObservableUpDownCounter( + "dotnet.process.memory.working_set", + () => Environment.WorkingSet, + unit: "By", + description: "The number of bytes of physical memory mapped to the process context."); + + private static readonly ObservableCounter 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_gcLastCollectionMemoryCommitted = s_meter.CreateObservableUpDownCounter( + "dotnet.gc.last_collection.memory.committed_size", + () => + { + GCMemoryInfo gcInfo = GC.GetGCMemoryInfo(); + + return gcInfo.Index == 0 + ? Array.Empty>() + : [new(gcInfo.TotalCommittedBytes)]; + }, + unit: "By", + description: "The amount of committed virtual memory in use by the .NET GC, as observed during the latest garbage collection."); + + private static readonly ObservableUpDownCounter 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_gcLastCollectionFragmentationSize = s_meter.CreateObservableUpDownCounter( + "dotnet.gc.last_collection.heap.fragmentation.size", + GetHeapFragmentation, + unit: "By", + description: "The heap fragmentation, as observed during the latest garbage collection."); + + private static readonly ObservableCounter s_gcPauseTime = s_meter.CreateObservableCounter( + "dotnet.gc.pause.time", + () => GC.GetTotalPauseDuration().TotalSeconds, + unit: "s", + description: "The total amount of time paused in GC since the process has started."); + + private static readonly ObservableCounter 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_methods", + () => Runtime.JitInfo.GetCompiledMethodCount(), + unit: "{method}", + description: "The number of times the JIT compiler (re)compiled methods since the process has started."); + + private static readonly ObservableCounter s_jitCompilationTime = s_meter.CreateObservableCounter( + "dotnet.jit.compilation.time", + () => Runtime.JitInfo.GetCompilationTime().TotalSeconds, + unit: "s", + description: "The number of times the JIT compiler (re)compiled methods since the process has started."); + + private static readonly ObservableCounter s_monitorLockContention = s_meter.CreateObservableCounter( + "dotnet.monitor.lock_contentions", + () => Monitor.LockContentionCount, + unit: "{contention}", + description: "The number of times there was contention when trying to acquire a monitor lock since the process has started."); + + private static readonly ObservableCounter 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."); + + 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."); + + private static readonly ObservableUpDownCounter s_assembliesCount = s_meter.CreateObservableUpDownCounter( + "dotnet.assembly.count", + () => (long)AppDomain.CurrentDomain.GetAssemblies().Length, + unit: "{assembly}", + description: "The number of .NET assemblies that are currently loaded."); + + private static readonly Counter 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_processCpuCount = s_meter.CreateObservableUpDownCounter( + "dotnet.process.cpu.count", + () => (long)Environment.ProcessorCount, + unit: "{cpu}", + description: "The number of processors available to the process."); + + // 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 + || s_gcHeapTotalAllocated.Enabled + || s_gcLastCollectionMemoryCommitted.Enabled + || s_gcLastCollectionHeapSize.Enabled + || s_gcLastCollectionFragmentationSize.Enabled + || s_gcPauseTime.Enabled + || s_jitCompiledSize.Enabled + || s_jitCompiledMethodCount.Enabled + || s_jitCompilationTime.Enabled + || s_monitorLockContention.Enabled + || s_timerCount.Enabled + || s_threadPoolThreadCount.Enabled + || s_threadPoolCompletedWorkItems.Enabled + || s_threadPoolQueueLength.Enabled + || s_assembliesCount.Enabled + || s_exceptions.Enabled + || s_processCpuCount.Enabled; + //|| s_processCpuTime.Enabled; + } + + private static IEnumerable> GetGarbageCollectionCounts() + { + long collectionsFromHigherGeneration = 0; + + 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])); + collectionsFromHigherGeneration = collectionsFromThisGeneration; + } + } + + // 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; + + // ProcessCpuUsage processCpuUsage = Environment.CpuUsage; + + // yield return new(processCpuUsage.UserTime.TotalSeconds, [new KeyValuePair("cpu.mode", "user")]); + // yield return new(processCpuUsage.PrivilegedTime.TotalSeconds, [new KeyValuePair("cpu.mode", "system")]); + //} + + 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("gc.heap.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("gc.heap.generation", s_genNames[i])); + } + } + } +} diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs index 3ed6175728250..161c9431df5dd 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(); 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 0000000000000..f5554720f4551 --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/RuntimeMetricsTests.cs @@ -0,0 +1,385 @@ +// 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 Xunit; +using Xunit.Abstractions; + +namespace System.Diagnostics.Metrics.Tests +{ + 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."; + + private static readonly string[] s_genNames = ["gen0", "gen1", "gen2", "loh", "poh"]; + + 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) + : (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 static readonly Func s_doubleGreaterThanOrEqualToZero = v => v >= 0 + ? (true, null) + : (false, $"{GreaterThanOrEqualToZeroMessage} Actual value was: {v}."); + + private readonly ITestOutputHelper _output = output; + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public void GcCollectionsCount() + { + using InstrumentRecorder instrumentRecorder = new("dotnet.gc.collections"); + + for (var gen = 0; gen <= GC.MaxGeneration; gen++) + { + GC.Collect(gen, GCCollectionMode.Forced); + } + + instrumentRecorder.RecordObservableInstruments(); + + bool[] foundGenerations = new bool[GC.MaxGeneration + 1]; + for (int i = 0; i < GC.MaxGeneration + 1; i++) + { + foundGenerations[i] = false; + } + + var measurements = instrumentRecorder.GetMeasurements(); + + 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)) + { + 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; + + 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 '{tagValue}'."); + break; + } + } + } + + for (int i = 0; i < foundGenerations.Length; i++) + { + 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}'."); + } + } + + // 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."); + // } + //} + + [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. + using InstrumentRecorder instrumentRecorder = new("dotnet.exceptions", injectException: true); + + try + { + throw new RuntimeMeterException(); + } + catch + { + // Ignore the exception. + } + + var measurements = instrumentRecorder.GetMeasurements(); + + AssertExceptions(measurements, 1); + + try + { + throw new RuntimeMeterException(); + } + catch + { + // Ignore the exception. + } + + measurements = instrumentRecorder.GetMeasurements(); + + 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); + } + } + + public static IEnumerable LongMeasurements => new List + { + new object[] { "dotnet.process.memory.working_set", s_longGreaterThanZero, null }, + new object[] { "dotnet.assembly.count", s_longGreaterThanZero, null }, + 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_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 }, + 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 }, + }; + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + [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); + } + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + [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) + { + using InstrumentRecorder instrumentRecorder = new(metricName); + + for (var gen = 0; gen <= GC.MaxGeneration; gen++) + { + GC.Collect(gen, GCCollectionMode.Forced); + } + + 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++) + { + foundGenerations[i] = false; + } + + 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) + { + 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.True(foundGenerations[i], $"Expected to find a measurement for '{s_genNames[i]}'."); + } + } + + private sealed class RuntimeMeterException() : Exception { } + + private sealed class InstrumentRecorderException() : Exception { } + + 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, bool injectException = false) + { + _injectException = injectException; + _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)); + + if (_injectException) + { + try + { + throw new InstrumentRecorderException(); + } + catch + { + // Ignore the exception. + } + } + } + + public IReadOnlyList> GetMeasurements() + { + // Wait enough time for all the measurements to be enqueued via the + // OnMeasurementRecorded callback. This value seems to be sufficient. + Thread.Sleep(100); + return _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 446c67800c4a0..7aa8ba5fe6be7 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 @@ -43,6 +43,9 @@ + + +