diff --git a/documentation/dotnet-trace-instructions.md b/documentation/dotnet-trace-instructions.md index 62ba14e752..db623c5962 100644 --- a/documentation/dotnet-trace-instructions.md +++ b/documentation/dotnet-trace-instructions.md @@ -175,6 +175,42 @@ Options: KeyValueArgs format: '[key1=value1][;key2=value2]' note: values that contain ';' or '=' characters should be surrounded by double quotes ("), e.g., 'key="value;with=symbols";key2=value2' + --clrevents + List of CLR events to collect. + + The string should be in the format '[Keyword1]+[Keyword2]+...+[KeywordN]'. For example: --clrevents GC+GCHandle + + List of CLR event keywords: + * GC + * GCHandle + * Fusion + * Loader + * JIT + * NGEN + * StartEnumeration + * EndEnumeration + * Security + * AppDomainResourceManagement + * JITTracing + * Interop + * Contention + * Exception + * Threading + * JittedMethodILToNativeMap + * OverrideAndSuppressNGENEvents + * Type + * GCHeapDump + * GCSampledObjectAllocationHigh + * GCHeapSurvivalAndMovement + * GCHeapCollect + * GCHeapAndTypeNames + * GCSampledObjectAllocationLow + * PerfTrack + * Stack + * ThreadTransfer + * Debugger + + --buffersize Sets the size of the in-memory circular buffer in megabytes. Default 256 MB. diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index b28eab6e28..fc7f6f50b7 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -19,7 +19,7 @@ namespace Microsoft.Diagnostics.Tools.Trace { internal static class CollectCommandHandler { - delegate Task CollectDelegate(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration); + delegate Task CollectDelegate(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel); /// /// Collects a diagnostic trace from a currently running process. @@ -32,8 +32,11 @@ internal static class CollectCommandHandler /// A list of EventPipe providers to be enabled. This is in the form 'Provider[,Provider]', where Provider is in the form: 'KnownProviderName[:Flags[:Level][:KeyValueArgs]]', and KeyValueArgs is in the form: '[key1=value1][;key2=value2]' /// A named pre-defined set of provider configurations that allows common tracing scenarios to be specified succinctly. /// The desired format of the created trace file. + /// The duration of trace to be taken. + /// A list of CLR events to be emitted. + /// The verbosity level of CLR events /// - private static async Task Collect(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration) + private static async Task Collect(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel) { try { @@ -56,7 +59,7 @@ private static async Task Collect(CancellationToken ct, IConsole console, i return ErrorCodes.ArgumentError; } - if (profile.Length == 0 && providers.Length == 0) + if (profile.Length == 0 && providers.Length == 0 && clrevents.Length == 0) { Console.Out.WriteLine("No profile or providers specified, defaulting to trace profile 'cpu-sampling'"); profile = "cpu-sampling"; @@ -83,6 +86,22 @@ private static async Task Collect(CancellationToken ct, IConsole console, i Profile.MergeProfileAndProviders(selectedProfile, providerCollection, enabledBy); } + // Parse --clrevents parameter + if (clrevents.Length != 0) + { + // Ignore --clrevents if CLR event provider was already specified via --profile or --providers command. + if (enabledBy.ContainsKey(Extensions.CLREventProviderName)) + { + Console.WriteLine($"The argument --clrevents {clrevents} will be ignored because the CLR provider was configured via either --profile or --providers command."); + } + else + { + var clrProvider = Extensions.ToCLREventPipeProvider(clrevents, clreventlevel); + providerCollection.Add(clrProvider); + enabledBy[Extensions.CLREventProviderName] = "--clrevents"; + } + } + if (providerCollection.Count <= 0) { @@ -284,7 +303,9 @@ public static Command CollectCommand() => ProvidersOption(), ProfileOption(), CommonOptions.FormatOption(), - DurationOption() + DurationOption(), + CLREventsOption(), + CLREventLevelOption() }; private static uint DefaultCircularBufferSizeInMB => 256; @@ -331,5 +352,21 @@ private static Option DurationOption() => Argument = new Argument(name: "duration-timespan", defaultValue: default), IsHidden = true }; + + private static Option CLREventsOption() => + new Option( + alias: "--clrevents", + description: @"List of CLR runtime events to emit.") + { + Argument = new Argument(name: "clrevents", defaultValue: "") + }; + + private static Option CLREventLevelOption() => + new Option( + alias: "--clreventlevel", + description: @"Verbosity of CLR events to be emitted.") + { + Argument = new Argument(name: "clreventlevel", defaultValue: "") + }; } } diff --git a/src/Tools/dotnet-trace/Extensions.cs b/src/Tools/dotnet-trace/Extensions.cs index 59b54664ec..aa0c8bff1e 100644 --- a/src/Tools/dotnet-trace/Extensions.cs +++ b/src/Tools/dotnet-trace/Extensions.cs @@ -12,7 +12,41 @@ namespace Microsoft.Diagnostics.Tools.Trace { internal static class Extensions { + public static string CLREventProviderName = "Microsoft-Windows-DotNETRuntime"; + private static EventLevel defaultEventLevel = EventLevel.Verbose; + // Keep this in sync with runtime repo's clretwall.man + private static Dictionary CLREventKeywords = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + { "gc", 0x1 }, + { "gchandle", 0x2 }, + { "fusion", 0x4 }, + { "loader", 0x8 }, + { "jit", 0x10 }, + { "ngen", 0x20 }, + { "startenumeration", 0x40 }, + { "endenumeration", 0x80 }, + { "security", 0x400 }, + { "appdomainresourcemanagement", 0x800 }, + { "jittracing", 0x1000 }, + { "interop", 0x2000 }, + { "contention", 0x4000 }, + { "exception", 0x8000 }, + { "threading", 0x10000 }, + { "jittedmethodiltonativemap", 0x20000 }, + { "overrideandsuppressngenevents", 0x40000 }, + { "type", 0x80000 }, + { "gcheapdump", 0x100000 }, + { "gcsampledobjectallcationhigh", 0x200000 }, + { "gcheapsurvivalandmovement", 0x400000 }, + { "gcheapcollect", 0x800000 }, + { "gcheapandtypenames", 0x1000000 }, + { "gcsampledobjectallcationlow", 0x2000000 }, + { "perftrack", 0x20000000 }, + { "stack", 0x40000000 }, + { "threadtransfer", 0x80000000 }, + { "debugger", 0x100000000 } + }; public static List ToProviders(string providers) { @@ -22,6 +56,37 @@ public static List ToProviders(string providers) new List() : providers.Split(',').Select(ToProvider).ToList(); } + public static EventPipeProvider ToCLREventPipeProvider(string clreventslist, string clreventlevel) + { + if (clreventslist == null || clreventslist.Length == 0) + { + return null; + } + + var clrevents = clreventslist.Split("+"); + long clrEventsKeywordsMask = 0; + for (var i = 0; i < clrevents.Length; i++) + { + if (CLREventKeywords.TryGetValue(clrevents[i], out var keyword)) + { + clrEventsKeywordsMask |= keyword; + } + else + { + throw new ArgumentException($"{clrevents[i]} is not a valid CLR event keyword"); + } + } + + EventLevel level = (EventLevel)4; // Default event level + + if (clreventlevel.Length != 0) + { + level = GetEventLevel(clreventlevel); + } + + return new EventPipeProvider(CLREventProviderName, level, clrEventsKeywordsMask, null); + } + private static EventLevel GetEventLevel(string token) { if (Int32.TryParse(token, out int level) && level >= 0) diff --git a/src/tests/dotnet-trace/CLRProviderParsing.cs b/src/tests/dotnet-trace/CLRProviderParsing.cs new file mode 100644 index 0000000000..b391f9f439 --- /dev/null +++ b/src/tests/dotnet-trace/CLRProviderParsing.cs @@ -0,0 +1,74 @@ +// 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.NETCore.Client; +using System; +using Xunit; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Diagnostics.Tools.Trace +{ + public class CLRProviderParsingTests + { + private static string CLRProviderName = "Microsoft-Windows-DotNETRuntime"; + + [Theory] + [InlineData("gc")] + [InlineData("Gc")] + [InlineData("GC")] + public void ValidSingleCLREvent(string providerToParse) + { + var provider = Extensions.ToCLREventPipeProvider(providerToParse, "4"); + Assert.True(provider.Name == CLRProviderName); + Assert.True(provider.Keywords == 1); + Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Informational); + Assert.True(provider.Arguments == null); + } + + [Theory] + [InlineData("nosuchevent")] + [InlineData("something")] + [InlineData("haha")] + public void InValidSingleCLREvent(string providerToParse) + { + Assert.Throws(() => Extensions.ToCLREventPipeProvider(providerToParse, "4")); + } + + [Theory] + [InlineData("gc+gchandle")] + [InlineData("gc+GCHandle")] + [InlineData("GC+GCHandle")] + public void ValidManyCLREvents(string providerToParse) + { + var provider = Extensions.ToCLREventPipeProvider(providerToParse, "5"); + Assert.True(provider.Name == CLRProviderName); + Assert.True(provider.Keywords == 3); + Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Verbose); + Assert.True(provider.Arguments == null); + } + + [Theory] + [InlineData("informational")] + [InlineData("4")] + [InlineData("Informational")] + [InlineData("InFORMationAL")] + public void ValidCLREventLevel(string clreventlevel) + { + var provider = Extensions.ToCLREventPipeProvider("gc", clreventlevel); + Assert.True(provider.Name == CLRProviderName); + Assert.True(provider.Keywords == 1); + Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Informational); + Assert.True(provider.Arguments == null); + } + + [Theory] + [InlineData("something")] + [InlineData("hello")] + public void InvalidCLREventLevel(string clreventlevel) + { + Assert.Throws(() => Extensions.ToCLREventPipeProvider("gc", clreventlevel)); + } + } +} \ No newline at end of file