Improve output formatting and fix bugs in dotnet-counters (#443)
authorHans Olav Norheim <33402265+hans-olav@users.noreply.github.com>
Fri, 23 Aug 2019 22:36:31 +0000 (15:36 -0700)
committerGitHub <noreply@github.com>
Fri, 23 Aug 2019 22:36:31 +0000 (15:36 -0700)
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
src/Tools/dotnet-counters/CounterMonitor.cs
src/Tools/dotnet-counters/CounterPayload.cs

index 0f2a5eaf47cd87cfe94a3b0af4dd2ac7e3ed04d7..0714529c398103d56bcf802f2578db571d58b14f 100644 (file)
@@ -4,67 +4,69 @@
 
 using System;
 using System.Collections.Generic;
-
+using System.Linq;
 
 namespace Microsoft.Diagnostics.Tools.Counters
 {
     public class ConsoleWriter
     {
-        private Dictionary<string, (int, int)> displayPosition; // Display position (x-y coordiates) of each counter values.
-        private Dictionary<string, int> 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
+        /// <summary>Information about an observed provider.</summary>
+        private class ObservedProvider
+        {
+            public ObservedProvider(string name)
+            {
+                Name = name;
+                KnownData.TryGetProvider(name, out KnownProvider);
+            }
+
+            public string Name { get; } // Name of the category.
+            public Dictionary<string, ObservedCounter> Counters { get; } = new Dictionary<string, ObservedCounter>(); // Counters in this category.
+            public readonly CounterProvider KnownProvider;
+        }
+
+        /// <summary>Information about an observed counter.</summary>
+        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<string, ObservedProvider> providers = new Dictionary<string, ObservedProvider>(); // 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<string, int> knownProvidersRowNum;
-        private Dictionary<string, int> 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");
+
+        /// <summary>Clears display and writes out category and counter name layout.</summary>
+        public void AssignRowsAndInitializeDisplay()
         {
-            displayPosition = new Dictionary<string, (int, int)>();
-            displayLength = new Dictionary<string, int>();
-            knownProvidersRowNum = new Dictionary<string, int>();
-            unknownProvidersRowNum = new Dictionary<string, int>();
+            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)}");
         }
     }
 }
index 7d20489931ab3ab48181bd3c476e5226cd810c02..3649444b2db35447c90eb4b137e1c60d78747bc2 100644 (file)
@@ -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
index 9f2f70b11b62460d11fd7194e88360983cae8d63..2b1e4ddee7c11aa449be2e162b5c2f4882f8c100 100644 (file)
@@ -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<string, object> 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<string, object> 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;
         }