From: Noah Falk Date: Tue, 16 Jan 2024 23:46:31 +0000 (-0800) Subject: Counter instruments display absolute values now in dotnet-counters (#4455) X-Git-Tag: accepted/tizen/unified/20241231.014852~40^2~232 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=2a0c0c714e781600234850dbc4a5daf2bf977c24;p=platform%2Fcore%2Fdotnet%2Fdiagnostics.git Counter instruments display absolute values now in dotnet-counters (#4455) Fixes #3751 Previously we always showed counter instruments as a rate, but for a variety of counters that isn't the most useful value. Now we are showing the absolute value as long as the target process is running .NET 8. For cases where it was useful to see rate of change there is now a --showDeltas option which enables a 2nd column of output for dotnet-counters monitor. The deltas show the difference between the previous value and the current value. Making the new column optional rather than a default setting is intended to minimize consuming horizontal line space which is already at a premium. When using csv or json outputs it is assumed the user's tool that processes the data later can compute the rate if desired so there is no special delta handling added for those. Take a look at the updated exporter tests to see the specific changes in dotnet-counters output format. For other tools consuming System.Diagnostics.Monitoring.EventPipe there is a new setting on the settings object that allows opting in to the new behavior. I'm guessing dotnet-monitor will want to do it too but I wanted to ensure they can opt-into the new behavior at a time of their choosing. --- diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs index 83ed76611..e783efad1 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs @@ -127,10 +127,15 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe } } + /// + /// This gets generated for Counter instruments from Meters. This is used for pre-.NET 8 versions of MetricsEventSource that only reported rate and not absolute value, + /// or for any tools that haven't opted into using RateAndValuePayload in the CounterConfiguration settings. + /// internal sealed class RatePayload : MeterPayload { - public RatePayload(CounterMetadata counterMetadata, string displayName, string displayUnits, string valueTags, double value, double intervalSecs, DateTime timestamp) : - base(timestamp, counterMetadata, displayName, displayUnits, value, CounterType.Rate, valueTags, EventType.Rate) + + public RatePayload(CounterMetadata counterMetadata, string displayName, string displayUnits, string valueTags, double rate, double intervalSecs, DateTime timestamp) : + base(timestamp, counterMetadata, displayName, displayUnits, rate, CounterType.Rate, valueTags, EventType.Rate) { // In case these properties are not provided, set them to appropriate values. string counterName = string.IsNullOrEmpty(displayName) ? counterMetadata.CounterName : displayName; @@ -140,6 +145,25 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe } } + /// + /// Starting in .NET 8, MetricsEventSource reports counters with both absolute value and rate. If enabled in the CounterConfiguration and the new value field is present + /// then this payload will be created rather than the older RatePayload. Unlike RatePayload, this one treats the absolute value as the primary statistic. + /// + internal sealed class CounterRateAndValuePayload : MeterPayload + { + public CounterRateAndValuePayload(CounterMetadata counterMetadata, string displayName, string displayUnits, string valueTags, double rate, double value, DateTime timestamp) : + base(timestamp, counterMetadata, displayName, displayUnits, value, CounterType.Metric, valueTags, EventType.Rate) + { + // In case these properties are not provided, set them to appropriate values. + string counterName = string.IsNullOrEmpty(displayName) ? counterMetadata.CounterName : displayName; + string unitsName = string.IsNullOrEmpty(displayUnits) ? "Count" : displayUnits; + DisplayName = $"{counterName} ({unitsName})"; + Rate = rate; + } + + public double Rate { get; private set; } + } + internal record struct Quantile(double Percentage, double Value); internal sealed class PercentilePayload : MeterPayload diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipeline.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipeline.cs index 9f8f34c15..65c2ba288 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipeline.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipeline.cs @@ -57,7 +57,8 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe SessionId = _sessionId, ClientId = _clientId, MaxHistograms = Settings.MaxHistograms, - MaxTimeseries = Settings.MaxTimeSeries + MaxTimeseries = Settings.MaxTimeSeries, + UseCounterRateAndValuePayload = Settings.UseCounterRateAndValuePayloads }; return config; diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipelineSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipelineSettings.cs index 4e9c4f83f..472d40023 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipelineSettings.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipelineSettings.cs @@ -18,6 +18,11 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe public int MaxTimeSeries { get; set; } public bool UseSharedSession { get; set; } + + // Starting in .NET 8 MetricsEventSource reports both absolute value and rate for Counter instruments + // If this is false the pipeline will produce RatePayload objects + // If this is true and the new absolute value field is available the pipeline will produce CounterRateAndValuePayload instead + public bool UseCounterRateAndValuePayloads { get; set; } } [Flags] diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs index f69dc85b5..7bd7e3611 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs @@ -25,6 +25,11 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe public int MaxHistograms { get; set; } public int MaxTimeseries { get; set; } + + // Starting in .NET 8 MetricsEventSource reports both absolute value and rate for Counter instruments + // If this is false the pipeline will produce RatePayload objects + // If this is true the pipeline will produce CounterRateAndValuePayload instead if value field is available + public bool UseCounterRateAndValuePayload { get; set; } } internal record struct ProviderAndCounter(string ProviderName, string CounterName); @@ -249,15 +254,30 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe string unit = (string)traceEvent.PayloadValue(4); string tags = (string)traceEvent.PayloadValue(5); string rateText = (string)traceEvent.PayloadValue(6); + //Starting in .NET 8 we also publish the absolute value of these counters + string absoluteValueText = null; + if (traceEvent.Version >= 1) + { + absoluteValueText = (string)traceEvent.PayloadValue(7); + } if (!counterConfiguration.CounterFilter.IsIncluded(meterName, instrumentName)) { return; } - if (double.TryParse(rateText, NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out double value)) + if (double.TryParse(rateText, NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out double rate)) { - payload = new RatePayload(GetCounterMetadata(meterName, instrumentName), null, unit, tags, value, counterConfiguration.CounterFilter.DefaultIntervalSeconds, traceEvent.TimeStamp); + if (absoluteValueText != null && + counterConfiguration.UseCounterRateAndValuePayload && + double.TryParse(absoluteValueText, NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out double value)) + { + payload = new CounterRateAndValuePayload(GetCounterMetadata(meterName, instrumentName), null, unit, tags, rate, value, traceEvent.TimeStamp); + } + else + { + payload = new RatePayload(GetCounterMetadata(meterName, instrumentName), null, unit, tags, rate, counterConfiguration.CounterFilter.DefaultIntervalSeconds, traceEvent.TimeStamp); + } } else { diff --git a/src/Tools/dotnet-counters/CounterMonitor.cs b/src/Tools/dotnet-counters/CounterMonitor.cs index ab72fa0c0..5f1ff39d6 100644 --- a/src/Tools/dotnet-counters/CounterMonitor.cs +++ b/src/Tools/dotnet-counters/CounterMonitor.cs @@ -166,7 +166,8 @@ namespace Microsoft.Diagnostics.Tools.Counters bool resumeRuntime, int maxHistograms, int maxTimeSeries, - TimeSpan duration) + TimeSpan duration, + bool showDeltas) { try { @@ -197,7 +198,7 @@ namespace Microsoft.Diagnostics.Tools.Counters // the launch command may misinterpret app arguments as the old space separated // provider list so we need to ignore it in that case _counterList = ConfigureCounters(counters, _processId != 0 ? counter_list : null); - _renderer = new ConsoleWriter(new DefaultConsole(useAnsi)); + _renderer = new ConsoleWriter(new DefaultConsole(useAnsi), showDeltaColumn:showDeltas); _diagnosticsClient = holder.Client; _settings = new MetricsPipelineSettings(); _settings.Duration = duration == TimeSpan.Zero ? Timeout.InfiniteTimeSpan : duration; @@ -206,6 +207,7 @@ namespace Microsoft.Diagnostics.Tools.Counters _settings.CounterIntervalSeconds = refreshInterval; _settings.ResumeRuntime = resumeRuntime; _settings.CounterGroups = GetEventPipeProviders(); + _settings.UseCounterRateAndValuePayloads = true; bool useSharedSession = false; if (_diagnosticsClient.GetProcessInfo().TryGetProcessClrVersion(out Version version)) diff --git a/src/Tools/dotnet-counters/Exporters/ConsoleWriter.cs b/src/Tools/dotnet-counters/Exporters/ConsoleWriter.cs index eaf084e74..954b498ee 100644 --- a/src/Tools/dotnet-counters/Exporters/ConsoleWriter.cs +++ b/src/Tools/dotnet-counters/Exporters/ConsoleWriter.cs @@ -29,13 +29,8 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters public readonly CounterProvider KnownProvider; } - private interface ICounterRow - { - int Row { get; set; } - } - /// Information about an observed counter. - private class ObservedCounter : ICounterRow + private class ObservedCounter { public ObservedCounter(string displayName) => DisplayName = displayName; public string DisplayName { get; } // Display name for this counter. @@ -45,9 +40,10 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters public bool RenderValueInline => TagSets.Count == 0 || (TagSets.Count == 1 && string.IsNullOrEmpty(TagSets.Keys.First())); public double LastValue { get; set; } + public double? LastDelta { get; set; } } - private class ObservedTagSet : ICounterRow + private class ObservedTagSet { public ObservedTagSet(string tags) { @@ -57,14 +53,16 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters public string DisplayTags => string.IsNullOrEmpty(Tags) ? "" : Tags; public int Row { get; set; } // Assigned row for this counter. May change during operation. public double LastValue { get; set; } + public double? LastDelta { get; set; } } private readonly object _lock = new(); private readonly Dictionary _providers = new(); // Tracks observed providers and counters. + private readonly bool _showDeltaColumn; private const int Indent = 4; // Counter name indent size. private const int CounterValueLength = 15; - private int _maxNameLength; + private int _nameColumnWidth; // fixed width of the name column. Names will be truncated if needed to fit in this space. private int _statusRow; // Row # of where we print the status of dotnet-counters private int _topRow; private bool _paused; @@ -77,9 +75,10 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters private int _consoleWidth = -1; private IConsole _console; - public ConsoleWriter(IConsole console) + public ConsoleWriter(IConsole console, bool showDeltaColumn = false) { _console = console; + _showDeltaColumn = showDeltaColumn; } public void Initialize() @@ -126,7 +125,9 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters _consoleWidth = _console.WindowWidth; _consoleHeight = _console.WindowHeight; - _maxNameLength = Math.Max(Math.Min(80, _consoleWidth) - (CounterValueLength + Indent + 1), 0); // Truncate the name to prevent line wrapping as long as the console width is >= CounterValueLength + Indent + 1 characters + // Truncate the name column if needed to prevent line wrapping + int numValueColumns = _showDeltaColumn ? 2 : 1; + _nameColumnWidth = Math.Max(Math.Min(80, _consoleWidth) - numValueColumns * (CounterValueLength + 1), 0); int row = _console.CursorTop; @@ -141,62 +142,36 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters row += GetLineWrappedLines(_errorText); } - bool RenderRow(ref int row, string lineOutput = null, ICounterRow counterRow = null) - { - if (row >= _consoleHeight + _topRow) // prevents from displaying more counters than vertical space available - { - return false; - } - - if (lineOutput != null) - { - _console.Write(lineOutput); - } - - if (row < _consoleHeight + _topRow - 1) // prevents screen from scrolling due to newline on last line of console - { - _console.WriteLine(); - } - - if (counterRow != null) - { - counterRow.Row = row; - } - - row++; - - return true; - } - - if (RenderRow(ref row)) // Blank line. + if (RenderRow(ref row) && // Blank line. + RenderTableRow(ref row, "Name", "Current Value", "Last Delta")) // Table header { foreach (ObservedProvider provider in _providers.Values.OrderBy(p => p.KnownProvider == null).ThenBy(p => p.Name)) // Known providers first. { - if (!RenderRow(ref row, $"[{provider.Name}]")) + if (!RenderTableRow(ref row, $"[{provider.Name}]")) { break; } foreach (ObservedCounter counter in provider.Counters.Values.OrderBy(c => c.DisplayName)) { - string name = MakeFixedWidth($"{new string(' ', Indent)}{counter.DisplayName}", Indent + _maxNameLength); + counter.Row = row; if (counter.RenderValueInline) { - if (!RenderRow(ref row, $"{name} {FormatValue(counter.LastValue)}", counter)) + if (!RenderCounterValueRow(ref row, indentLevel:1, counter.DisplayName, counter.LastValue, 0)) { break; } } else { - if (!RenderRow(ref row, name, counter)) + if (!RenderCounterNameRow(ref row, counter.DisplayName)) { break; } foreach (ObservedTagSet tagSet in counter.TagSets.Values.OrderBy(t => t.Tags)) { - string tagName = MakeFixedWidth($"{new string(' ', 2 * Indent)}{tagSet.Tags}", Indent + _maxNameLength); - if (!RenderRow(ref row, $"{tagName} {FormatValue(tagSet.LastValue)}", tagSet)) + tagSet.Row = row; + if (!RenderCounterValueRow(ref row, indentLevel: 2, tagSet.Tags, tagSet.LastValue, 0)) { break; } @@ -251,21 +226,30 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters { string displayName = payload.GetDisplay(); provider.Counters[name] = counter = new ObservedCounter(displayName); - _maxNameLength = Math.Max(_maxNameLength, displayName.Length); - if (tags != null) - { - counter.LastValue = payload.Value; - } redraw = true; } + else + { + counter.LastDelta = payload.Value - counter.LastValue; + } ObservedTagSet tagSet = null; - if (tags != null && !counter.TagSets.TryGetValue(tags, out tagSet)) + if (string.IsNullOrEmpty(tags)) + { + counter.LastValue = payload.Value; + } + else { - counter.TagSets[tags] = tagSet = new ObservedTagSet(tags); - _maxNameLength = Math.Max(_maxNameLength, tagSet.DisplayTags.Length); + if (!counter.TagSets.TryGetValue(tags, out tagSet)) + { + counter.TagSets[tags] = tagSet = new ObservedTagSet(tags); + redraw = true; + } + else + { + tagSet.LastDelta = payload.Value - tagSet.LastValue; + } tagSet.LastValue = payload.Value; - redraw = true; } if (_console.WindowWidth != _consoleWidth || _console.WindowHeight != _consoleHeight) @@ -277,14 +261,17 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters { AssignRowsAndInitializeDisplay(); } - - int row = counter.RenderValueInline ? counter.Row : tagSet.Row; - if (row < 0) + else { - return; + if (tagSet != null) + { + IncrementalUpdateCounterValueRow(tagSet.Row, tagSet.LastValue, tagSet.LastDelta.Value); + } + else + { + IncrementalUpdateCounterValueRow(counter.Row, counter.LastValue, counter.LastDelta.Value); + } } - _console.SetCursorPosition(Indent + _maxNameLength + 1, row); - _console.Write(FormatValue(payload.Value)); } } @@ -342,6 +329,81 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters return lineCount; } + private bool RenderCounterValueRow(ref int row, int indentLevel, string name, double value, double? delta) + { + // if you change line formatting, keep it in sync with IncrementaUpdateCounterValueRow below + string deltaText = delta.HasValue ? "" : FormatValue(delta.Value); + return RenderTableRow(ref row, $"{new string(' ', Indent * indentLevel)}{name}", FormatValue(value), deltaText); + } + + private bool RenderCounterNameRow(ref int row, string name) + { + return RenderTableRow(ref row, $"{new string(' ', Indent)}{name}"); + } + + private bool RenderTableRow(ref int row, string name, string value = null, string delta = null) + { + // if you change line formatting, keep it in sync with IncrementaUpdateCounterValueRow below + string nameCellText = MakeFixedWidth(name, _nameColumnWidth); + string valueCellText = MakeFixedWidth(value, CounterValueLength, alignRight: true); + string deltaCellText = MakeFixedWidth(delta, CounterValueLength, alignRight: true); + string lineText; + if (_showDeltaColumn) + { + lineText = $"{nameCellText} {valueCellText} {deltaCellText}"; + } + else + { + lineText = $"{nameCellText} {valueCellText}"; + } + return RenderRow(ref row, lineText); + } + + private bool RenderRow(ref int row, string text = null) + { + if (row >= _consoleHeight + _topRow) // prevents from displaying more counters than vertical space available + { + return false; + } + + if (text != null) + { + _console.Write(text); + } + + if (row < _consoleHeight + _topRow - 1) // prevents screen from scrolling due to newline on last line of console + { + _console.WriteLine(); + } + + row++; + + return true; + } + + private void IncrementalUpdateCounterValueRow(int row, double value, double delta) + { + // prevents from displaying more counters than vertical space available + if (row < 0 || row >= _consoleHeight + _topRow) + { + return; + } + + _console.SetCursorPosition(_nameColumnWidth + 1, row); + string valueCellText = MakeFixedWidth(FormatValue(value), CounterValueLength); + string deltaCellText = MakeFixedWidth(FormatValue(delta), CounterValueLength); + string partialLineText; + if (_showDeltaColumn) + { + partialLineText = $"{valueCellText} {deltaCellText}"; + } + else + { + partialLineText = $"{valueCellText}"; + } + _console.Write(partialLineText); + } + private static string FormatValue(double value) { string valueText; @@ -382,9 +444,13 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters return valueText; } - private static string MakeFixedWidth(string text, int width) + private static string MakeFixedWidth(string text, int width, bool alignRight = false) { - if (text.Length == width) + if (text == null) + { + return new string(' ', width); + } + else if (text.Length == width) { return text; } @@ -394,7 +460,14 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters } else { - return text += new string(' ', width - text.Length); + if (alignRight) + { + return new string(' ', width - text.Length) + text; + } + else + { + return text + new string(' ', width - text.Length); + } } } diff --git a/src/Tools/dotnet-counters/Program.cs b/src/Tools/dotnet-counters/Program.cs index d1fde42e6..da00ca078 100644 --- a/src/Tools/dotnet-counters/Program.cs +++ b/src/Tools/dotnet-counters/Program.cs @@ -49,7 +49,8 @@ namespace Microsoft.Diagnostics.Tools.Counters bool resumeRuntime, int maxHistograms, int maxTimeSeries, - TimeSpan duration); + TimeSpan duration, + bool showDeltas); private static Command MonitorCommand() => new( @@ -68,7 +69,8 @@ namespace Microsoft.Diagnostics.Tools.Counters ResumeRuntimeOption(), MaxHistogramOption(), MaxTimeSeriesOption(), - DurationOption() + DurationOption(), + ShowDeltasOption() }; private static Command CollectCommand() => @@ -207,6 +209,13 @@ namespace Microsoft.Diagnostics.Tools.Counters Argument = new Argument(name: "duration-timespan", getDefaultValue: () => default) }; + private static Option ShowDeltasOption() => + new( + alias: "--showDeltas", + description: @"Shows an extra column in the metrics table that displays the delta between the previous metric value and the most recent value." + + " This is useful to monitor the rate of change for a metric.") + { }; + private static readonly string[] s_SupportedRuntimeVersions = KnownData.s_AllVersions; public static int List(IConsole console, string runtimeVersion) diff --git a/src/tests/dotnet-counters/CSVExporterTests.cs b/src/tests/dotnet-counters/CSVExporterTests.cs index db4c6ff35..9bd4b6e52 100644 --- a/src/tests/dotnet-counters/CSVExporterTests.cs +++ b/src/tests/dotnet-counters/CSVExporterTests.cs @@ -109,6 +109,47 @@ namespace DotnetCounters.UnitTests } } + // Starting in .NET 8 MetricsEventSource, Meter counter instruments report both rate of change and + // absolute value. Reporting rate in the UI was less useful for many counters than just seeing the raw + // value. Now dotnet-counters reports these counters as absolute values. + [Fact] + public void CounterReportsAbsoluteValuePostNet8() + { + string fileName = "CounterReportsAbsoluteValuePostNet8.csv"; + CSVExporter exporter = new(fileName); + exporter.Initialize(); + DateTime start = DateTime.Now; + for (int i = 0; i < 100; i++) + { + exporter.CounterPayloadReceived(new CounterRateAndValuePayload(new CounterMetadata("myProvider", "counter", null, null, null), "Counter One", string.Empty, string.Empty, rate:0, i, start + TimeSpan.FromSeconds(i)), false); + } + exporter.Stop(); + + Assert.True(File.Exists(fileName)); + + try + { + List lines = File.ReadLines(fileName).ToList(); + Assert.Equal(101, lines.Count); // should be 101 including the headers + + ValidateHeaderTokens(lines[0]); + + for (int i = 1; i < lines.Count; i++) + { + string[] tokens = lines[i].Split(','); + + Assert.Equal("myProvider", tokens[1]); + Assert.Equal($"Counter One (Count)", tokens[2]); + Assert.Equal("Metric", tokens[3]); + Assert.Equal((i - 1).ToString(), tokens[4]); + } + } + finally + { + File.Delete(fileName); + } + } + [Fact] public void CounterTest_SameMeterDifferentTagsPerInstrument() { diff --git a/src/tests/dotnet-counters/ConsoleExporterTests.cs b/src/tests/dotnet-counters/ConsoleExporterTests.cs index 4fae98070..381ccc570 100644 --- a/src/tests/dotnet-counters/ConsoleExporterTests.cs +++ b/src/tests/dotnet-counters/ConsoleExporterTests.cs @@ -36,6 +36,7 @@ namespace DotnetCounters.UnitTests console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "", + "Name Current Value", "[System.Runtime]", " % Time in GC since last GC (%) 12"); } @@ -51,6 +52,7 @@ namespace DotnetCounters.UnitTests console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "", + "Name Current Value", "[System.Runtime]", " Allocation Rate (B / 1 sec) 1,731"); } @@ -67,6 +69,7 @@ namespace DotnetCounters.UnitTests console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "", + "Name Current Value", "[System.Runtime]", " Allocation Rate (B / 1 sec) 1,731", "[Provider2]", @@ -86,6 +89,7 @@ namespace DotnetCounters.UnitTests console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "", + "Name Current Value", "[System.Runtime]", " % Time in GC since last GC (%) 12", " Allocation Rate (B / 1 sec) 1,731"); @@ -96,6 +100,7 @@ namespace DotnetCounters.UnitTests console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "", + "Name Current Value", "[System.Runtime]", " % Time in GC since last GC (%) 7", " Allocation Rate (B / 1 sec) 123,456"); @@ -114,6 +119,7 @@ namespace DotnetCounters.UnitTests console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "", + "Name Current Value", "[System.Runtime]", " % Time in GC since last GC (%) 12", " Allocation Rate (B / 1 sec) 1,731"); @@ -123,6 +129,7 @@ namespace DotnetCounters.UnitTests console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Paused", "", + "Name Current Value", "[System.Runtime]", " % Time in GC since last GC (%) 12", " Allocation Rate (B / 1 sec) 1,731"); @@ -133,6 +140,7 @@ namespace DotnetCounters.UnitTests console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Paused", "", + "Name Current Value", "[System.Runtime]", " % Time in GC since last GC (%) 12", " Allocation Rate (B / 1 sec) 1,731"); @@ -142,6 +150,7 @@ namespace DotnetCounters.UnitTests console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "", + "Name Current Value", "[System.Runtime]", " % Time in GC since last GC (%) 12", " Allocation Rate (B / 1 sec) 1,731"); @@ -153,6 +162,7 @@ namespace DotnetCounters.UnitTests console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "", + "Name Current Value", "[System.Runtime]", " % Time in GC since last GC (%) 1", " Allocation Rate (B / 1 sec) 2"); @@ -171,6 +181,7 @@ namespace DotnetCounters.UnitTests console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "", + "Name Current Value", "[System.Runtime]", " % Time in GC since last GC (%) 0.1", " Allocation Rate (B / 1 sec) 1,731", @@ -189,6 +200,7 @@ namespace DotnetCounters.UnitTests console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "", + "Name Current Value", "[System.Runtime]", " % Time in GC since last GC (%) 0.1", " Allocation Rate (B / 1 sec) 1,731"); @@ -205,6 +217,7 @@ namespace DotnetCounters.UnitTests console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "", + "Name Current Value", "[System.Runtime]", " ThisCounterHasAVeryLongNameTha 0.1"); } @@ -216,13 +229,14 @@ namespace DotnetCounters.UnitTests ConsoleWriter exporter = new ConsoleWriter(console); exporter.Initialize(); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=red", 0.1), false); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=blue", 87), false); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "size=1", 14), false); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "temp=hot", 160), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=red", 0.1), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=blue", 87), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "size=1", 14), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "temp=hot", 160), false); console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "", + "Name Current Value", "[Provider1]", " Counter1 ({widget} / 1 sec)", " color=blue 87", @@ -239,13 +253,14 @@ namespace DotnetCounters.UnitTests ConsoleWriter exporter = new ConsoleWriter(console); exporter.Initialize(); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=red", 0.1), false); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=blue,LongNameTag=ThisDoesNotFit,AnotherOne=Hi", 87), false); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "size=1", 14), false); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "temp=hot", 160), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=red", 0.1), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=blue,LongNameTag=ThisDoesNotFit,AnotherOne=Hi", 87), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "size=1", 14), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "temp=hot", 160), false); console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "", + "Name Current Value", "[Provider1]", " Counter1 ({widget} / 1 sec)", " color=blue,LongNameTag=Thi 87", @@ -258,17 +273,18 @@ namespace DotnetCounters.UnitTests [Fact] public void CountersAreTruncatedBeyondScreenHeight() { - MockConsole console = new MockConsole(50, 6); + MockConsole console = new MockConsole(50, 7); ConsoleWriter exporter = new ConsoleWriter(console); exporter.Initialize(); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=red", 0.1), false); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=blue", 87), false); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "size=1", 14), false); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "temp=hot", 160), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=red", 0.1), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=blue", 87), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "size=1", 14), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "temp=hot", 160), false); console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "", + "Name Current Value", "[Provider1]", " Counter1 ({widget} / 1 sec)", " color=blue 87"); @@ -281,16 +297,17 @@ namespace DotnetCounters.UnitTests ConsoleWriter exporter = new ConsoleWriter(console); exporter.Initialize(); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=red", 0.1), false); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=blue", 87), false); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "size=1", 14), false); - exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "temp=hot", 160), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=red", 0.1), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=blue", 87), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "size=1", 14), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "temp=hot", 160), false); exporter.SetErrorText("Uh-oh, a bad thing happened"); console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", " Status: Running", "Uh-oh, a bad thing happened", "", + "Name Current Value", "[Provider1]", " Counter1 ({widget} / 1 sec)", " color=blue 87", @@ -300,6 +317,118 @@ namespace DotnetCounters.UnitTests " temp=hot 160"); } + [Fact] + public void DeltaColumnDisplaysInitiallyEmpty() + { + MockConsole console = new MockConsole(64, 40); + ConsoleWriter exporter = new ConsoleWriter(console, showDeltaColumn:true); + exporter.Initialize(); + + exporter.CounterPayloadReceived(CreateIncrementingEventCounter("System.Runtime", "Allocation Rate", "B", 1731), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=red", 0.1), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=blue", 87), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "size=1", 14), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "temp=hot", 160), false); + + console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", + " Status: Running", + "", + "Name Current Value Last Delta", + "[System.Runtime]", + " Allocation Rate (B / 1 sec) 1,731", + "[Provider1]", + " Counter1 ({widget} / 1 sec)", + " color=blue 87", + " color=red 0.1", + " Counter2 ({widget} / 1 sec)", + " size=1 14", + " temp=hot 160"); + } + + [Fact] + public void DeltaColumnDisplaysNumbersAfterUpdate() + { + MockConsole console = new MockConsole(64, 40); + ConsoleWriter exporter = new ConsoleWriter(console, showDeltaColumn: true); + exporter.Initialize(); + + exporter.CounterPayloadReceived(CreateIncrementingEventCounter("System.Runtime", "Allocation Rate", "B", 1731), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=red", 0.1), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=blue", 87), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "size=1", 14), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "temp=hot", 160), false); + console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", + " Status: Running", + "", + "Name Current Value Last Delta", + "[System.Runtime]", + " Allocation Rate (B / 1 sec) 1,731", + "[Provider1]", + " Counter1 ({widget} / 1 sec)", + " color=blue 87", + " color=red 0.1", + " Counter2 ({widget} / 1 sec)", + " size=1 14", + " temp=hot 160"); + + exporter.CounterPayloadReceived(CreateIncrementingEventCounter("System.Runtime", "Allocation Rate", "B", 1732), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=red", 0.2), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=blue", 87), false); + exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "size=1", 10), false); + console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", + " Status: Running", + "", + "Name Current Value Last Delta", + "[System.Runtime]", + " Allocation Rate (B / 1 sec) 1,732 1", + "[Provider1]", + " Counter1 ({widget} / 1 sec)", + " color=blue 87 0", + " color=red 0.2 0.1", + " Counter2 ({widget} / 1 sec)", + " size=1 10 -4", + " temp=hot 160"); + } + + // Starting in .NET 8 MetricsEventSource, Meter counter instruments report both rate of change and + // absolute value. Reporting rate in the UI was less useful for many counters than just seeing the raw + // value. Now dotnet-counters reports these counters as absolute by default and the optional delta column + // is available for folks who still want to visualize rate of change. + [Fact] + public void MeterCounterIsAbsoluteInNet8() + { + MockConsole console = new MockConsole(64, 40); + ConsoleWriter exporter = new ConsoleWriter(console, showDeltaColumn: true); + exporter.Initialize(); + + exporter.CounterPayloadReceived(CreateMeterCounterPostNet8("Provider1", "Counter1", "{widget}", "color=red", 0.1), false); + exporter.CounterPayloadReceived(CreateMeterCounterPostNet8("Provider1", "Counter1", "{widget}", "color=blue", 87), false); + exporter.CounterPayloadReceived(CreateMeterCounterPostNet8("Provider1", "Counter2", "{widget}", "", 14), false); + + console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", + " Status: Running", + "", + "Name Current Value Last Delta", + "[Provider1]", + " Counter1 ({widget})", // There is no longer (unit / 1 sec) here + " color=blue 87", + " color=red 0.1", + " Counter2 ({widget}) 14"); + + exporter.CounterPayloadReceived(CreateMeterCounterPostNet8("Provider1", "Counter1", "{widget}", "color=red", 0.2), false); + exporter.CounterPayloadReceived(CreateMeterCounterPostNet8("Provider1", "Counter1", "{widget}", "color=blue", 87), false); + exporter.CounterPayloadReceived(CreateMeterCounterPostNet8("Provider1", "Counter2", "{widget}", "", 10), false); + + console.AssertLinesEqual("Press p to pause, r to resume, q to quit.", + " Status: Running", + "", + "Name Current Value Last Delta", + "[Provider1]", + " Counter1 ({widget})", // There is no longer (unit / 1 sec) here + " color=blue 87 0", + " color=red 0.2 0.1", + " Counter2 ({widget}) 10 -4"); + } private static CounterPayload CreateEventCounter(string provider, string displayName, string unit, double value) @@ -312,9 +441,14 @@ namespace DotnetCounters.UnitTests return new EventCounterPayload(DateTime.MinValue, provider, displayName, displayName, unit, value, CounterType.Rate, 0, 1, ""); } - private static CounterPayload CreateMeterCounter(string meterName, string instrumentName, string unit, string tags, double value) + private static CounterPayload CreateMeterCounterPreNet8(string meterName, string instrumentName, string unit, string tags, double value) { return new RatePayload(new CounterMetadata(meterName, instrumentName, null, null, null), instrumentName, unit, tags, value, 1, DateTime.MinValue); } + + private static CounterPayload CreateMeterCounterPostNet8(string meterName, string instrumentName, string unit, string tags, double value) + { + return new CounterRateAndValuePayload(new CounterMetadata(meterName, instrumentName, null, null, null), instrumentName, unit, tags, rate:double.NaN, value, DateTime.MinValue); + } } } diff --git a/src/tests/dotnet-counters/JSONExporterTests.cs b/src/tests/dotnet-counters/JSONExporterTests.cs index 3660612dc..bcb47f9d3 100644 --- a/src/tests/dotnet-counters/JSONExporterTests.cs +++ b/src/tests/dotnet-counters/JSONExporterTests.cs @@ -319,6 +319,42 @@ namespace DotnetCounters.UnitTests } } } + + [Fact] + public void CounterReportsAbsoluteValuePostNet8() + { + // Starting in .NET 8 MetricsEventSource, Meter counter instruments report both rate of change and + // absolute value. Reporting rate in the UI was less useful for many counters than just seeing the raw + // value. Now dotnet-counters reports these counters as absolute values. + + string fileName = "counterReportsAbsoluteValuePostNet8.json"; + JSONExporter exporter = new(fileName, "myProcess.exe"); + exporter.Initialize(); + DateTime start = DateTime.Now; + for (int i = 0; i < 20; i++) + { + exporter.CounterPayloadReceived(new CounterRateAndValuePayload(new CounterMetadata("myProvider", "heapSize", null, null, null), "Heap Size", "MB", string.Empty, rate: 0, i, start + TimeSpan.FromSeconds(i)), false); + } + exporter.Stop(); + + Assert.True(File.Exists(fileName)); + using (StreamReader r = new(fileName)) + { + string json = r.ReadToEnd(); + JSONCounterTrace counterTrace = JsonConvert.DeserializeObject(json); + Assert.Equal("myProcess.exe", counterTrace.targetProcess); + Assert.Equal(20, counterTrace.events.Length); + int i = 0; + foreach (JSONCounterPayload payload in counterTrace.events) + { + Assert.Equal("myProvider", payload.provider); + Assert.Equal("Heap Size (MB)", payload.name); + Assert.Equal("Metric", payload.counterType); + Assert.Equal(i, payload.value); + i += 1; + } + } + } } internal class JSONCounterPayload diff --git a/src/tests/dotnet-counters/MockConsole.cs b/src/tests/dotnet-counters/MockConsole.cs index acc5aa140..57d41fc13 100644 --- a/src/tests/dotnet-counters/MockConsole.cs +++ b/src/tests/dotnet-counters/MockConsole.cs @@ -114,8 +114,15 @@ namespace DotnetCounters.UnitTests public void AssertLinesEqual(int startLine, params string[] expectedLines) { - for(int i = 0; i < expectedLines.Length; i++) + if (startLine + expectedLines.Length > Lines.Length) { + Assert.Fail("MockConsole output had fewer output lines than expected." + Environment.NewLine + + $"Expected line count: {expectedLines.Length}" + Environment.NewLine + + $"Actual line count: {Lines.Length}"); + } + for (int i = 0; i < expectedLines.Length; i++) + { + string actualLine = GetLineText(startLine+i); string expectedLine = expectedLines[i]; if(actualLine != expectedLine)