initial pull request for dotnet-trace report (topN feature) (#2705)
authormikelle-rogers <45022607+mikelle-rogers@users.noreply.github.com>
Wed, 1 Dec 2021 08:14:17 +0000 (01:14 -0700)
committerGitHub <noreply@github.com>
Wed, 1 Dec 2021 08:14:17 +0000 (00:14 -0800)
src/Tools/dotnet-trace/CommandLine/Commands/ReportCommand.cs [new file with mode: 0644]
src/Tools/dotnet-trace/CommandLine/PrintReportHelper.cs [new file with mode: 0644]
src/Tools/dotnet-trace/Program.cs

diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/ReportCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/ReportCommand.cs
new file mode 100644 (file)
index 0000000..c64540e
--- /dev/null
@@ -0,0 +1,156 @@
+using Microsoft.Tools.Common;
+using System;
+using System.Collections.Generic;
+using System.CommandLine;
+using System.CommandLine.Binding;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Diagnostics.Tracing.Etlx;
+using Microsoft.Diagnostics.Symbols;
+using Microsoft.Diagnostics.Tracing.Stacks;
+using Microsoft.Diagnostics.Tracing;
+using Diagnostics.Tracing.StackSources;
+using Microsoft.Diagnostics.Tools.Trace.CommandLine;
+
+namespace Microsoft.Diagnostics.Tools.Trace 
+{
+    internal static class ReportCommandHandler 
+    {
+        static List<string> unwantedMethodNames = new List<string>() { "ROOT", "Process"};
+
+        //Create an extension function to help 
+        public static List<CallTreeNodeBase> ByIDSortedInclusiveMetric(this CallTree callTree) 
+        {
+            var ret = new List<CallTreeNodeBase>(callTree.ByID);
+            ret.Sort((x, y) => Math.Abs(y.InclusiveMetric).CompareTo(Math.Abs(x.InclusiveMetric)));
+            return ret;
+        }
+        delegate Task<int> ReportDelegate(CancellationToken ct, IConsole console, string traceFile);
+        private static Task<int> Report(CancellationToken ct, IConsole console, string traceFile)
+        {
+            Console.Error.WriteLine("Error: subcommand was not provided. Available subcommands:");
+            Console.Error.WriteLine("    topN: Finds the top N methods on the callstack the longest.");
+            return Task.FromResult(-1);
+        }
+
+        delegate Task<int> TopNReportDelegate(CancellationToken ct, IConsole console, string traceFile, int n, bool inclusive, bool verbose);
+        private static async Task<int> TopNReport(CancellationToken ct, IConsole console, string traceFile, int number, bool inclusive, bool verbose) 
+        {          
+            try 
+            {
+                string tempEtlxFilename = TraceLog.CreateFromEventPipeDataFile(traceFile);
+                int count = 0;
+                int index = 0;
+                List<CallTreeNodeBase> nodesToReport = new List<CallTreeNodeBase>();
+                using (var symbolReader = new SymbolReader(System.IO.TextWriter.Null) { SymbolPath = SymbolPath.MicrosoftSymbolServerPath })
+                using (var eventLog = new TraceLog(tempEtlxFilename))
+                {
+                    var stackSource = new MutableTraceEventStackSource(eventLog)
+                    {
+                        OnlyManagedCodeStacks = true
+                    };
+
+                    var computer = new SampleProfilerThreadTimeComputer(eventLog,symbolReader);
+
+                    computer.GenerateThreadTimeStacks(stackSource);
+
+                    FilterParams filterParams = new FilterParams()
+                    {
+                        FoldRegExs = "CPU_TIME;UNMANAGED_CODE_TIME;{Thread (}",
+                    };
+                    FilterStackSource filterStack = new FilterStackSource(filterParams, stackSource, ScalingPolicyKind.ScaleToData);
+                    CallTree callTree = new(ScalingPolicyKind.ScaleToData);
+                    callTree.StackSource = filterStack;
+
+                    List<CallTreeNodeBase> callTreeNodes = null;
+
+                    if(!inclusive)
+                    {
+                        callTreeNodes = callTree.ByIDSortedExclusiveMetric();
+                    }
+                    else
+                    {
+                        callTreeNodes = callTree.ByIDSortedInclusiveMetric();
+                    }
+
+                    int totalElements = callTreeNodes.Count;
+                        while(count < number && index < totalElements)
+                        {
+                            CallTreeNodeBase node = callTreeNodes[index];
+                            index++;
+                            if(!unwantedMethodNames.Any(node.Name.Contains))
+                            {
+                                nodesToReport.Add(node);
+                                count++;
+                            }
+                        }
+
+                    PrintReportHelper.TopNWriteToStdOut(nodesToReport, inclusive, verbose);
+                }
+                return await Task.FromResult(0);
+            }
+            catch(Exception ex)
+            {
+                Console.Error.WriteLine($"[ERROR] {ex.ToString()}");
+            }
+
+            return await Task.FromResult(0);
+        }
+
+        public static Command ReportCommand() =>
+            new Command(
+                name: "report",
+                description: "Generates a report into stdout from a previously generated trace.")
+                {
+                    //Handler
+                    HandlerDescriptor.FromDelegate((ReportDelegate)Report).GetCommandHandler(),
+                    //Options
+                    FileNameArgument(),
+                    new Command(
+                        name: "topN",
+                        description: "Finds the top N methods that have been on the callstack the longest.")
+                        {
+                            //Handler
+                            HandlerDescriptor.FromDelegate((TopNReportDelegate)TopNReport).GetCommandHandler(),
+                            TopNOption(),
+                            InclusiveOption(),
+                            VerboseOption(),
+                        }
+                };
+
+        private static Argument<string> FileNameArgument() =>
+            new Argument<string>("trace_filename")
+            {
+                Name = "tracefile",
+                Description = "The file path for the trace being analyzed.",
+                Arity = new ArgumentArity(1, 1)
+            };
+
+        private static Option TopNOption()
+        {
+            return new Option(
+                aliases: new[] {"-n", "--number" },
+                description: $"Gives the top N methods on the callstack.")
+                {
+                    Argument = new Argument<int>(name: "n", getDefaultValue: () => 5)
+                };
+        }         
+
+        private static Option InclusiveOption() =>
+            new Option(
+                aliases: new[] { "--inclusive" },
+                description: $"Output the top N methods based on inclusive time. If not specified, exclusive time is used by default.")
+                {
+                    Argument = new Argument<bool>(name: "inclusive", getDefaultValue: () => false)
+                };
+
+        private static Option VerboseOption() =>
+            new Option(
+                aliases: new[] {"-v", "--verbose"},
+                description: $"Output the parameters of each method in full. If not specified, parameters will be truncated.")
+                {
+                    Argument = new Argument<bool>(name: "verbose", getDefaultValue: () => false)
+                };
+    }
+}
\ No newline at end of file
diff --git a/src/Tools/dotnet-trace/CommandLine/PrintReportHelper.cs b/src/Tools/dotnet-trace/CommandLine/PrintReportHelper.cs
new file mode 100644 (file)
index 0000000..caa3d5d
--- /dev/null
@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.Diagnostics.Tracing.Stacks;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.Diagnostics.Tools.Trace.CommandLine
+{
+    internal static class PrintReportHelper
+    {
+        private static string MakeFixedWidth(string text, int width)
+        {
+            if(text.Length == width)
+            {
+                return text;
+            }
+            else if(text.Length > width)
+            {
+                return text.Substring(0, width);
+            }
+            else
+            {
+                return text += new string(' ', width - text.Length);
+            }
+        }
+
+        private static string FormatFunction(string name)
+        {
+            string classMethod;
+            Regex nameRx = new Regex(@"(.*\..*)(\(.*\))");
+            Match match = nameRx.Match(name);
+            string functionList = match.Groups[1].Value;
+            string arguments = match.Groups[2].Value;
+            string[] usingStatement = functionList.Split(".");
+            int length = usingStatement.Length;
+
+            if (length < 2) 
+            {
+                if (length == 1)
+                {
+                    classMethod = usingStatement[length - 1];
+                }
+                else 
+                {
+                    classMethod = usingStatement[length];
+                }
+            }
+            else
+            {
+                classMethod = usingStatement[length - 2] + "." + usingStatement[length - 1];
+            }
+            return classMethod + arguments;
+        }
+
+        public static List<string> SplitInto(string str, int n)
+        {
+            int length = str.Length;
+            if(length < n)
+            {
+                string shortName = MakeFixedWidth(str, n);
+
+                return new List<string> {shortName};
+            }
+
+            if (String.IsNullOrEmpty(str) || n < 1)
+            {
+                throw new ArgumentException();
+            }
+            IEnumerable<string> uniformName = Enumerable.Range(0, length / n).Select(i => str.Substring(i * n, n));
+            List<string> strList = uniformName.ToList<string>();
+            int remainder = (length / n)*n; 
+            strList.Add(str.Substring(remainder, length - remainder));
+            return strList;
+        }
+
+
+        internal static void TopNWriteToStdOut(List<CallTreeNodeBase> nodesToReport, bool isInclusive, bool isVerbose) 
+        {
+            const int functionColumnWidth = 70;
+            const int measureColumnWidth = 20;
+            string measureType = null;
+            if (isInclusive)
+            {
+                measureType = "Inclusive";
+            }
+            else
+            {
+                measureType = "Exclusive";
+            }
+
+            int n = nodesToReport.Count;
+            int maxDigit = n.ToString().Count();
+            string extra = new string(' ', maxDigit - 1);
+
+            string header = "Top " + n.ToString() + " Functions (" + measureType + ")";
+            string uniformHeader = MakeFixedWidth(header, functionColumnWidth+7);
+
+            string inclusive = "Inclusive";
+            string uniformInclusive = MakeFixedWidth(inclusive, measureColumnWidth);
+
+            string exclusive = "Exclusive";
+            string uniformExclusive = MakeFixedWidth(exclusive, measureColumnWidth);
+            Console.WriteLine(uniformHeader + extra + uniformInclusive + uniformExclusive);
+
+            int numLines;
+            for(int i = 0; i < n; i++)
+            {
+
+                int iLength = (i+1).ToString().Count();
+                int numSpace = maxDigit - iLength + 1;
+
+                CallTreeNodeBase node = nodesToReport[i];
+                string name = node.Name;
+                string formatName = FormatFunction(name);
+                List<string> nameList = SplitInto(formatName, functionColumnWidth);
+
+                if(isVerbose)
+                {
+                    numLines = nameList.Count;
+                }
+                else
+                {
+                    numLines = 1;
+                }
+
+                for(int j = 0; j < numLines; j++)
+                {
+                    string inclusiveMeasure = "";
+                    string exclusiveMeasure = "";
+                    string number = new string(' ', maxDigit + 2); //+2 lines 130 and 137 account for '. '
+
+                    if(j == 0)
+                    {
+                        inclusiveMeasure = Math.Round(node.InclusiveMetricPercent, 2).ToString() + "%";
+                        exclusiveMeasure = Math.Round(node.ExclusiveMetricPercent, 2).ToString() + "%";
+                        number = (i + 1).ToString() + "." + number.Substring(maxDigit - numSpace + 2);
+                    }
+
+                    string uniformIMeasure = MakeFixedWidth(inclusiveMeasure, measureColumnWidth).PadLeft(measureColumnWidth+4);
+                    string uniformEMeasure = MakeFixedWidth(exclusiveMeasure, measureColumnWidth);
+                    Console.WriteLine(number + nameList[j] + uniformIMeasure + uniformEMeasure);
+                }
+                
+            }
+        }
+    }
+}
\ No newline at end of file
index 2b7f538155a8f96ad6b820004609a2f10cf576ab..d99260580aa1e1c21aad0a9b41ebedd0d130971a 100644 (file)
@@ -20,6 +20,7 @@ namespace Microsoft.Diagnostics.Tools.Trace
                 .AddCommand(ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that traces can be collected"))
                 .AddCommand(ListProfilesCommandHandler.ListProfilesCommand())
                 .AddCommand(ConvertCommandHandler.ConvertCommand())
+                .AddCommand(ReportCommandHandler.ReportCommand())
                 .UseDefaults()
                 .Build();
             ParseResult parseResult = parser.Parse(args);