Make dotnet-counter stream values in real time (#197)
authorSung Yoon Whang <suwhang@microsoft.com>
Thu, 25 Apr 2019 22:50:00 +0000 (15:50 -0700)
committerGitHub <noreply@github.com>
Thu, 25 Apr 2019 22:50:00 +0000 (15:50 -0700)
* Streaming counters

* DisplayName stuff

* fix build

* few demo changes

* parsing command-line provided counter list works now

* No more string parsing!!!

* some more fixes in writer

* update license header, display rates

* Removing useless files

* Use known displaynames for known providers

* cleanup

* spaces

* more cleanup

* more cleanup

* PR feedback

src/Tools/dotnet-counters/ConsoleWriter.cs [new file with mode: 0644]
src/Tools/dotnet-counters/CounterMonitor.cs
src/Tools/dotnet-counters/CounterPayload.cs [new file with mode: 0644]
src/Tools/dotnet-counters/CounterProvider.cs
src/Tools/dotnet-counters/KnownData.cs
src/Tools/dotnet-counters/Program.cs
src/Tools/dotnet-counters/dotnet-counters.csproj
src/Versions.props

diff --git a/src/Tools/dotnet-counters/ConsoleWriter.cs b/src/Tools/dotnet-counters/ConsoleWriter.cs
new file mode 100644 (file)
index 0000000..4f2a50e
--- /dev/null
@@ -0,0 +1,96 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+
+
+namespace Microsoft.Diagnostics.Tools.Counters
+{
+    public class ConsoleWriter
+    {
+        private Dictionary<string, (int, int)> displayPosition; // Display position (x-y coordiates) of each counter values.
+        private int origRow;
+        private int origCol;
+        private int maxRow;  // Running maximum of row number
+        private int maxCol;  // Running maximum of col number
+        private Dictionary<string, int> knownProvidersRowNum;
+
+        public ConsoleWriter()
+        {
+            displayPosition = new Dictionary<string, (int, int)>();
+            knownProvidersRowNum = new Dictionary<string, int>();
+
+            foreach(CounterProvider provider in KnownData.GetAllProviders())
+            {
+                knownProvidersRowNum[provider.Name] = -1;
+            }
+        }
+
+        public void InitializeDisplay()
+        {
+            Console.Clear();
+            origRow = Console.CursorTop;
+            origCol = Console.CursorLeft;
+
+            maxRow = origRow;
+            maxCol = origCol;
+        }
+
+        public void Update(string providerName, ICounterPayload payload)
+        {
+            string name = payload.GetName();
+
+            // We already know what this counter is! Just update the value string on the console.
+            if (displayPosition.ContainsKey(name))
+            {
+                (int left, int row) = displayPosition[name];
+                Console.SetCursorPosition(left, row);
+                Console.Write(new String(' ', 16));
+
+                Console.SetCursorPosition(left, row);
+                Console.Write(payload.GetValue());  
+            }
+            // Got a payload from a new counter that hasn't been written to the console yet.
+            else
+            {
+                bool isWellKnownProvider = knownProvidersRowNum.ContainsKey(providerName);
+
+                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 + 7; // displayName + " : "
+                    int row = maxRow;
+                    displayPosition[name] = (left, row);
+                    Console.WriteLine($"    {displayName} : {payload.GetValue()}");
+                    maxRow += 1;
+                }
+                else
+                {
+                    // If it's from an provider, just append it at the end.
+                    string displayName = payload.GetDisplay();
+                    int left = displayName.Length + 7; // displayName + " : "
+                    int row = maxRow;
+                    displayPosition[name] = (left, row);
+                    Console.WriteLine($"    {displayName} : {payload.GetValue()}");
+                    maxRow += 1;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
index d911498a0a6fd77f1878f102dc28730a219b088f..892b5382fc0d29e6b9c05168dd0b5fece614b062 100644 (file)
@@ -1,37 +1,56 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
 
 using Microsoft.Diagnostics.Tools.RuntimeClient;
 using System;
+using System.Collections.Generic;
 using System.CommandLine;
 using System.IO;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 
+using Microsoft.Diagnostics.Tracing;
+
 namespace Microsoft.Diagnostics.Tools.Counters
 {
     public class CounterMonitor
     {
-        private string outputPath;
-        private ulong sessionId;
-
         private int _processId;
         private int _interval;
         private string _counterList;
         private CancellationToken _ct;
         private IConsole _console;
-
+        private ConsoleWriter writer;
+        private ulong _sessionId;
         public CounterMonitor()
         {
+            writer = new ConsoleWriter();
+        }
+
+        private void Dynamic_All(TraceEvent obj)
+        {
+            if (obj.EventName.Equals("EventCounters"))
+            {
+                IDictionary<string, object> payloadVal = (IDictionary<string, object>)(obj.PayloadValue(0));
+                IDictionary<string, object> payloadFields = (IDictionary<string, object>)(payloadVal["Payload"]);
+
+                // There really isn't a great way to tell whether an EventCounter payload is an instance of 
+                // IncrementingCounterPayload or CounterPayload, so here we check the number of fields 
+                // to distinguish the two.                
+                ICounterPayload payload = payloadFields["CounterType"] == "Sum" ? (ICounterPayload)new IncrementingCounterPayload(payloadFields) : (ICounterPayload)new CounterPayload(payloadFields);
+                
+                writer.Update(obj.ProviderName, payload);
+            }
         }
 
-        public async Task<int> Monitor(CancellationToken ct, string counterList, IConsole console, int processId, int interval)
+        public async Task<int> Monitor(CancellationToken ct, string counter_list, IConsole console, int processId, int interval)
         {
             try
             {
                 _ct = ct;
-                _counterList = counterList;
+                _counterList = counter_list; // NOTE: This variable name has an underscore because that's the "name" that the CLI displays. System.CommandLine doesn't like it if we change the variable to camelcase. 
                 _console = console;
                 _processId = processId;
                 _interval = interval;
@@ -43,11 +62,10 @@ namespace Microsoft.Diagnostics.Tools.Counters
             {
                 try
                 {
-                    EventPipeClient.StopTracing(_processId, sessionId);    
+                    EventPipeClient.StopTracing(_processId, _sessionId);    
                 }
                 catch (Exception) {} // Swallow all exceptions for now.
                 
-                console.Out.WriteLine($"Tracing stopped. Trace files written to {outputPath}");
                 console.Out.WriteLine($"Complete");
                 return 1;
             }
@@ -65,8 +83,6 @@ namespace Microsoft.Diagnostics.Tools.Counters
                 return 1;
             }
 
-            outputPath = Path.Combine(Directory.GetCurrentDirectory(), $"dotnet-counters-{_processId}.netperf"); // TODO: This can be removed once events can be streamed in real time.
-
             String providerString;
 
             if (string.IsNullOrEmpty(_counterList))
@@ -102,17 +118,24 @@ namespace Microsoft.Diagnostics.Tools.Counters
                 }
                 providerString = sb.ToString();
             }
-
-            var configuration = new SessionConfiguration(
-                circularBufferSizeMB: 1000,
-                outputPath: outputPath,
-                providers: Trace.Extensions.ToProviders(providerString));
-
-            sessionId = EventPipeClient.StartTracingToFile(_processId, configuration);
-
-            // Write the config file contents
-            _console.Out.WriteLine("Tracing has started. Press Ctrl-C to stop.");
-            await Task.Delay(int.MaxValue, _ct);
+            Task monitorTask = new Task(() => {
+                var configuration = new SessionConfiguration(
+                    circularBufferSizeMB: 1000,
+                    outputPath: "",
+                    providers: Trace.Extensions.ToProviders(providerString));
+
+                var binaryReader = EventPipeClient.CollectTracing(_processId, configuration, out _sessionId);
+                EventPipeEventSource source = new EventPipeEventSource(binaryReader);
+                writer.InitializeDisplay();
+                source.Dynamic.All += Dynamic_All;
+                source.Process();
+            });
+
+            monitorTask.Start();
+            await monitorTask;
+            EventPipeClient.StopTracing(_processId, _sessionId);
+
+            Task.FromResult(0);
             return 0;
         }
     }
diff --git a/src/Tools/dotnet-counters/CounterPayload.cs b/src/Tools/dotnet-counters/CounterPayload.cs
new file mode 100644 (file)
index 0000000..8a8e751
--- /dev/null
@@ -0,0 +1,76 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.Diagnostics.Tools.Counters
+{
+    public interface ICounterPayload
+    {
+        string GetName();
+        string GetValue();
+        string GetDisplay();
+    }
+
+
+    class CounterPayload : ICounterPayload
+    {
+        public string m_Name;
+        public string m_Value;
+        public string m_DisplayName;
+        public CounterPayload(IDictionary<string, object> payloadFields)
+        {
+            m_Name = payloadFields["Name"].ToString();
+            m_Value = payloadFields["Mean"].ToString();
+            m_DisplayName = payloadFields["DisplayName"].ToString();
+        }
+
+        public string GetName()
+        {
+            return m_Name;
+        }
+
+        public string GetValue()
+        {
+            return m_Value;
+        }
+
+        public string GetDisplay()
+        {
+            return m_DisplayName;
+        }
+    }
+
+    class IncrementingCounterPayload : ICounterPayload
+    {
+        public string m_Name;
+        public string m_Value;
+        public string m_DisplayName;
+        public string m_DisplayRateTimeScale;
+        public IncrementingCounterPayload(IDictionary<string, object> payloadFields)
+        {
+            m_Name = payloadFields["Name"].ToString();
+            m_Value = payloadFields["Increment"].ToString();
+            m_DisplayName = payloadFields["DisplayName"].ToString();
+            m_DisplayRateTimeScale = TimeSpan.Parse(payloadFields["DisplayRateTimeScale"].ToString()).ToString("%s' sec'");
+        }
+
+        public string GetName()
+        {
+            return m_Name;
+        }
+
+        public string GetValue()
+        {
+            return m_Value;
+        }
+
+        public string GetDisplay()
+        {
+            return $"{m_DisplayName} / {m_DisplayRateTimeScale}";
+        }
+    }
+}
index 5c6e62835ef2de917eba4b4b255f27374ebf3d75..e1b821b11651e4994e573abd48d8a9b3bd7171e1 100644 (file)
@@ -1,6 +1,8 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
 
+using System;
 using System.Collections.Generic;
 using System.Linq;
 
@@ -8,13 +10,11 @@ namespace Microsoft.Diagnostics.Tools.Counters
 {
     public class CounterProvider
     {
-        public static readonly string DefaultProviderName = "System.Runtime";
-
         public string Name { get; }
         public string Description { get; }
         public string Keywords { get; }
         public string Level { get; }
-        public IReadOnlyList<CounterProfile> Counters { get; }
+        public Dictionary<string, CounterProfile> Counters { get; }
 
         public CounterProvider(string name, string description, string keywords, string level, IEnumerable<CounterProfile> counters)
         {
@@ -22,7 +22,18 @@ namespace Microsoft.Diagnostics.Tools.Counters
             Description = description;
             Keywords = keywords;
             Level = level;
-            Counters = counters.ToList();
+            Counters = new Dictionary<string, CounterProfile>();
+            foreach (CounterProfile counter in counters)
+            {
+                Counters.Add(counter.Name, counter);
+            }
+        }
+
+        public string TryGetDisplayName(string counterName)
+        {
+            if (Counters.ContainsKey(counterName))
+                return Counters[counterName].DisplayName;
+            return null;
         }
 
         public string ToProviderString(int interval)
@@ -34,6 +45,7 @@ namespace Microsoft.Diagnostics.Tools.Counters
     public class CounterProfile
     {
         public string Name { get; set; }
+        public string DisplayName { get; set; }
         public string Description { get; set; }
     }
 }
index a8e50c5cea36dbd3acd889a0b88f4a8a0051deeb..a8ed85b07d2da0151c464bb4fe276a144659e1e5 100644 (file)
@@ -1,5 +1,6 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
 
 using System;
 using System.Collections.Generic;
@@ -21,20 +22,16 @@ namespace Microsoft.Diagnostics.Tools.Counters
                 "System.Runtime", // Name
                 "A default set of performance counters provided by the .NET runtime.", // Description
                 "0xffffffff", // Keywords
-                "0x5", // Level 
+                "5", // Level 
                 new[] { // Counters
-                    // NOTE: For now, the set of counters below doesn't really matter because 
-                    // we don't really display any counters in real time. (We just collect .netperf files) 
-                    // In the future (with IPC), we should filter counter payloads by name provided below to display.  
-                    // These are mainly here as placeholders. 
-                    new CounterProfile{ Name="total-processor-time", Description="Amount of time the process has utilized the CPU (ms)" },
-                    new CounterProfile{ Name="private-memory", Description="Amount of private virtual memory used by the process (KB)" },
-                    new CounterProfile{ Name="working-set", Description="Amount of working set used by the process (KB)" },
-                    new CounterProfile{ Name="virtual-memory", Description="Amount of virtual memory used by the process (KB)" },
-                    new CounterProfile{ Name="gc-total-memory", Description="Amount of committed virtual memory used by the GC (KB)" },
-                    new CounterProfile{ Name="exceptions-thrown-rate", Description="Number of exceptions thrown in a recent 1 minute window (exceptions/min)" },
+                    new CounterProfile{ Name="cpu-usage", Description="Amount of time the process has utilized the CPU (ms)", DisplayName="CPU Usage (%)" },
+                    new CounterProfile{ Name="working-set", Description="Amount of working set used by the process (MB)", DisplayName="Working Set (MB)" },
+                    new CounterProfile{ Name="gc-heap-size", Description="Total heap size reported by the GC (MB)", DisplayName="GC Heap Size (MB)" },
+                    new CounterProfile{ Name="gen-0-gc-count", Description="Number of Gen 0 GCs", DisplayName="Gen 0 GC / sec" },
+                    new CounterProfile{ Name="gen-1-gc-count", Description="Number of Gen 1 GCs", DisplayName="Gen 1 GC / sec" },
+                    new CounterProfile{ Name="gen-2-gc-count", Description="Number of Gen 2 GCs", DisplayName="Gen 2 GC / sec" },
+                    new CounterProfile{ Name="exception-count", Description="Number of Exceptions / Sec", DisplayName="Exceptions / sec" },
                 });
-
             // TODO: Add more providers (ex. ASP.NET ones)
         }
 
index 7d6a53c1540f331ab87416412f05202735269b2a..6ab5ff985343ebb372b02ff299a30022e1d32df4 100644 (file)
@@ -1,5 +1,6 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
 
 using System;
 using System.CommandLine;
@@ -41,7 +42,7 @@ namespace Microsoft.Diagnostics.Tools.Counters
                 Description = @"A space separated list of counters. Counters can be specified provider_name[:counter_name].
                 If the provider_name is used without a qualifying counter_name then all counters will be shown. To discover 
                 provider and counter names, use the list command.
-                ."
+                .",
             };
 
         private static Command ListCommand() =>
@@ -59,7 +60,7 @@ namespace Microsoft.Diagnostics.Tools.Counters
             foreach (var profile in profiles)
             {
                 Console.WriteLine($"* {profile.Name.PadRight(maxNameLength)}");
-                foreach (var counter in profile.Counters)
+                foreach (var counter in profile.Counters.Values)
                 {
                     Console.WriteLine($"    {counter.Name} \t\t {counter.Description}");
                 }
index b30daa60374243357a4c812a6d8f84d7a0ee3785..ceff9fafb15e20abde3895cc276c90b8aa9af4fd 100644 (file)
@@ -29,6 +29,7 @@
   <ItemGroup>
     <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostic.Repl\Microsoft.Diagnostic.Repl.csproj" />
     <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostics.Tools.RuntimeClient\Microsoft.Diagnostics.Tools.RuntimeClient.csproj" />
+    <PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="2.0.41" />
   </ItemGroup>
 
 </Project>
\ No newline at end of file
index 4d4f5c7178f0aee09bcf8aadc14db268273e30bc..140bd32e2ed0425d36804b39815fd8473561442f 100644 (file)
@@ -1,6 +1,6 @@
 <Project>
   <PropertyGroup>
-    <MicrosoftDiagnosticsTracingLibraryVersion>2.0.40</MicrosoftDiagnosticsTracingLibraryVersion>
+    <MicrosoftDiagnosticsTracingLibraryVersion>2.0.41</MicrosoftDiagnosticsTracingLibraryVersion>
     <SystemCommandLineExperimentalVersion>0.2.0-alpha.19213.1</SystemCommandLineExperimentalVersion>
     <SystemCommandLineRenderingVersion>0.2.0-alpha.19213.1</SystemCommandLineRenderingVersion>
   </PropertyGroup>