From 836539dc4f5c9996ab9a5c00b0799c00bb1e9620 Mon Sep 17 00:00:00 2001 From: Hans Olav Norheim <33402265+hans-olav@users.noreply.github.com> Date: Fri, 23 Aug 2019 15:36:31 -0700 Subject: [PATCH] Improve output formatting and fix bugs in dotnet-counters (#443) This change improves output formatting in dotnet-counters by: - Right aligning counter values. - Decimal numbers are aligned on the decimal separator. - Introducing thousand separators. It fixes the following bug: - If monitoring multiple providers (for example System.Runtime and a user-defined one), the provider and counter names get mixed up in the output. I simplified the logic in ConsoleWriter.cs a great deal with these changes, so it has 63 fewer lines than before now. --- src/Tools/dotnet-counters/ConsoleWriter.cs | 223 +++++++------------- src/Tools/dotnet-counters/CounterMonitor.cs | 2 +- src/Tools/dotnet-counters/CounterPayload.cs | 14 +- 3 files changed, 88 insertions(+), 151 deletions(-) diff --git a/src/Tools/dotnet-counters/ConsoleWriter.cs b/src/Tools/dotnet-counters/ConsoleWriter.cs index 0f2a5eaf4..0714529c3 100644 --- a/src/Tools/dotnet-counters/ConsoleWriter.cs +++ b/src/Tools/dotnet-counters/ConsoleWriter.cs @@ -4,67 +4,69 @@ using System; using System.Collections.Generic; - +using System.Linq; namespace Microsoft.Diagnostics.Tools.Counters { public class ConsoleWriter { - private Dictionary displayPosition; // Display position (x-y coordiates) of each counter values. - private Dictionary displayLength; // Length of the counter values displayed for each counter. - private int origRow; - private int origCol; - private int maxRow; // Running maximum of row number - private int maxCol; // Running maximum of col number + /// Information about an observed provider. + private class ObservedProvider + { + public ObservedProvider(string name) + { + Name = name; + KnownData.TryGetProvider(name, out KnownProvider); + } + + public string Name { get; } // Name of the category. + public Dictionary Counters { get; } = new Dictionary(); // Counters in this category. + public readonly CounterProvider KnownProvider; + } + + /// Information about an observed counter. + private class ObservedCounter + { + public ObservedCounter(string displayName) => DisplayName = displayName; + public string DisplayName { get; } // Display name for this counter. + public int Row { get; set; } // Assigned row for this counter. May change during operation. + } + + private readonly Dictionary providers = new Dictionary(); // Tracks observed providers and counters. + private const int Indent = 4; // Counter name indent size. + private int maxNameLength = 40; // Allow room for 40 character counter names by default. + private int maxPreDecimalDigits = 11; // Allow room for values up to 999 million by default. + private int STATUS_ROW; // Row # of where we print the status of dotnet-counters - private int leftAlign; private bool paused = false; private bool initialized = false; - private Dictionary knownProvidersRowNum; - private Dictionary unknownProvidersRowNum; - private void UpdateStatus(string msg) + private void UpdateStatus() { Console.SetCursorPosition(0, STATUS_ROW); - Console.Write(new String(' ', 42)); // Length of the initial string we print on the console.. - Console.SetCursorPosition(0, STATUS_ROW); - Console.Write(msg); - Console.SetCursorPosition(maxRow, maxCol); + Console.Write($" Status: {GetStatus()}{new string(' ', 40)}"); // Write enough blanks to clear previous status. } - public ConsoleWriter() + private string GetStatus() => !initialized ? "Waiting for initial payload..." : (paused ? "Paused" : "Running"); + + /// Clears display and writes out category and counter name layout. + public void AssignRowsAndInitializeDisplay() { - displayPosition = new Dictionary(); - displayLength = new Dictionary(); - knownProvidersRowNum = new Dictionary(); - unknownProvidersRowNum = new Dictionary(); + Console.Clear(); + int row = Console.CursorTop; + Console.WriteLine("Press p to pause, r to resume, q to quit."); row++; + Console.WriteLine($" Status: {GetStatus()}"); STATUS_ROW = row++; + Console.WriteLine(); row++; // Blank line. - int maxNameWidth = -1; - foreach(CounterProvider provider in KnownData.GetAllProviders()) + foreach (ObservedProvider provider in providers.Values.OrderBy(p => p.KnownProvider == null).ThenBy(p => p.Name)) // Known providers first. { - foreach(CounterProfile counterProfile in provider.GetAllCounters()) + Console.WriteLine($"[{provider.Name}]"); row++; + foreach (ObservedCounter counter in provider.Counters.Values.OrderBy(c => c.DisplayName)) { - if (counterProfile.DisplayName.Length > maxNameWidth) - { - maxNameWidth = counterProfile.DisplayName.Length; - } + Console.WriteLine($"{new string(' ', Indent)}{counter.DisplayName}"); + counter.Row = row++; } - knownProvidersRowNum[provider.Name] = -1; } - leftAlign = maxNameWidth + 15; - } - - public void InitializeDisplay() - { - Console.Clear(); - origRow = Console.CursorTop; - origCol = Console.CursorLeft; - Console.WriteLine("Press p to pause, r to resume, q to quit."); - Console.WriteLine(" Status: Waiting for initial payload..."); - - STATUS_ROW = origRow+1; - maxRow = origRow+2; - maxCol = origCol; } public void ToggleStatus(bool pauseCmdSet) @@ -73,129 +75,64 @@ namespace Microsoft.Diagnostics.Tools.Counters { return; } - else if (pauseCmdSet) - { - UpdateStatus(" Status: Paused"); - } - else - { - UpdateStatus(" Status: Running"); - } - paused = pauseCmdSet; - } - // Generates a string using providerName and counterName that can be used as a dictionary key to prevent key collision - private string CounterNameString(string providerName, string counterName) - { - return $"{providerName}:{counterName}"; + paused = pauseCmdSet; + UpdateStatus(); } public void Update(string providerName, ICounterPayload payload, bool pauseCmdSet) { - if (!initialized) { initialized = true; - UpdateStatus(" Status: Running"); + AssignRowsAndInitializeDisplay(); } if (pauseCmdSet) { return; } + string name = payload.GetName(); - string keyName = CounterNameString(providerName, name); - const string indent = " "; - const int indentLength = 4; - // We already know what this counter is! Just update the value string on the console. - if (displayPosition.ContainsKey(keyName)) + + bool redraw = false; + if (!providers.TryGetValue(providerName, out ObservedProvider provider)) { - (int left, int row) = displayPosition[keyName]; - string payloadVal = payload.GetValue(); + providers[providerName] = provider = new ObservedProvider(providerName); + redraw = true; + } - int clearLength = Math.Max(displayLength[keyName], payloadVal.Length); // Compute how long we need to clear out. - displayLength[keyName] = clearLength; - Console.SetCursorPosition(left, row); - Console.Write(new String(' ', clearLength)); + if (!provider.Counters.TryGetValue(name, out ObservedCounter counter)) + { + string displayName = provider.KnownProvider?.TryGetDisplayName(name) ?? payload.GetDisplay() ?? name; + provider.Counters[name] = counter = new ObservedCounter(displayName); + maxNameLength = Math.Max(maxNameLength, displayName.Length); + redraw = true; + } - if (left < leftAlign) - { - displayPosition[keyName] = (leftAlign, row); - Console.SetCursorPosition(leftAlign, row); - } - else - { - Console.SetCursorPosition(left, row); - } - Console.Write(payloadVal); + const string DecimalPlaces = "###"; + string payloadVal = payload.GetValue().ToString("#,0." + DecimalPlaces); + int decimalIndex = payloadVal.IndexOf('.'); + if (decimalIndex == -1) + { + decimalIndex = payloadVal.Length; } - // Got a payload from a new counter that hasn't been written to the console yet. - else + + if (decimalIndex > maxPreDecimalDigits) { - bool isWellKnownProvider = knownProvidersRowNum.ContainsKey(providerName); + maxPreDecimalDigits = decimalIndex; + redraw = true; + } - if (isWellKnownProvider) - { - if (knownProvidersRowNum[providerName] < 0) - { - knownProvidersRowNum[providerName] = maxRow + 1; - Console.SetCursorPosition(0, maxRow); - Console.WriteLine($"[{providerName}]"); - maxRow += 1; - } - - KnownData.TryGetProvider(providerName, out CounterProvider counterProvider); - string displayName = counterProvider.TryGetDisplayName(name); - - if (displayName == null) - { - displayName = payload.GetDisplay(); - } - - int left = displayName.Length; - string spaces = new String(' ', leftAlign-left-indentLength); - int row = maxRow; - string val = payload.GetValue(); - displayPosition[keyName] = (leftAlign, row); - displayLength[keyName] = val.Length; - Console.WriteLine($"{indent}{displayName}{spaces}{val}"); - maxRow += 1; - } - else - { - // If it's from an unknown provider, just append it at the end. - if (!unknownProvidersRowNum.ContainsKey(providerName)) - { - unknownProvidersRowNum[providerName] = maxRow + 1; - Console.SetCursorPosition(0, maxRow); - Console.WriteLine($"[{providerName}]"); - maxRow += 1; - } - - string displayName = payload.GetDisplay(); - - if (string.IsNullOrEmpty(displayName)) - { - displayName = payload.GetName(); - } - - int left = displayName.Length; - - // If counter name is exceeds position of counter values, move values to the right - if (left+indentLength+4 > leftAlign) // +4 so that the counter value does not start right where the counter name ends - { - leftAlign = left+indentLength+4; - } - - string spaces = new String(' ', leftAlign-left-indentLength); - int row = maxRow; - string val = payload.GetValue(); - displayPosition[keyName] = (leftAlign, row); - displayLength[keyName] = val.Length; - Console.WriteLine($"{indent}{displayName}{spaces}{val}"); - maxRow += 1; - } + if (redraw) + { + AssignRowsAndInitializeDisplay(); } + + Console.SetCursorPosition(Indent + maxNameLength + 1, counter.Row); + int prefixSpaces = maxPreDecimalDigits - decimalIndex; + int postfixSpaces = DecimalPlaces.Length - (payloadVal.Length - decimalIndex - 1); + Console.Write($"{new string(' ', prefixSpaces)}{payloadVal}{new string(' ', postfixSpaces)}"); } } } diff --git a/src/Tools/dotnet-counters/CounterMonitor.cs b/src/Tools/dotnet-counters/CounterMonitor.cs index 7d2048993..3649444b2 100644 --- a/src/Tools/dotnet-counters/CounterMonitor.cs +++ b/src/Tools/dotnet-counters/CounterMonitor.cs @@ -195,7 +195,7 @@ namespace Microsoft.Diagnostics.Tools.Counters _ct.Register(() => shouldExit.Set()); var terminated = false; - writer.InitializeDisplay(); + writer.AssignRowsAndInitializeDisplay(); Task monitorTask = new Task(() => { try diff --git a/src/Tools/dotnet-counters/CounterPayload.cs b/src/Tools/dotnet-counters/CounterPayload.cs index 9f2f70b11..2b1e4ddee 100644 --- a/src/Tools/dotnet-counters/CounterPayload.cs +++ b/src/Tools/dotnet-counters/CounterPayload.cs @@ -11,7 +11,7 @@ namespace Microsoft.Diagnostics.Tools.Counters public interface ICounterPayload { string GetName(); - string GetValue(); + double GetValue(); string GetDisplay(); } @@ -19,12 +19,12 @@ namespace Microsoft.Diagnostics.Tools.Counters class CounterPayload : ICounterPayload { public string m_Name; - public string m_Value; + public double m_Value; public string m_DisplayName; public CounterPayload(IDictionary payloadFields) { m_Name = payloadFields["Name"].ToString(); - m_Value = payloadFields["Mean"].ToString(); + m_Value = (double)payloadFields["Mean"]; m_DisplayName = payloadFields["DisplayName"].ToString(); } @@ -33,7 +33,7 @@ namespace Microsoft.Diagnostics.Tools.Counters return m_Name; } - public string GetValue() + public double GetValue() { return m_Value; } @@ -47,13 +47,13 @@ namespace Microsoft.Diagnostics.Tools.Counters class IncrementingCounterPayload : ICounterPayload { public string m_Name; - public string m_Value; + public double m_Value; public string m_DisplayName; public string m_DisplayRateTimeScale; public IncrementingCounterPayload(IDictionary payloadFields, int interval) { m_Name = payloadFields["Name"].ToString(); - m_Value = payloadFields["Increment"].ToString(); + m_Value = (double)payloadFields["Increment"]; m_DisplayName = payloadFields["DisplayName"].ToString(); m_DisplayRateTimeScale = payloadFields["DisplayRateTimeScale"].ToString(); @@ -67,7 +67,7 @@ namespace Microsoft.Diagnostics.Tools.Counters return m_Name; } - public string GetValue() + public double GetValue() { return m_Value; } -- 2.34.1