diff --git a/src/Tools/Common/Commands/Utils.cs b/src/Tools/Common/Commands/Utils.cs index 0b28a7ff2e..03b317f606 100644 --- a/src/Tools/Common/Commands/Utils.cs +++ b/src/Tools/Common/Commands/Utils.cs @@ -211,6 +211,7 @@ internal enum ReturnCode SessionCreationError, TracingError, ArgumentError, + PlatformNotSupportedError, UnknownError } } diff --git a/src/Tools/dotnet-counters/dotnet-counters.csproj b/src/Tools/dotnet-counters/dotnet-counters.csproj index 95bf4c4060..cd660176dd 100644 --- a/src/Tools/dotnet-counters/dotnet-counters.csproj +++ b/src/Tools/dotnet-counters/dotnet-counters.csproj @@ -12,7 +12,6 @@ - diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index 054a6e83a7..29e7adf713 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -54,7 +54,7 @@ private static void ConsoleWriteLine(string str) /// A string, parsed as [payload_field_name]:[payload_field_value] pairs separated by commas, that will stop the trace upon hitting an event with a matching payload. Requires `--stopping-event-provider-name` and `--stopping-event-event-name` to be set. /// Collect rundown events. /// - private static async Task Collect(CancellationToken ct, CommandLineConfiguration cliConfig, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string diagnosticPort, bool showchildio, bool resumeRuntime, string stoppingEventProviderName, string stoppingEventEventName, string stoppingEventPayloadFilter, bool? rundown, string dsrouter) + private static async Task Collect(CancellationToken ct, CommandLineConfiguration cliConfig, int processId, FileInfo output, uint buffersize, string[] providers, string[] profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string diagnosticPort, bool showchildio, bool resumeRuntime, string stoppingEventProviderName, string stoppingEventEventName, string stoppingEventPayloadFilter, bool? rundown, string dsrouter) { bool collectionStopped = false; bool cancelOnEnter = true; @@ -110,37 +110,35 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur if (profile.Length == 0 && providers.Length == 0 && clrevents.Length == 0) { - ConsoleWriteLine("No profile or providers specified, defaulting to trace profile 'cpu-sampling'"); - profile = "cpu-sampling"; + ConsoleWriteLine("No profile or providers specified, defaulting to trace profiles 'dotnet-common' + 'dotnet-sampled-thread-time'."); + profile = new[] { "dotnet-common", "dotnet-sampled-thread-time" }; } - Dictionary enabledBy = new(); - - List providerCollection = Extensions.ToProviders(providers); - foreach (EventPipeProvider providerCollectionProvider in providerCollection) + List providerCollection = ProviderUtils.ComputeProviderConfig(providers, clrevents, clreventlevel, profile, !IsQuiet, "collect"); + if (providerCollection.Count <= 0) { - enabledBy[providerCollectionProvider.Name] = "--providers "; + Console.Error.WriteLine("No providers were specified to start a trace."); + return (int)ReturnCode.ArgumentError; } - long rundownKeyword = EventPipeSession.DefaultRundownKeyword; + long rundownKeyword = 0; RetryStrategy retryStrategy = RetryStrategy.NothingToRetry; - - if (profile.Length != 0) + foreach (string prof in profile) { - Profile selectedProfile = ListProfilesCommandHandler.DotNETRuntimeProfiles - .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); - if (selectedProfile == null) + // Profiles are already validated in ComputeProviderConfig + Profile selectedProfile = ListProfilesCommandHandler.TraceProfiles + .FirstOrDefault(p => p.Name.Equals(prof, StringComparison.OrdinalIgnoreCase)); + + rundownKeyword |= selectedProfile.RundownKeyword; + if (selectedProfile.RetryStrategy > retryStrategy) { - Console.Error.WriteLine($"Invalid profile name: {profile}"); - return (int)ReturnCode.ArgumentError; + retryStrategy = selectedProfile.RetryStrategy; } - - rundownKeyword = selectedProfile.RundownKeyword; - retryStrategy = selectedProfile.RetryStrategy; - - Profile.MergeProfileAndProviders(selectedProfile, providerCollection, enabledBy); } - + if (rundownKeyword == 0) + { + rundownKeyword = EventPipeSession.DefaultRundownKeyword; + } if (rundown.HasValue) { if (rundown.Value) @@ -155,31 +153,6 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur } } - // 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)) - { - ConsoleWriteLine($"The argument --clrevents {clrevents} will be ignored because the CLR provider was configured via either --profile or --providers command."); - } - else - { - EventPipeProvider clrProvider = Extensions.ToCLREventPipeProvider(clrevents, clreventlevel); - providerCollection.Add(clrProvider); - enabledBy[Extensions.CLREventProviderName] = "--clrevents"; - } - } - - - if (providerCollection.Count <= 0) - { - Console.Error.WriteLine("No providers were specified to start a trace."); - return (int)ReturnCode.ArgumentError; - } - - PrintProviders(providerCollection, enabledBy); - // Validate and parse stoppingEvent parameters: stoppingEventProviderName, stoppingEventEventName, stoppingEventPayloadFilter bool hasStoppingEventProviderName = !string.IsNullOrEmpty(stoppingEventProviderName); @@ -263,7 +236,7 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur } - if (string.Equals(output.Name, DefaultTraceName, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(output.Name, CommonOptions.DefaultTraceName, StringComparison.OrdinalIgnoreCase)) { DateTime now = DateTime.Now; FileInfo processMainModuleFileInfo = new(processMainModuleFileName); @@ -524,20 +497,6 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur return ret; } - private static void PrintProviders(IReadOnlyList providers, Dictionary enabledBy) - { - ConsoleWriteLine(""); - ConsoleWriteLine(string.Format("{0, -40}", "Provider Name") + string.Format("{0, -20}", "Keywords") + - string.Format("{0, -20}", "Level") + "Enabled By"); // +4 is for the tab - foreach (EventPipeProvider provider in providers) - { - ConsoleWriteLine(string.Format("{0, -80}", $"{GetProviderDisplayString(provider)}") + $"{enabledBy[provider.Name]}"); - } - ConsoleWriteLine(""); - } - private static string GetProviderDisplayString(EventPipeProvider provider) => - string.Format("{0, -40}", provider.Name) + string.Format("0x{0, -18}", $"{provider.Keywords:X16}") + string.Format("{0, -8}", provider.EventLevel.ToString() + $"({(int)provider.EventLevel})"); - private static string GetSize(long length) { if (length > 1e9) @@ -565,13 +524,13 @@ public static Command CollectCommand() // Options CommonOptions.ProcessIdOption, CircularBufferOption, - OutputPathOption, - ProvidersOption, - ProfileOption, + CommonOptions.OutputPathOption, + CommonOptions.ProvidersOption, + CommonOptions.ProfileOption, CommonOptions.FormatOption, - DurationOption, - CLREventsOption, - CLREventLevelOption, + CommonOptions.DurationOption, + CommonOptions.CLREventsOption, + CommonOptions.CLREventLevelOption, CommonOptions.NameOption, DiagnosticPortOption, ShowChildIOOption, @@ -585,18 +544,22 @@ public static Command CollectCommand() collectCommand.TreatUnmatchedTokensAsErrors = false; // see the logic in Program.Main that handles UnmatchedTokens collectCommand.Description = "Collects a diagnostic trace from a currently running process or launch a child process and trace it. Append -- to the collect command to instruct the tool to run a command and trace it immediately. When tracing a child process, the exit code of dotnet-trace shall be that of the traced process unless the trace process encounters an error."; - collectCommand.SetAction((parseResult, ct) => Collect( + collectCommand.SetAction((parseResult, ct) => { + string providersValue = parseResult.GetValue(CommonOptions.ProvidersOption) ?? string.Empty; + string profileValue = parseResult.GetValue(CommonOptions.ProfileOption) ?? string.Empty; + + return Collect( ct, cliConfig: parseResult.Configuration, processId: parseResult.GetValue(CommonOptions.ProcessIdOption), - output: parseResult.GetValue(OutputPathOption), + output: parseResult.GetValue(CommonOptions.OutputPathOption), buffersize: parseResult.GetValue(CircularBufferOption), - providers: parseResult.GetValue(ProvidersOption) ?? string.Empty, - profile: parseResult.GetValue(ProfileOption) ?? string.Empty, + providers: providersValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), + profile: profileValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), format: parseResult.GetValue(CommonOptions.FormatOption), - duration: parseResult.GetValue(DurationOption), - clrevents: parseResult.GetValue(CLREventsOption) ?? string.Empty, - clreventlevel: parseResult.GetValue(CLREventLevelOption) ?? string.Empty, + duration: parseResult.GetValue(CommonOptions.DurationOption), + clrevents: parseResult.GetValue(CommonOptions.CLREventsOption) ?? string.Empty, + clreventlevel: parseResult.GetValue(CommonOptions.CLREventLevelOption) ?? string.Empty, name: parseResult.GetValue(CommonOptions.NameOption), diagnosticPort: parseResult.GetValue(DiagnosticPortOption) ?? string.Empty, showchildio: parseResult.GetValue(ShowChildIOOption), @@ -605,7 +568,8 @@ public static Command CollectCommand() stoppingEventEventName: parseResult.GetValue(StoppingEventEventNameOption), stoppingEventPayloadFilter: parseResult.GetValue(StoppingEventPayloadFilterOption), rundown: parseResult.GetValue(RundownOption), - dsrouter: parseResult.GetValue(DSRouterOption))); + dsrouter: parseResult.GetValue(DSRouterOption)); + }); return collectCommand; } @@ -619,53 +583,6 @@ public static Command CollectCommand() DefaultValueFactory = _ => DefaultCircularBufferSizeInMB, }; - public static string DefaultTraceName => "default"; - - private static readonly Option OutputPathOption = - new("--output", "-o") - { - Description = $"The output path for the collected trace data. If not specified it defaults to '__.nettrace', e.g., 'myapp_20210315_111514.nettrace'.", - DefaultValueFactory = _ => new FileInfo(DefaultTraceName) - }; - - private static readonly Option ProvidersOption = - new("--providers") - { - Description = @"A comma delimitted 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]'. Values in KeyValueArgs that contain ';' or '=' characters need to be surrounded by '""', " + - @"e.g., FilterAndPayloadSpecs=""MyProvider/MyEvent:-Prop1=Prop1;Prop2=Prop2.A.B;"". Depending on your shell, you may need to " + - @"escape the '""' characters and/or surround the entire provider specification in quotes, e.g., " + - @"--providers 'KnownProviderName:0x1:1:FilterSpec=\""KnownProviderName/EventName:-Prop1=Prop1;Prop2=Prop2.A.B;\""'. These providers are in " + - @"addition to any providers implied by the --profile argument. If there is any discrepancy for a particular provider, the " + - @"configuration here takes precedence over the implicit configuration from the profile. See documentation for examples." - // TODO: Can we specify an actual type? - }; - - private static readonly Option ProfileOption = - new("--profile") - { - Description = @"A named pre-defined set of provider configurations that allows common tracing scenarios to be specified succinctly." - }; - - private static readonly Option DurationOption = - new("--duration") - { - Description = @"When specified, will trace for the given timespan and then automatically stop the trace. Provided in the form of dd:hh:mm:ss." - }; - - private static readonly Option CLREventsOption = - new("--clrevents") - { - Description = @"List of CLR runtime events to emit." - }; - - private static readonly Option CLREventLevelOption = - new("--clreventlevel") - { - Description = @"Verbosity of CLR events to be emitted." - }; - private static readonly Option DiagnosticPortOption = new("--diagnostic-port", "--dport") { diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs new file mode 100644 index 0000000000..ff6f38dee4 --- /dev/null +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -0,0 +1,295 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Internal.Common.Utils; + +namespace Microsoft.Diagnostics.Tools.Trace +{ + internal static partial class CollectLinuxCommandHandler + { + private static int s_recordStatus; + + internal sealed record CollectLinuxArgs( + string[] Providers, + string ClrEventLevel, + string ClrEvents, + string[] PerfEvents, + string[] Profiles, + FileInfo Output, + TimeSpan Duration, + string Name, + int ProcessId); + + /// + /// Collects diagnostic traces using perf_events, a Linux OS technology. collect-linux requires admin privileges to capture kernel- and user-mode events, and by default, captures events from all processes. + /// This Linux-only command includes the same .NET events as dotnet-trace collect, and it uses the kernel’s user_events mechanism to emit .NET events as perf events, enabling unification of user-space .NET events with kernel-space system events. + /// + private static int CollectLinux(CollectLinuxArgs args) + { + if (!OperatingSystem.IsLinux()) + { + Console.Error.WriteLine("The collect-linux command is only supported on Linux."); + return (int)ReturnCode.ArgumentError; + } + + if (args.ProcessId != 0 && !string.IsNullOrEmpty(args.Name)) + { + Console.Error.WriteLine("Only one of --process-id or --name can be specified."); + return (int)ReturnCode.ArgumentError; + } + + return RunRecordTrace(args); + } + + public static Command CollectLinuxCommand() + { + Command collectLinuxCommand = new("collect-linux") + { + CommonOptions.ProvidersOption, + CommonOptions.CLREventLevelOption, + CommonOptions.CLREventsOption, + PerfEventsOption, + CommonOptions.ProfileOption, + CommonOptions.OutputPathOption, + CommonOptions.DurationOption, + CommonOptions.NameOption, + CommonOptions.ProcessIdOption + }; + collectLinuxCommand.TreatUnmatchedTokensAsErrors = true; // collect-linux currently does not support child process tracing. + collectLinuxCommand.Description = "Collects diagnostic traces using perf_events, a Linux OS technology. collect-linux requires admin privileges to capture kernel- and user-mode events, and by default, captures events from all processes. This Linux-only command includes the same .NET events as dotnet-trace collect, and it uses the kernel’s user_events mechanism to emit .NET events as perf events, enabling unification of user-space .NET events with kernel-space system events."; + + collectLinuxCommand.SetAction((parseResult, ct) => { + string providersValue = parseResult.GetValue(CommonOptions.ProvidersOption) ?? string.Empty; + string perfEventsValue = parseResult.GetValue(PerfEventsOption) ?? string.Empty; + string profilesValue = parseResult.GetValue(CommonOptions.ProfileOption) ?? string.Empty; + + int rc = CollectLinux(new CollectLinuxArgs( + Providers: providersValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), + ClrEventLevel: parseResult.GetValue(CommonOptions.CLREventLevelOption) ?? string.Empty, + ClrEvents: parseResult.GetValue(CommonOptions.CLREventsOption) ?? string.Empty, + PerfEvents: perfEventsValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), + Profiles: profilesValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), + Output: parseResult.GetValue(CommonOptions.OutputPathOption) ?? new FileInfo(CommonOptions.DefaultTraceName), + Duration: parseResult.GetValue(CommonOptions.DurationOption), + Name: parseResult.GetValue(CommonOptions.NameOption) ?? string.Empty, + ProcessId: parseResult.GetValue(CommonOptions.ProcessIdOption))); + return Task.FromResult(rc); + }); + + return collectLinuxCommand; + } + + private static int RunRecordTrace(CollectLinuxArgs args) + { + s_recordStatus = 0; + + ConsoleCancelEventHandler handler = (sender, e) => + { + e.Cancel = true; + s_recordStatus = 1; + }; + Console.CancelKeyPress += handler; + + IEnumerable recordTraceArgList = BuildRecordTraceArgs(args, out string scriptPath); + + string options = string.Join(' ', recordTraceArgList); + byte[] command = Encoding.UTF8.GetBytes(options); + int rc; + try + { + rc = RecordTrace(command, (UIntPtr)command.Length, OutputHandler); + } + finally + { + Console.CancelKeyPress -= handler; + if (!string.IsNullOrEmpty(scriptPath)) + { + try { + if (File.Exists(scriptPath)) + { + File.Delete(scriptPath); + } + } catch { } + } + } + + return rc; + } + + private static List BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath) + { + scriptPath = null; + List recordTraceArgs = new(); + int pid = args.ProcessId; + if (!string.IsNullOrEmpty(args.Name)) + { + pid = CommandUtils.FindProcessIdWithName(args.Name); + } + if (pid > 0) + { + recordTraceArgs.Add($"--pid"); + recordTraceArgs.Add($"{pid}"); + } + + string resolvedOutput = ResolveOutputPath(args.Output, pid); + recordTraceArgs.Add($"--out"); + recordTraceArgs.Add(resolvedOutput); + + if (args.Duration != default) + { + recordTraceArgs.Add($"--duration"); + recordTraceArgs.Add(args.Duration.ToString()); + } + + string[] profiles = args.Profiles; + if (args.Profiles.Length == 0 && args.Providers.Length == 0 && string.IsNullOrEmpty(args.ClrEvents) && args.PerfEvents.Length == 0) + { + Console.WriteLine("No providers, profiles, ClrEvents, or PerfEvents were specified, defaulting to trace profiles 'dotnet-common' + 'cpu-sampling'."); + profiles = new[] { "dotnet-common", "cpu-sampling" }; + } + + foreach (string profile in profiles) + { + Profile traceProfile = ListProfilesCommandHandler.TraceProfiles + .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); + + if (!string.IsNullOrEmpty(traceProfile.VerbExclusivity) && + traceProfile.VerbExclusivity.Equals("collect-linux", StringComparison.OrdinalIgnoreCase)) + { + recordTraceArgs.Add(traceProfile.CollectLinuxArgs); + } + } + + StringBuilder scriptBuilder = new(); + + List providerCollection = ProviderUtils.ComputeProviderConfig(args.Providers, args.ClrEvents, args.ClrEventLevel, profiles, true, "collect-linux"); + foreach (EventPipeProvider provider in providerCollection) + { + string providerName = provider.Name; + string providerNameSanitized = providerName.Replace('-', '_').Replace('.', '_'); + long keywords = provider.Keywords; + uint eventLevel = (uint)provider.EventLevel; + IDictionary arguments = provider.Arguments; + if (arguments != null && arguments.Count > 0) + { + scriptBuilder.Append($"set_dotnet_filter_args(\n\t\"{providerName}\""); + foreach ((string key, string value) in arguments) + { + scriptBuilder.Append($",\n\t\"{key}={value}\""); + } + scriptBuilder.Append($");\n"); + } + + scriptBuilder.Append($"let {providerNameSanitized}_flags = new_dotnet_provider_flags();\n"); + scriptBuilder.Append($"record_dotnet_provider(\"{providerName}\", 0x{keywords:X}, {eventLevel}, {providerNameSanitized}_flags);\n\n"); + } + + foreach (string perfEvent in args.PerfEvents) + { + string[] split = perfEvent.Split(':', 2, StringSplitOptions.TrimEntries); + if (split.Length != 2 || string.IsNullOrEmpty(split[0]) || string.IsNullOrEmpty(split[1])) + { + throw new ArgumentException($"Invalid perf event specification '{perfEvent}'. Expected format 'provider:event'."); + } + + string perfProvider = split[0]; + string perfEventName = split[1]; + Console.WriteLine($"Enabling perf event '{perfEvent}'"); + scriptBuilder.Append($"let {perfEventName} = event_from_tracefs(\"{perfProvider}\", \"{perfEventName}\");\nrecord_event({perfEventName});\n\n"); + } + + string scriptText = scriptBuilder.ToString(); + string scriptFileName = $"{Path.GetFileNameWithoutExtension(resolvedOutput)}.script"; + scriptPath = Path.Combine(Environment.CurrentDirectory, scriptFileName); + File.WriteAllText(scriptPath, scriptText); + + recordTraceArgs.Add("--script-file"); + recordTraceArgs.Add(scriptPath); + + return recordTraceArgs; + } + + private static string ResolveOutputPath(FileInfo output, int processId) + { + if (!string.Equals(output.Name, CommonOptions.DefaultTraceName, StringComparison.OrdinalIgnoreCase)) + { + return output.Name; + } + + DateTime now = DateTime.Now; + if (processId > 0) + { + Process process = Process.GetProcessById(processId); + FileInfo processMainModuleFileInfo = new(process.MainModule.FileName); + return $"{processMainModuleFileInfo.Name}_{now:yyyyMMdd}_{now:HHmmss}.nettrace"; + } + + return $"trace_{now:yyyyMMdd}_{now:HHmmss}.nettrace"; + } + + private static int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) + { + OutputType ot = (OutputType)type; + if (ot != OutputType.Progress) + { + int len = checked((int)dataLen); + if (len > 0) + { + byte[] buffer = new byte[len]; + Marshal.Copy(data, buffer, 0, len); + string text = Encoding.UTF8.GetString(buffer); + switch (ot) + { + case OutputType.Normal: + case OutputType.Live: + Console.Out.WriteLine(text); + break; + case OutputType.Error: + Console.Error.WriteLine(text); + break; + default: + Console.Error.WriteLine($"[{ot}] {text}"); + break; + } + } + } + + return s_recordStatus; + } + + private static readonly Option PerfEventsOption = + new("--perf-events") + { + Description = @"Comma-separated list of perf events (e.g. syscalls:sys_enter_execve,sched:sched_switch)." + }; + + private enum OutputType : uint + { + Normal = 0, + Live = 1, + Error = 2, + Progress = 3, + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate int recordTraceCallback( + [In] uint type, + [In] IntPtr data, + [In] UIntPtr dataLen); + + [LibraryImport("recordtrace")] + private static partial int RecordTrace( + byte[] command, + UIntPtr commandLen, + recordTraceCallback callback); + } +} diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/ConvertCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/ConvertCommand.cs index 055051ae5a..c0288ff100 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/ConvertCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/ConvertCommand.cs @@ -116,8 +116,8 @@ public static Command ConvertCommand() private static readonly Argument InputFileArgument = new Argument(name: "input-filename") { - Description = $"Input trace file to be converted. Defaults to '{CollectCommandHandler.DefaultTraceName}'.", - DefaultValueFactory = _ => new FileInfo(CollectCommandHandler.DefaultTraceName), + Description = $"Input trace file to be converted. Defaults to '{CommonOptions.DefaultTraceName}'.", + DefaultValueFactory = _ => new FileInfo(CommonOptions.DefaultTraceName), }.AcceptExistingOnly(); private static readonly Option OutputOption = diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs b/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs index 7e8c2cea56..58b86ede6e 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs @@ -14,13 +14,38 @@ namespace Microsoft.Diagnostics.Tools.Trace { internal sealed class ListProfilesCommandHandler { + private static long defaultKeyword = 0x1 | // GC + 0x4 | // AssemblyLoader + 0x8 | // Loader + 0x10 | // JIT + 0x8000 | // Exceptions + 0x10000 | // Threading + 0x20000 | // JittedMethodILToNativeMap + 0x1000000000; // Compilation + + private static string dotnetCommonDescription = """ + Lightweight .NET runtime diagnostics designed to stay low overhead. + Includes: + GC + AssemblyLoader + Loader + JIT + Exceptions + Threading + JittedMethodILToNativeMap + Compilation + Equivalent to --providers "Microsoft-Windows-DotNETRuntime:0x100003801D:4". + """; + public static int GetProfiles() { try { - foreach (Profile profile in DotNETRuntimeProfiles) + Console.Out.WriteLine("dotnet-trace profiles:"); + int profileNameWidth = ProfileNamesMaxWidth(TraceProfiles); + foreach (Profile profile in TraceProfiles) { - Console.Out.WriteLine($"\t{profile.Name,-16} - {profile.Description}"); + PrintProfile(profile, profileNameWidth); } return 0; @@ -42,14 +67,19 @@ public static Command ListProfilesCommand() return listProfilesCommand; } - internal static IEnumerable DotNETRuntimeProfiles { get; } = new[] { + internal static IEnumerable TraceProfiles { get; } = new[] { new Profile( - "cpu-sampling", + "dotnet-common", + new EventPipeProvider[] { + new("Microsoft-Windows-DotNETRuntime", EventLevel.Informational, defaultKeyword) + }, + dotnetCommonDescription), + new Profile( + "dotnet-sampled-thread-time", new EventPipeProvider[] { new("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational), - new("Microsoft-Windows-DotNETRuntime", EventLevel.Informational, (long)ClrTraceEventParser.Keywords.Default) }, - "Useful for tracking CPU usage and general .NET runtime information. This is the default option if no profile or providers are specified."), + "Samples .NET thread stacks (~100 Hz) toestimate how much wall clock time code is using.") { VerbExclusivity = "collect" }, new Profile( "gc-verbose", new EventPipeProvider[] { @@ -101,9 +131,55 @@ public static Command ListProfilesCommand() } ) }, - "Captures ADO.NET and Entity Framework database commands") + "Captures ADO.NET and Entity Framework database commands"), + new Profile( + "cpu-sampling", + providers: Array.Empty(), + description: "Kernel CPU sampling events for measuring CPU usage.") { VerbExclusivity = "collect-linux", CollectLinuxArgs = "--on-cpu" }, + new Profile( + "thread-time", + providers: Array.Empty(), + description: "Kernel thread context switch events for measuring CPU usage and wall clock time") { VerbExclusivity = "collect-linux", CollectLinuxArgs = "--off-cpu" }, }; + private static int ProfileNamesMaxWidth(IEnumerable profiles) + { + int maxWidth = 0; + foreach (Profile profile in profiles) + { + int profileNameWidth = profile.Name.Length; + if (!string.IsNullOrEmpty(profile.VerbExclusivity)) + { + profileNameWidth = $"{profile.Name} ({profile.VerbExclusivity})".Length; + } + if (profileNameWidth > maxWidth) + { + maxWidth = profileNameWidth; + } + } + + return maxWidth; + } + + private static void PrintProfile(Profile profile, int nameColumnWidth) + { + string[] descriptionLines = profile.Description.Replace("\r\n", "\n").Split('\n'); + + string profileColumn = $"{profile.Name}"; + if (!string.IsNullOrEmpty(profile.VerbExclusivity)) + { + profileColumn = $"{profile.Name} ({profile.VerbExclusivity})"; + } + + Console.Out.WriteLine($"\t{profileColumn.PadRight(nameColumnWidth)} - {descriptionLines[0]}"); + + string continuationPrefix = $"\t{new string(' ', nameColumnWidth)} "; + for (int i = 1; i < descriptionLines.Length; i++) + { + Console.Out.WriteLine(continuationPrefix + descriptionLines[i]); + } + } + /// /// Keywords for DiagnosticSourceEventSource provider /// diff --git a/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs b/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs index 98be9c055a..013e6e5749 100644 --- a/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs +++ b/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs @@ -1,22 +1,43 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.CommandLine; +using System.IO; namespace Microsoft.Diagnostics.Tools.Trace { internal static class CommonOptions { - public static readonly Option ProcessIdOption = - new("--process-id", "-p") + public static readonly Option ProvidersOption = + new("--providers") { - Description = "The process id to collect the trace." + Description = @"A comma delimited 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]'. Values in KeyValueArgs that contain ';' or '=' characters need to be surrounded by '""', " + + @"e.g., FilterAndPayloadSpecs=""MyProvider/MyEvent:-Prop1=Prop1;Prop2=Prop2.A.B;"". Depending on your shell, you may need to " + + @"escape the '""' characters and/or surround the entire provider specification in quotes, e.g., " + + @"--providers 'KnownProviderName:0x1:1:FilterSpec=\""KnownProviderName/EventName:-Prop1=Prop1;Prop2=Prop2.A.B;\""'. These providers are in " + + @"addition to any providers implied by the --profile argument. If there is any discrepancy for a particular provider, the " + + @"configuration here takes precedence over the implicit configuration from the profile. See documentation for examples." }; - public static readonly Option NameOption = - new("--name", "-n") + public static readonly Option CLREventLevelOption = + new("--clreventlevel") { - Description = "The name of the process to collect the trace.", + Description = @"Verbosity of CLR events to be emitted." + }; + + public static readonly Option CLREventsOption = + new("--clrevents") + { + Description = @"List of CLR runtime events to emit." + }; + + public static readonly Option ProfileOption = + new("--profile") + { + Description = @"A named, pre-defined set of provider configurations for common tracing scenarios. You can specify multiple profiles as a comma-separated list. When multiple profiles are specified, the providers and settings are combined (union), and duplicates are ignored." }; public static TraceFileFormat DefaultTraceFileFormat() => TraceFileFormat.NetTrace; @@ -28,6 +49,33 @@ internal static class CommonOptions DefaultValueFactory = _ => DefaultTraceFileFormat() }; + public static string DefaultTraceName => "default"; + + public static readonly Option OutputPathOption = + new("--output", "-o") + { + Description = $"The output path for the collected trace data. If not specified it defaults to '__.nettrace', e.g., 'myapp_20210315_111514.nettrace'.", + DefaultValueFactory = _ => new FileInfo(DefaultTraceName) + }; + + public static readonly Option DurationOption = + new("--duration") + { + Description = @"When specified, will trace for the given timespan and then automatically stop the trace. Provided in the form of dd:hh:mm:ss." + }; + + public static readonly Option NameOption = + new("--name", "-n") + { + Description = "The name of the process to collect the trace.", + }; + + public static readonly Option ProcessIdOption = + new("--process-id", "-p") + { + Description = "The process id to collect the trace." + }; + public static readonly Option ConvertFormatOption = new("--format") { diff --git a/src/Tools/dotnet-trace/Profile.cs b/src/Tools/dotnet-trace/Profile.cs index 2114ff0540..4ba36b2445 100644 --- a/src/Tools/dotnet-trace/Profile.cs +++ b/src/Tools/dotnet-trace/Profile.cs @@ -26,35 +26,8 @@ public Profile(string name, IEnumerable providers, string des public RetryStrategy RetryStrategy { get; set; } = RetryStrategy.NothingToRetry; - public static void MergeProfileAndProviders(Profile selectedProfile, List providerCollection, Dictionary enabledBy) - { - List profileProviders = new(); - // If user defined a different key/level on the same provider via --providers option that was specified via --profile option, - // --providers option takes precedence. Go through the list of providers specified and only add it if it wasn't specified - // via --providers options. - if (selectedProfile.Providers != null) - { - foreach (EventPipeProvider selectedProfileProvider in selectedProfile.Providers) - { - bool shouldAdd = true; - - foreach (EventPipeProvider providerCollectionProvider in providerCollection) - { - if (providerCollectionProvider.Name.Equals(selectedProfileProvider.Name)) - { - shouldAdd = false; - break; - } - } - - if (shouldAdd) - { - enabledBy[selectedProfileProvider.Name] = "--profile "; - profileProviders.Add(selectedProfileProvider); - } - } - } - providerCollection.AddRange(profileProviders); - } + public string VerbExclusivity { get; set; } = string.Empty; + + public string CollectLinuxArgs { get; set; } = string.Empty; } } diff --git a/src/Tools/dotnet-trace/Program.cs b/src/Tools/dotnet-trace/Program.cs index 69c3cff0be..c1821471ff 100644 --- a/src/Tools/dotnet-trace/Program.cs +++ b/src/Tools/dotnet-trace/Program.cs @@ -18,6 +18,7 @@ public static Task Main(string[] args) RootCommand rootCommand = new() { CollectCommandHandler.CollectCommand(), + CollectLinuxCommandHandler.CollectLinuxCommand(), ProcessStatusCommandHandler.ProcessStatusCommand("Lists the dotnet processes that traces can be collected from."), ListProfilesCommandHandler.ListProfilesCommand(), ConvertCommandHandler.ConvertCommand(), diff --git a/src/Tools/dotnet-trace/Extensions.cs b/src/Tools/dotnet-trace/ProviderUtils.cs similarity index 53% rename from src/Tools/dotnet-trace/Extensions.cs rename to src/Tools/dotnet-trace/ProviderUtils.cs index b9bd829c70..d9c31cbc82 100644 --- a/src/Tools/dotnet-trace/Extensions.cs +++ b/src/Tools/dotnet-trace/ProviderUtils.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.Tracing; using System.Linq; using System.Text.RegularExpressions; @@ -10,7 +11,7 @@ namespace Microsoft.Diagnostics.Tools.Trace { - internal static class Extensions + internal static class ProviderUtils { public static string CLREventProviderName = "Microsoft-Windows-DotNETRuntime"; @@ -23,6 +24,7 @@ internal static class Extensions { "gc", 0x1 }, { "gchandle", 0x2 }, { "fusion", 0x4 }, + { "assemblyloader", 0x4 }, { "loader", 0x8 }, { "jit", 0x10 }, { "ngen", 0x20 }, @@ -42,6 +44,7 @@ internal static class Extensions { "gcsampledobjectallocationhigh", 0x200000 }, { "gcheapsurvivalandmovement", 0x400000 }, { "gcheapcollect", 0x800000 }, + { "managedheadcollect", 0x800000 }, { "gcheapandtypenames", 0x1000000 }, { "gcsampledobjectallocationlow", 0x2000000 }, { "perftrack", 0x20000000 }, @@ -55,57 +58,154 @@ internal static class Extensions { "compilationdiagnostic", 0x2000000000 }, { "methoddiagnostic", 0x4000000000 }, { "typediagnostic", 0x8000000000 }, + { "jitinstrumentationdata", 0x10000000000 }, + { "profiler", 0x20000000000 }, { "waithandle", 0x40000000000 }, + { "allocationsampling", 0x80000000000 }, }; - public static List ToProviders(string providersRawInput) + private enum ProviderSource { - if (providersRawInput == null) + ProvidersArg = 1, + CLREventsArg = 2, + ProfileArg = 4, + } + + public static List ComputeProviderConfig(string[] providersArg, string clreventsArg, string clreventlevel, string[] profiles, bool shouldPrintProviders = false, string verbExclusivity = null) + { + Dictionary merged = new(StringComparer.OrdinalIgnoreCase); + Dictionary providerSources = new(StringComparer.OrdinalIgnoreCase); + + foreach (string providerArg in providersArg) { - throw new ArgumentNullException(nameof(providersRawInput)); + EventPipeProvider provider = ToProvider(providerArg); + if (!merged.TryGetValue(provider.Name, out EventPipeProvider existing)) + { + merged[provider.Name] = provider; + providerSources[provider.Name] = (int)ProviderSource.ProvidersArg; + } + else + { + merged[provider.Name] = MergeProviderConfigs(existing, provider); + } } - if (string.IsNullOrWhiteSpace(providersRawInput)) + foreach (string profile in profiles) { - return new List(); + Profile traceProfile = ListProfilesCommandHandler.TraceProfiles + .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); + + if (traceProfile == null) + { + throw new ArgumentException($"Invalid profile name: {profile}"); + } + + if (!string.IsNullOrEmpty(verbExclusivity) && + !string.IsNullOrEmpty(traceProfile.VerbExclusivity) && + !string.Equals(traceProfile.VerbExclusivity, verbExclusivity, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException($"The specified profile '{traceProfile.Name}' does not apply to `dotnet-trace {verbExclusivity}`."); + } + + if (shouldPrintProviders) + { + string profileEffect = string.Join(", ", traceProfile.Providers.Select(p => p.ToString())); + if (!string.IsNullOrEmpty(traceProfile.VerbExclusivity) && + traceProfile.VerbExclusivity.Equals("collect-linux", StringComparison.OrdinalIgnoreCase)) + { + profileEffect = traceProfile.CollectLinuxArgs; + } + Console.WriteLine($"Applying profile '{traceProfile.Name}': {profileEffect}"); + } + + IEnumerable profileProviders = traceProfile.Providers; + foreach (EventPipeProvider provider in profileProviders) + { + if (merged.TryAdd(provider.Name, provider)) + { + providerSources[provider.Name] = (int)ProviderSource.ProfileArg; + } + // Prefer providers set through --providers over implicit profile configuration + } } - IEnumerable providers = providersRawInput.Split(',').Select(ToProvider).ToList(); - - // Dedupe the entries - providers = providers.GroupBy(p => p.Name) - .Select(p => { - string providerName = p.Key; - EventLevel providerLevel = EventLevel.Critical; - long providerKeywords = 0; - IDictionary providerFilterArgs = null; - - foreach (EventPipeProvider currentProvider in p) - { - providerKeywords |= currentProvider.Keywords; - - if ((currentProvider.EventLevel == EventLevel.LogAlways) - || (providerLevel != EventLevel.LogAlways && currentProvider.EventLevel > providerLevel)) - { - providerLevel = currentProvider.EventLevel; - } - - if (currentProvider.Arguments != null) - { - if (providerFilterArgs != null) - { - throw new ArgumentException($"Provider \"{providerName}\" is declared multiple times with filter arguments."); - } - - providerFilterArgs = currentProvider.Arguments; - } - } - - return new EventPipeProvider(providerName, providerLevel, providerKeywords, providerFilterArgs); - }); - - return providers.ToList(); + if (!string.IsNullOrEmpty(clreventsArg)) + { + EventPipeProvider provider = ToCLREventPipeProvider(clreventsArg, clreventlevel); + if (provider is not null) + { + if (!merged.TryGetValue(provider.Name, out EventPipeProvider existing)) + { + merged[provider.Name] = provider; + providerSources[provider.Name] = (int)ProviderSource.CLREventsArg; + } + else if (shouldPrintProviders) + { + Console.WriteLine($"Warning: The CLR provider was already specified through --providers or --profile. Ignoring --clrevents."); + } + } + } + + List unifiedProviders = merged.Values.ToList(); + if (shouldPrintProviders) + { + PrintProviders(unifiedProviders, providerSources); + } + + return unifiedProviders; + } + + private static EventPipeProvider MergeProviderConfigs(EventPipeProvider providerConfigA, EventPipeProvider providerConfigB) + { + Debug.Assert(string.Equals(providerConfigA.Name, providerConfigB.Name, StringComparison.OrdinalIgnoreCase)); + + EventLevel level = (providerConfigA.EventLevel == EventLevel.LogAlways || providerConfigB.EventLevel == EventLevel.LogAlways) ? + EventLevel.LogAlways : + (providerConfigA.EventLevel > providerConfigB.EventLevel ? providerConfigA.EventLevel : providerConfigB.EventLevel); + + if (providerConfigA.Arguments != null && providerConfigB.Arguments != null) + { + throw new ArgumentException($"Provider \"{providerConfigA.Name}\" is declared multiple times with filter arguments."); + } + + return new EventPipeProvider(providerConfigA.Name, level, providerConfigA.Keywords | providerConfigB.Keywords, providerConfigA.Arguments ?? providerConfigB.Arguments); + } + + private static void PrintProviders(IReadOnlyList providers, Dictionary enabledBy) + { + if (providers.Count == 0) + { + Console.WriteLine("No providers were configured."); + return; + } + + Console.WriteLine(""); + Console.WriteLine(string.Format("{0, -40}", "Provider Name") + string.Format("{0, -20}", "Keywords") + + string.Format("{0, -20}", "Level") + "Enabled By"); // +4 is for the tab + foreach (EventPipeProvider provider in providers) + { + List providerSources = new(); + if (enabledBy.TryGetValue(provider.Name, out int source)) + { + if ((source & (int)ProviderSource.ProvidersArg) == (int)ProviderSource.ProvidersArg) + { + providerSources.Add("--providers"); + } + if ((source & (int)ProviderSource.CLREventsArg) == (int)ProviderSource.CLREventsArg) + { + providerSources.Add("--clrevents"); + } + if ((source & (int)ProviderSource.ProfileArg) == (int)ProviderSource.ProfileArg) + { + providerSources.Add("--profile"); + } + } + Console.WriteLine(string.Format("{0, -80}", $"{GetProviderDisplayString(provider)}") + string.Join(", ", providerSources)); + } + Console.WriteLine(""); } + private static string GetProviderDisplayString(EventPipeProvider provider) => + string.Format("{0, -40}", provider.Name) + string.Format("0x{0, -18}", $"{provider.Keywords:X16}") + string.Format("{0, -8}", provider.EventLevel.ToString() + $"({(int)provider.EventLevel})"); public static EventPipeProvider ToCLREventPipeProvider(string clreventslist, string clreventlevel) { diff --git a/src/Tools/dotnet-trace/dotnet-trace.csproj b/src/Tools/dotnet-trace/dotnet-trace.csproj index 71b6aacb2e..0a303ce663 100644 --- a/src/Tools/dotnet-trace/dotnet-trace.csproj +++ b/src/Tools/dotnet-trace/dotnet-trace.csproj @@ -14,6 +14,8 @@ + @@ -29,4 +31,8 @@ + + + diff --git a/src/tests/dotnet-trace/CLRProviderParsing.cs b/src/tests/dotnet-trace/CLRProviderParsing.cs index 01d7b4607c..36d0dc9335 100644 --- a/src/tests/dotnet-trace/CLRProviderParsing.cs +++ b/src/tests/dotnet-trace/CLRProviderParsing.cs @@ -16,7 +16,7 @@ public class CLRProviderParsingTests [InlineData("GC")] public void ValidSingleCLREvent(string providerToParse) { - NETCore.Client.EventPipeProvider provider = Extensions.ToCLREventPipeProvider(providerToParse, "4"); + NETCore.Client.EventPipeProvider provider = ProviderUtils.ToCLREventPipeProvider(providerToParse, "4"); Assert.True(provider.Name == CLRProviderName); Assert.True(provider.Keywords == 1); Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Informational); @@ -29,7 +29,7 @@ public void ValidSingleCLREvent(string providerToParse) [InlineData("haha")] public void InValidSingleCLREvent(string providerToParse) { - Assert.Throws(() => Extensions.ToCLREventPipeProvider(providerToParse, "4")); + Assert.Throws(() => ProviderUtils.ToCLREventPipeProvider(providerToParse, "4")); } [Theory] @@ -38,7 +38,7 @@ public void InValidSingleCLREvent(string providerToParse) [InlineData("GC+GCHandle")] public void ValidManyCLREvents(string providerToParse) { - NETCore.Client.EventPipeProvider provider = Extensions.ToCLREventPipeProvider(providerToParse, "5"); + NETCore.Client.EventPipeProvider provider = ProviderUtils.ToCLREventPipeProvider(providerToParse, "5"); Assert.True(provider.Name == CLRProviderName); Assert.True(provider.Keywords == 3); Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Verbose); @@ -52,7 +52,7 @@ public void ValidManyCLREvents(string providerToParse) [InlineData("InFORMationAL")] public void ValidCLREventLevel(string clreventlevel) { - NETCore.Client.EventPipeProvider provider = Extensions.ToCLREventPipeProvider("gc", clreventlevel); + NETCore.Client.EventPipeProvider provider = ProviderUtils.ToCLREventPipeProvider("gc", clreventlevel); Assert.True(provider.Name == CLRProviderName); Assert.True(provider.Keywords == 1); Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Informational); @@ -64,7 +64,7 @@ public void ValidCLREventLevel(string clreventlevel) [InlineData("hello")] public void InvalidCLREventLevel(string clreventlevel) { - Assert.Throws(() => Extensions.ToCLREventPipeProvider("gc", clreventlevel)); + Assert.Throws(() => ProviderUtils.ToCLREventPipeProvider("gc", clreventlevel)); } } } diff --git a/src/tests/dotnet-trace/ProfileProviderMerging.cs b/src/tests/dotnet-trace/ProfileProviderMerging.cs deleted file mode 100644 index 606fa24e50..0000000000 --- a/src/tests/dotnet-trace/ProfileProviderMerging.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics.Tracing; -using System.Linq; -using Microsoft.Diagnostics.NETCore.Client; -using Xunit; - -namespace Microsoft.Diagnostics.Tools.Trace -{ - public class ProfileProviderMergeTests - { - [Theory] - [InlineData("cpu-sampling", "Microsoft-Windows-DotNETRuntime")] - [InlineData("gc-verbose", "Microsoft-Windows-DotNETRuntime")] - [InlineData("gc-collect", "Microsoft-Windows-DotNETRuntime")] - public void DuplicateProvider_CorrectlyOverrides(string profileName, string providerToParse) - { - Dictionary enabledBy = new(); - - List parsedProviders = Extensions.ToProviders(providerToParse); - - foreach (EventPipeProvider provider in parsedProviders) - { - enabledBy[provider.Name] = "--providers"; - } - - Profile selectedProfile = ListProfilesCommandHandler.DotNETRuntimeProfiles - .FirstOrDefault(p => p.Name.Equals(profileName, StringComparison.OrdinalIgnoreCase)); - Assert.NotNull(selectedProfile); - - Profile.MergeProfileAndProviders(selectedProfile, parsedProviders, enabledBy); - - EventPipeProvider enabledProvider = parsedProviders.SingleOrDefault(p => p.Name == "Microsoft-Windows-DotNETRuntime"); - - // Assert that our specified provider overrides the version in the profile - Assert.Equal((long)(0), enabledProvider.Keywords); - Assert.Equal(EventLevel.Informational, enabledProvider.EventLevel); - Assert.Equal("--providers", enabledBy[enabledProvider.Name]); - } - } -} diff --git a/src/tests/dotnet-trace/ProviderCompositionTests.cs b/src/tests/dotnet-trace/ProviderCompositionTests.cs new file mode 100644 index 0000000000..b51c6d545f --- /dev/null +++ b/src/tests/dotnet-trace/ProviderCompositionTests.cs @@ -0,0 +1,336 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.IO; +using System.Linq; +using Microsoft.Diagnostics.NETCore.Client; +using Xunit; + +namespace Microsoft.Diagnostics.Tools.Trace +{ + public class ProviderCompositionTests + { + private static readonly Dictionary simpleArgs = new() { { "FilterAndPayloadSpecs", "QuotedValue" } }; + private static readonly Dictionary keyValueArgs = new() { { "key", "value" } }; + private static readonly Dictionary complexArgs = new() { { "FilterAndPayloadSpecs", "QuotedValue:-\r\nQuoted/Value" } }; + private static readonly Dictionary complexABCDArgs = new() { { "FilterAndPayloadSpecs", "QuotedValue:-\r\nQuoted/Value:-A=B;C=D;" } }; + + public static IEnumerable ValidProviders() + { + yield return new object[] { "VeryCoolProvider:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\"", new EventPipeProvider("VeryCoolProvider", EventLevel.Verbose, 0x1, simpleArgs) }; + yield return new object[] { "VeryCoolProvider:1:5:FilterAndPayloadSpecs=\"QuotedValue\"", new EventPipeProvider("VeryCoolProvider", EventLevel.Verbose, 0x1, simpleArgs) }; + yield return new object[] { "VeryCoolProvider:0x1:5:FilterAndPayloadSpecs=\"QuotedValue:-\r\nQuoted/Value\"", new EventPipeProvider("VeryCoolProvider", EventLevel.Verbose, 0x1, complexArgs) }; + yield return new object[] { "VeryCoolProvider:0xFFFFFFFFFFFFFFFF:5:FilterAndPayloadSpecs=\"QuotedValue\"", new EventPipeProvider("VeryCoolProvider", EventLevel.Verbose, unchecked((long)0xFFFFFFFFFFFFFFFF), simpleArgs) }; + yield return new object[] { "VeryCoolProvider::4:FilterAndPayloadSpecs=\"QuotedValue\"", new EventPipeProvider("VeryCoolProvider", EventLevel.Informational, 0, simpleArgs) }; + yield return new object[] { "VeryCoolProvider:::FilterAndPayloadSpecs=\"QuotedValue\"", new EventPipeProvider("VeryCoolProvider", EventLevel.Informational, 0, simpleArgs) }; + yield return new object[] { "ProviderOne:0x1:Verbose", new EventPipeProvider("ProviderOne", EventLevel.Verbose, 0x1) }; + yield return new object[] { "ProviderOne:0x1:verbose", new EventPipeProvider("ProviderOne", EventLevel.Verbose, 0x1) }; + yield return new object[] { "ProviderOne:0x1:Informational", new EventPipeProvider("ProviderOne", EventLevel.Informational, 0x1) }; + yield return new object[] { "ProviderOne:0x1:INFORMATIONAL", new EventPipeProvider("ProviderOne", EventLevel.Informational, 0x1) }; + yield return new object[] { "ProviderOne:0x1:LogAlways", new EventPipeProvider("ProviderOne", EventLevel.LogAlways, 0x1) }; + yield return new object[] { "ProviderOne:0x1:LogAlwayS", new EventPipeProvider("ProviderOne", EventLevel.LogAlways, 0x1) }; + yield return new object[] { "ProviderOne:0x1:Error", new EventPipeProvider("ProviderOne", EventLevel.Error, 0x1) }; + yield return new object[] { "ProviderOne:0x1:ERRor", new EventPipeProvider("ProviderOne", EventLevel.Error, 0x1) }; + yield return new object[] { "ProviderOne:0x1:Critical", new EventPipeProvider("ProviderOne", EventLevel.Critical, 0x1) }; + yield return new object[] { "ProviderOne:0x1:CRITICAL", new EventPipeProvider("ProviderOne", EventLevel.Critical, 0x1) }; + yield return new object[] { "ProviderOne:0x1:Warning", new EventPipeProvider("ProviderOne", EventLevel.Warning, 0x1) }; + yield return new object[] { "ProviderOne:0x1:warning", new EventPipeProvider("ProviderOne", EventLevel.Warning, 0x1) }; + yield return new object[] { "MyProvider:::A=B;C=D", new EventPipeProvider("MyProvider", EventLevel.Informational, 0x0, new Dictionary { { "A", "B" }, { "C", "D" } }) }; + } + + public static IEnumerable InvalidProviders() + { + yield return new object[] { ":::", typeof(ArgumentException) }; + yield return new object[] { ":1:1", typeof(ArgumentException) }; + yield return new object[] { "ProviderOne:0x1:UnknownLevel", typeof(ArgumentException) }; + yield return new object[] { "VeryCoolProvider:0x0:-1", typeof(ArgumentException) }; + yield return new object[] { "VeryCoolProvider:0xFFFFFFFFFFFFFFFFF:5:FilterAndPayloadSpecs=\"QuotedValue\"", typeof(OverflowException) }; + yield return new object[] { "VeryCoolProvider:0x10000000000000000::FilterAndPayloadSpecs=\"QuotedValue\"", typeof(OverflowException) }; + yield return new object[] { "VeryCoolProvider:__:5:FilterAndPayloadSpecs=\"QuotedValue\"", typeof(FormatException) }; + yield return new object[] { "VeryCoolProvider:gh::FilterAndPayloadSpecs=\"QuotedValue\"", typeof(FormatException) }; + } + + [Theory] + [MemberData(nameof(ValidProviders))] + public void ProvidersArg_ParsesCorrectly(string providersArg, EventPipeProvider expected) + { + string[] providers = providersArg.Split(',', StringSplitOptions.RemoveEmptyEntries); + List parsedProviders = ProviderUtils.ComputeProviderConfig(providers, string.Empty, string.Empty, Array.Empty()); + EventPipeProvider actual = Assert.Single(parsedProviders); + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(InvalidProviders))] + public void InvalidProvidersArg_Throws(string providersArg, Type expectedException) + { + string[] providers = providersArg.Split(',', StringSplitOptions.RemoveEmptyEntries); + Assert.Throws(expectedException, () => ProviderUtils.ComputeProviderConfig(providers, string.Empty, string.Empty, Array.Empty())); + } + + public static IEnumerable MultipleValidProviders() + { + yield return new object[] { + "ProviderOne:0x1:1:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:2:2:key=value,ProviderThree:3:3:key=value", + new[] { + new EventPipeProvider("ProviderOne", EventLevel.Critical, 0x1, simpleArgs), + new EventPipeProvider("ProviderTwo", EventLevel.Error, 0x2, keyValueArgs), + new EventPipeProvider("ProviderThree", EventLevel.Warning, 0x3, keyValueArgs) + } + }; + yield return new object[] { + "ProviderOne:0x1:1:FilterAndPayloadSpecs=\"QuotedValue:-\r\nQuoted/Value:-A=B;C=D;\",ProviderTwo:2:2:FilterAndPayloadSpecs=\"QuotedValue\",ProviderThree:3:3:FilterAndPayloadSpecs=\"QuotedValue:-\r\nQuoted/Value:-A=B;C=D;\"", + new[] { + new EventPipeProvider("ProviderOne", EventLevel.Critical, 0x1, complexABCDArgs), + new EventPipeProvider("ProviderTwo", EventLevel.Error, 0x2, simpleArgs), + new EventPipeProvider("ProviderThree", EventLevel.Warning, 0x3, complexABCDArgs) + } + }; + yield return new object[] { + "MyProvider:::A=B;C=\"D\",MyProvider2:::A=1;B=2;", + new[] { + new EventPipeProvider("MyProvider", EventLevel.Informational, 0x0, new Dictionary { { "A", "B" }, { "C", "D" } }), + new EventPipeProvider("MyProvider2", EventLevel.Informational, 0x0, new Dictionary { { "A", "1" }, { "B", "2" } }) + } + }; + yield return new object[] { + "MyProvider:::A=\"B;C=D\",MyProvider2:::A=\"spaced words\";C=1285;D=Spaced Words 2", + new[] { + new EventPipeProvider("MyProvider", EventLevel.Informational, 0x0, new Dictionary { { "A", "B;C=D" } }), + new EventPipeProvider("MyProvider2", EventLevel.Informational, 0x0, new Dictionary { { "A", "spaced words" }, { "C", "1285" }, { "D", "Spaced Words 2" } }) + } + }; + } + + [Theory] + [MemberData(nameof(MultipleValidProviders))] + public void MultipleProviders_Parse_AsExpected(string providersArg, EventPipeProvider[] expected) + { + string[] providers = providersArg.Split(',', StringSplitOptions.RemoveEmptyEntries); + List parsed = ProviderUtils.ComputeProviderConfig(providers, string.Empty, string.Empty, Array.Empty()); + Assert.Equal(expected.Length, parsed.Count); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], parsed[i]); + } + } + + public static IEnumerable MultipleInvalidProviders() + { + yield return new object[] { "ProviderOne:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\",:2:2:key=value,ProviderThree:3:3:key=value", typeof(ArgumentException) }; + yield return new object[] { "ProviderOne:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:0xFFFFFFFFFFFFFFFFF:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderThree:3:3:key=value", typeof(OverflowException) }; + yield return new object[] { "ProviderOne:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:0x10000000000000000:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderThree:3:3:key=value", typeof(OverflowException) }; + yield return new object[] { "ProviderOne:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:18446744073709551615:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderThree:3:3:key=value", typeof(OverflowException) }; + yield return new object[] { "ProviderOne:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:__:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderThree:3:3:key=value", typeof(FormatException) }; + } + + [Theory] + [MemberData(nameof(MultipleInvalidProviders))] + public void MultipleProviders_FailureCases_Throw(string providersArg, Type expectedException) + { + string[] providers = providersArg.Split(',', StringSplitOptions.RemoveEmptyEntries); + Assert.Throws(expectedException, () => ProviderUtils.ComputeProviderConfig(providers, string.Empty, string.Empty, Array.Empty())); + } + + public static IEnumerable DedupeSuccessCases() + { + yield return new object[] { new[]{ "DupeProvider", "DupeProvider:0xF:LogAlways" }, new EventPipeProvider("DupeProvider", EventLevel.LogAlways, 0xF) }; + yield return new object[] { new[]{ "DupeProvider:0xF0:Informational", "DupeProvider:0xF:Verbose" }, new EventPipeProvider("DupeProvider", EventLevel.Verbose, 0xFF) }; + yield return new object[] { new[]{ "MyProvider:0x1:Informational", "MyProvider:0x2:Verbose" }, new EventPipeProvider("MyProvider", EventLevel.Verbose, 0x3) }; + yield return new object[] { new[] { "MyProvider:0x1:5", "MyProvider:0x2:LogAlways" }, new EventPipeProvider("MyProvider", EventLevel.LogAlways, 0x3) }; + yield return new object[] { new[]{ "MyProvider:0x1:Error", "myprovider:0x2:Critical" }, new EventPipeProvider("MyProvider", EventLevel.Error, 0x3) }; + } + + public static IEnumerable DedupeFailureCases() + { + yield return new object[] { new[]{ "MyProvider:::key=value", "MyProvider:::key=value" }, typeof(ArgumentException) }; + } + + [Theory] + [MemberData(nameof(DedupeSuccessCases))] + public void DedupeProviders_Success(string[] providersArg, EventPipeProvider expected) + { + List list = ProviderUtils.ComputeProviderConfig(providersArg, string.Empty, string.Empty, Array.Empty()); + EventPipeProvider actual = Assert.Single(list); + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(DedupeFailureCases))] + public void DedupeProviders_Failure(string[] providersArg, Type expectedException) + { + Assert.Throws(expectedException, () => ProviderUtils.ComputeProviderConfig(providersArg, string.Empty, string.Empty, Array.Empty())); + } + + public static IEnumerable PrecedenceCases() + { + yield return new object[] { + Array.Empty(), + "gc+jit", + string.Empty, + Array.Empty(), + new[]{ new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Informational, 0x1 | 0x10) } + }; + + yield return new object[] { + Array.Empty(), + "gc", + "Verbose", + new[]{ "dotnet-common", "dotnet-sampled-thread-time" }, + new[]{ + new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Informational, 0x100003801D), + new EventPipeProvider("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational, 0xF00000000000) + } + }; + + yield return new object[] { + new[]{ "Microsoft-Windows-DotNETRuntime:0x40000000:Verbose" }, + "gc", + "Informational", + new[]{ "dotnet-common" }, + new[]{ new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Verbose, 0x40000000) } + }; + + yield return new object[] { + Array.Empty(), + string.Empty, + string.Empty, + new[]{ "dotnet-common" }, + new[]{ new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Informational, 0x100003801D) } + }; + + yield return new object[] { + Array.Empty(), + string.Empty, + string.Empty, + new[]{ "dotnet-common", "gc-verbose" }, + new[]{ new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Informational, 0x100003801D) } + }; + + yield return new object[] { + new[]{ "Microsoft-Windows-DotNETRuntime:0x0:Informational" }, + string.Empty, + string.Empty, + new[]{ "dotnet-common" }, + new[]{ new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Informational, 0x0) } + }; + } + + [Theory] + [MemberData(nameof(PrecedenceCases))] + public void ProviderSourcePrecedence(string[] providersArg, string clreventsArg, string clreventLevel, string[] profiles, EventPipeProvider[] expected) + { + List actual = ProviderUtils.ComputeProviderConfig(providersArg, clreventsArg, clreventLevel, profiles); + Assert.Equal(expected.Length, actual.Count); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], actual[i]); + } + } + + public static IEnumerable InvalidClrEvents() + { + yield return new object[] { Array.Empty(), "gc+bogus", string.Empty, Array.Empty(), typeof(ArgumentException) }; + } + + [Theory] + [MemberData(nameof(InvalidClrEvents))] + public void UnknownClrEvents_Throws(string[] providersArg, string clreventsArg, string clreventLevel, string[] profiles, Type expectedException) + { + Assert.Throws(expectedException, () => ProviderUtils.ComputeProviderConfig(providersArg, clreventsArg, clreventLevel, profiles)); + } + + public record ProviderSourceExpectation(string Name, bool FromProviders, bool FromClrEvents, bool FromProfile); + + public static IEnumerable ProviderSourcePrintCases() + { + yield return new object[] { + new[]{ "MyProvider:0x1:Error" }, + "gc", + "Informational", + new[]{ "dotnet-sampled-thread-time" }, + new[]{ + new ProviderSourceExpectation("MyProvider", true, false, false), + new ProviderSourceExpectation("Microsoft-Windows-DotNETRuntime", false, true, false), + new ProviderSourceExpectation("Microsoft-DotNETCore-SampleProfiler", false, false, true) + } + }; + } + + [Theory] + [MemberData(nameof(ProviderSourcePrintCases))] + public void PrintProviders_Sources(string[] providersArg, string clreventsArg, string clreventLevel, string[] profiles, ProviderSourceExpectation[] expectations) + { + StringWriter capture = new(); + TextWriter original = Console.Out; + try + { + Console.SetOut(capture); + _ = ProviderUtils.ComputeProviderConfig(providersArg, clreventsArg, clreventLevel, profiles, true); + string output = capture.ToString(); + foreach (ProviderSourceExpectation e in expectations) + { + string line = output.WhereLineContains(e.Name); + Assert.Equal(e.FromProviders, line.Contains("--providers", StringComparison.Ordinal)); + Assert.Equal(e.FromClrEvents, line.Contains("--clrevents", StringComparison.Ordinal)); + Assert.Equal(e.FromProfile, line.Contains("--profile", StringComparison.Ordinal)); + } + } + finally + { + Console.SetOut(original); + } + } + + public static IEnumerable MergingCases() + { + yield return new object[] { new[]{ "MyProvider:0x1:5", "MyProvider:0x2:LogAlways" }, string.Empty, string.Empty, Array.Empty(), new EventPipeProvider("MyProvider", EventLevel.LogAlways, 0x3) }; + yield return new object[] { new[]{ "MyProvider:0x1:Error", "myprovider:0x2:Critical" }, string.Empty, string.Empty, Array.Empty(), new EventPipeProvider("MyProvider", EventLevel.Error, 0x3) }; + } + + [Theory] + [MemberData(nameof(MergingCases))] + public void MergeDuplicateProviders(string[] providersArg, string clreventsArg, string clreventLevel, string[] profiles, EventPipeProvider expected) + { + List actual = ProviderUtils.ComputeProviderConfig(providersArg, clreventsArg, clreventLevel, profiles); + EventPipeProvider single = Assert.Single(actual); + Assert.Equal(expected, single); + } + + [Theory] + [InlineData("MyProvider:0x0:9", EventLevel.Verbose)] + public void ProviderEventLevel_Clamps(string providersArg, EventLevel expected) + { + string[] providers = providersArg.Split(',', StringSplitOptions.RemoveEmptyEntries); + EventPipeProvider actual = Assert.Single(ProviderUtils.ComputeProviderConfig(providers, string.Empty, string.Empty, Array.Empty())); + Assert.Equal(expected, actual.EventLevel); + } + + public static IEnumerable ClrEventLevelCases() + { + yield return new object[] { Array.Empty(), "gc+jit", "5", Array.Empty(), new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Verbose, 0x1 | 0x10) }; + } + + [Theory] + [MemberData(nameof(ClrEventLevelCases))] + public void CLREvents_NumericLevel_Parses(string[] providersArg, string clreventsArg, string clreventLevel, string[] profiles, EventPipeProvider expected) + { + List actual = ProviderUtils.ComputeProviderConfig(providersArg, clreventsArg, clreventLevel, profiles); + EventPipeProvider single = Assert.Single(actual, p => p.Name == "Microsoft-Windows-DotNETRuntime"); + Assert.Equal(expected, single); + } + } + + internal static class TestStringExtensions + { + extension(string text) + { + public string WhereLineContains(string search) => string.Join(Environment.NewLine, + text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None) + .Where(l => l.Contains(search, StringComparison.Ordinal))); + } + } +} diff --git a/src/tests/dotnet-trace/ProviderParsing.cs b/src/tests/dotnet-trace/ProviderParsing.cs deleted file mode 100644 index bab3b6fb0d..0000000000 --- a/src/tests/dotnet-trace/ProviderParsing.cs +++ /dev/null @@ -1,341 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Diagnostics.NETCore.Client; -using Xunit; - -namespace Microsoft.Diagnostics.Tools.Trace -{ - public class ProviderParsingTests - { - [Theory] - [InlineData("VeryCoolProvider:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\"")] - [InlineData("VeryCoolProvider:1:5:FilterAndPayloadSpecs=\"QuotedValue\"")] - public void ValidProvider_CorrectlyParses(string providerToParse) - { - List parsedProviders = Extensions.ToProviders(providerToParse); - Assert.True(parsedProviders.Count == 1); - EventPipeProvider provider = parsedProviders.First(); - Assert.True(provider.Name == "VeryCoolProvider"); - Assert.True(provider.Keywords == 1); - Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Verbose); - Assert.True(provider.Arguments.Count == 1); - Assert.True(provider.Arguments["FilterAndPayloadSpecs"] == "QuotedValue"); - } - - [Theory] - [InlineData("VeryCoolProvider:0x1:5:FilterAndPayloadSpecs=\"QuotedValue:-\r\nQuoted/Value\"")] - public void ValidProviderFilter_CorrectlyParses(string providerToParse) - { - List parsedProviders = Extensions.ToProviders(providerToParse); - Assert.True(parsedProviders.Count == 1); - EventPipeProvider provider = parsedProviders.First(); - Assert.True(provider.Name == "VeryCoolProvider"); - Assert.True(provider.Keywords == 1); - Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Verbose); - Assert.True(provider.Arguments.Count == 1); - Assert.True(provider.Arguments["FilterAndPayloadSpecs"] == "QuotedValue:-\r\nQuoted/Value"); - } - - [Theory] - [InlineData(null)] - [InlineData(",")] - public void EmptyProvider_CorrectlyThrows(string providerToParse) - { - Assert.Throws(() => Extensions.ToProviders(providerToParse)); - } - - [Theory] - [InlineData(":::")] - [InlineData(":1:1")] - public void InvalidProvider_CorrectlyThrows(string providerToParse) - { - Assert.Throws(() => Extensions.ToProviders(providerToParse)); - } - - [Theory] - [InlineData("VeryCoolProvider:0xFFFFFFFFFFFFFFFF:5:FilterAndPayloadSpecs=\"QuotedValue\"")] - public void ValidProviderKeyword_CorrectlyParses(string providerToParse) - { - List parsedProviders = Extensions.ToProviders(providerToParse); - Assert.True(parsedProviders.Count == 1); - EventPipeProvider provider = parsedProviders.First(); - Assert.True(provider.Name == "VeryCoolProvider"); - Assert.True(provider.Keywords == (long)(-1)); - Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Verbose); - Assert.True(provider.Arguments.Count == 1); - Assert.True(provider.Arguments["FilterAndPayloadSpecs"] == "QuotedValue"); - } - - [Theory] - [InlineData("VeryCoolProvider::4:FilterAndPayloadSpecs=\"QuotedValue\"")] - [InlineData("VeryCoolProvider:::FilterAndPayloadSpecs=\"QuotedValue\"")] - public void ValidProviderEventLevel_CorrectlyParses(string providerToParse) - { - List parsedProviders = Extensions.ToProviders(providerToParse); - Assert.Equal(1, parsedProviders.Count); - EventPipeProvider provider = parsedProviders.First(); - Assert.Equal("VeryCoolProvider", provider.Name); - Assert.Equal(0, provider.Keywords); - Assert.Equal(System.Diagnostics.Tracing.EventLevel.Informational, provider.EventLevel); - Assert.Equal(1, provider.Arguments.Count); - Assert.Equal("QuotedValue", provider.Arguments["FilterAndPayloadSpecs"]); - } - - [Theory] - [InlineData("VeryCoolProvider:0xFFFFFFFFFFFFFFFFF:5:FilterAndPayloadSpecs=\"QuotedValue\"")] - [InlineData("VeryCoolProvider:0x10000000000000000::FilterAndPayloadSpecs=\"QuotedValue\"")] - public void OutOfRangekeyword_CorrectlyThrows(string providerToParse) - { - Assert.Throws(() => Extensions.ToProviders(providerToParse)); - } - - [Theory] - [InlineData("VeryCoolProvider:__:5:FilterAndPayloadSpecs=\"QuotedValue\"")] - [InlineData("VeryCoolProvider:gh::FilterAndPayloadSpecs=\"QuotedValue\"")] - public void Invalidkeyword_CorrectlyThrows(string providerToParse) - { - Assert.Throws(() => Extensions.ToProviders(providerToParse)); - } - - [Theory] - [InlineData("ProviderOne:0x1:1:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:2:2:key=value,ProviderThree:3:3:key=value")] - [InlineData("ProviderOne:1:1:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:0x2:2:key=value,ProviderThree:0x3:3:key=value")] - public void MultipleValidProviders_CorrectlyParses(string providersToParse) - { - List parsedProviders = Extensions.ToProviders(providersToParse); - Assert.True(parsedProviders.Count == 3); - EventPipeProvider providerOne = parsedProviders[0]; - EventPipeProvider providerTwo = parsedProviders[1]; - EventPipeProvider providerThree = parsedProviders[2]; - - Assert.True(providerOne.Name == "ProviderOne"); - Assert.True(providerOne.Keywords == 1); - Assert.True(providerOne.EventLevel == System.Diagnostics.Tracing.EventLevel.Critical); - Assert.True(providerOne.Arguments.Count == 1); - Assert.True(providerOne.Arguments["FilterAndPayloadSpecs"] == "QuotedValue"); - - Assert.True(providerTwo.Name == "ProviderTwo"); - Assert.True(providerTwo.Keywords == 2); - Assert.True(providerTwo.EventLevel == System.Diagnostics.Tracing.EventLevel.Error); - Assert.True(providerTwo.Arguments.Count == 1); - Assert.True(providerTwo.Arguments["key"] == "value"); - - Assert.True(providerThree.Name == "ProviderThree"); - Assert.True(providerThree.Keywords == 3); - Assert.True(providerThree.EventLevel == System.Diagnostics.Tracing.EventLevel.Warning); - Assert.True(providerThree.Arguments.Count == 1); - Assert.True(providerThree.Arguments["key"] == "value"); - } - - [Theory] - [InlineData("ProviderOne:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\",:2:2:key=value,ProviderThree:3:3:key=value")] - [InlineData("ProviderOne:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:2:2:key=value,:3:3:key=value")] - [InlineData("ProviderOne:0x1:5:key=value,key=FilterAndPayloadSpecs=\"QuotedValue\",:2:2:key=value,ProviderThree:3:3:key=value")] - public void MultipleValidProvidersWithOneInvalidProvider_CorrectlyThrows(string providersToParse) - { - Assert.Throws(() => Extensions.ToProviders(providersToParse)); - } - - [Theory] - [InlineData("ProviderOne:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:0xFFFFFFFFFFFFFFFFF:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderThree:3:3:key=value")] - [InlineData("ProviderOne:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:0x10000000000000000:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderThree:3:3:key=value")] - [InlineData("ProviderOne:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:18446744073709551615:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderThree:3:3:key=value")] - public void MultipleValidProvidersWithOneOutOfRangeKeyword_CorrectlyThrows(string providersToParse) - { - Assert.Throws(() => Extensions.ToProviders(providersToParse)); - } - - [Theory] - [InlineData("ProviderOne:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:__:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderThree:3:3:key=value")] - [InlineData("ProviderOne:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:gh:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderThree:3:3:key=value")] - [InlineData("ProviderOne:0x1:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:$:5:FilterAndPayloadSpecs=\"QuotedValue\",ProviderThree:3:3:key=value")] - public void MultipleValidProvidersWithOneInvalidKeyword_CorrectlyThrows(string providersToParse) - { - Assert.Throws(() => Extensions.ToProviders(providersToParse)); - } - - [Theory] - [InlineData("ProviderOne:0x1:1:FilterAndPayloadSpecs=\"QuotedValue:-\r\nQuoted/Value:-A=B;C=D;\",ProviderTwo:2:2:FilterAndPayloadSpecs=\"QuotedValue\",ProviderThree:3:3:FilterAndPayloadSpecs=\"QuotedValue:-\r\nQuoted/Value:-A=B;C=D;\"")] - public void MultipleProvidersWithComplexFilters_CorrectlyParse(string providersToParse) - { - List parsedProviders = Extensions.ToProviders(providersToParse); - Assert.True(parsedProviders.Count == 3); - EventPipeProvider providerOne = parsedProviders[0]; - EventPipeProvider providerTwo = parsedProviders[1]; - EventPipeProvider providerThree = parsedProviders[2]; - - Assert.True(providerOne.Name == "ProviderOne"); - Assert.True(providerOne.Keywords == 1); - Assert.True(providerOne.EventLevel == System.Diagnostics.Tracing.EventLevel.Critical); - Assert.True(providerOne.Arguments.Count == 1); - Assert.True(providerOne.Arguments["FilterAndPayloadSpecs"] == "QuotedValue:-\r\nQuoted/Value:-A=B;C=D;"); - - Assert.True(providerTwo.Name == "ProviderTwo"); - Assert.True(providerTwo.Keywords == 2); - Assert.True(providerTwo.EventLevel == System.Diagnostics.Tracing.EventLevel.Error); - Assert.True(providerTwo.Arguments.Count == 1); - Assert.True(providerTwo.Arguments["FilterAndPayloadSpecs"] == "QuotedValue"); - - Assert.True(providerThree.Name == "ProviderThree"); - Assert.True(providerThree.Keywords == 3); - Assert.True(providerThree.EventLevel == System.Diagnostics.Tracing.EventLevel.Warning); - Assert.True(providerThree.Arguments.Count == 1); - Assert.True(providerThree.Arguments["FilterAndPayloadSpecs"] == "QuotedValue:-\r\nQuoted/Value:-A=B;C=D;"); - } - - [Fact] - public void ProvidersWithComplexFilters_CorrectlyParse() - { - string providersToParse = @"MyProvider:::A=B;C=D"; - List parsedProviders = Extensions.ToProviders(providersToParse); - Assert.Single(parsedProviders); - EventPipeProvider providerOne = parsedProviders[0]; - Assert.Equal("MyProvider", providerOne.Name); - Assert.Equal(2, providerOne.Arguments.Count); - Assert.Equal("B", providerOne.Arguments["A"]); - Assert.Equal("D", providerOne.Arguments["C"]); - - providersToParse = @"MyProvider:::A=B;C=""D"",MyProvider2:::A=1;B=2;"; - parsedProviders = Extensions.ToProviders(providersToParse); - Assert.Equal(2, parsedProviders.Count); - providerOne = parsedProviders[0]; - EventPipeProvider providerTwo = parsedProviders[1]; - Assert.Equal("MyProvider", providerOne.Name); - Assert.Equal("MyProvider2", providerTwo.Name); - Assert.Equal(2, providerOne.Arguments.Count); - Assert.Equal("B", providerOne.Arguments["A"]); - Assert.Equal("D", providerOne.Arguments["C"]); - Assert.Equal(2, providerTwo.Arguments.Count); - Assert.Equal("1", providerTwo.Arguments["A"]); - Assert.Equal("2", providerTwo.Arguments["B"]); - - providersToParse = @"MyProvider:::A=""B;C=D"",MyProvider2:::A=""spaced words"";C=1285;D=Spaced Words 2"; - parsedProviders = Extensions.ToProviders(providersToParse); - Assert.Equal(2, parsedProviders.Count); - providerOne = parsedProviders[0]; - providerTwo = parsedProviders[1]; - Assert.Equal("MyProvider", providerOne.Name); - Assert.Equal("MyProvider2", providerTwo.Name); - Assert.Equal(1, providerOne.Arguments.Count); - Assert.Equal(3, providerTwo.Arguments.Count); - Assert.Equal("B;C=D", providerOne.Arguments["A"]); - Assert.Equal("spaced words", providerTwo.Arguments["A"]); - Assert.Equal("Spaced Words 2", providerTwo.Arguments["D"]); - Assert.Equal("1285", providerTwo.Arguments["C"]); - } - - [Theory] - [InlineData("ProviderOne:0x1:Verbose")] - [InlineData("ProviderOne:0x1:verbose")] - public void TextLevelProviderSpecVerbose_CorrectlyParse(string providerToParse) - { - List parsedProviders = Extensions.ToProviders(providerToParse); - Assert.True(parsedProviders.Count == 1); - Assert.True(parsedProviders[0].Name == "ProviderOne"); - Assert.True(parsedProviders[0].Keywords == 1); - Assert.True(parsedProviders[0].EventLevel == System.Diagnostics.Tracing.EventLevel.Verbose); - } - - [Theory] - [InlineData("ProviderOne:0x1:Informational")] - [InlineData("ProviderOne:0x1:INFORMATIONAL")] - public void TextLevelProviderSpecInformational_CorrectlyParse(string providerToParse) - { - List parsedProviders = Extensions.ToProviders(providerToParse); - Assert.True(parsedProviders.Count == 1); - Assert.True(parsedProviders[0].Name == "ProviderOne"); - Assert.True(parsedProviders[0].Keywords == 1); - Assert.True(parsedProviders[0].EventLevel == System.Diagnostics.Tracing.EventLevel.Informational); - } - - [Theory] - [InlineData("ProviderOne:0x1:LogAlways")] - [InlineData("ProviderOne:0x1:LogAlwayS")] - public void TextLevelProviderSpecLogAlways_CorrectlyParse(string providerToParse) - { - List parsedProviders = Extensions.ToProviders(providerToParse); - Assert.True(parsedProviders.Count == 1); - Assert.True(parsedProviders[0].Name == "ProviderOne"); - Assert.True(parsedProviders[0].Keywords == 1); - Assert.True(parsedProviders[0].EventLevel == System.Diagnostics.Tracing.EventLevel.LogAlways); - } - - [Theory] - [InlineData("ProviderOne:0x1:Error")] - [InlineData("ProviderOne:0x1:ERRor")] - public void TextLevelProviderSpecError_CorrectlyParse(string providerToParse) - { - List parsedProviders = Extensions.ToProviders(providerToParse); - Assert.True(parsedProviders.Count == 1); - Assert.True(parsedProviders[0].Name == "ProviderOne"); - Assert.True(parsedProviders[0].Keywords == 1); - Assert.True(parsedProviders[0].EventLevel == System.Diagnostics.Tracing.EventLevel.Error); - } - - [Theory] - [InlineData("ProviderOne:0x1:Critical")] - [InlineData("ProviderOne:0x1:CRITICAL")] - public void TextLevelProviderSpecCritical_CorrectlyParse(string providerToParse) - { - List parsedProviders = Extensions.ToProviders(providerToParse); - Assert.True(parsedProviders.Count == 1); - Assert.True(parsedProviders[0].Name == "ProviderOne"); - Assert.True(parsedProviders[0].Keywords == 1); - Assert.True(parsedProviders[0].EventLevel == System.Diagnostics.Tracing.EventLevel.Critical); - } - - [Theory] - [InlineData("ProviderOne:0x1:Warning")] - [InlineData("ProviderOne:0x1:warning")] - public void TextLevelProviderSpecWarning_CorrectlyParse(string providerToParse) - { - List parsedProviders = Extensions.ToProviders(providerToParse); - Assert.True(parsedProviders.Count == 1); - Assert.True(parsedProviders[0].Name == "ProviderOne"); - Assert.True(parsedProviders[0].Keywords == 1); - Assert.True(parsedProviders[0].EventLevel == System.Diagnostics.Tracing.EventLevel.Warning); - } - - [Theory] - [InlineData("ProviderOne:0x1:UnknownLevel")] - public void TextLevelProviderSpec_CorrectlyThrows(string providerToParse) - { - Assert.Throws(() => Extensions.ToProviders(providerToParse)); - } - - [Theory] - [InlineData("DupeProvider,DupeProvider:0xF:LogAlways")] - public void DeDupeProviders_DefaultAndSpecified(string providersToParse) - { - List parsedProviders = Extensions.ToProviders(providersToParse); - Assert.Equal("DupeProvider", parsedProviders.First().Name); - Assert.Equal(1, parsedProviders.Count); - Assert.Equal(0xF, parsedProviders.First().Keywords); - Assert.Equal(System.Diagnostics.Tracing.EventLevel.LogAlways, parsedProviders.First().EventLevel); - Assert.Null(parsedProviders.First().Arguments); - } - - [Theory] - [InlineData("DupeProvider:0xF0:Informational,DupeProvider:0xF:Verbose")] - public void DeDupeProviders_BothSpecified(string providersToParse) - { - List parsedProviders = Extensions.ToProviders(providersToParse); - Assert.Equal("DupeProvider", parsedProviders.First().Name); - Assert.Equal(1, parsedProviders.Count); - Assert.Equal(0xFF, parsedProviders.First().Keywords); - Assert.Equal(System.Diagnostics.Tracing.EventLevel.Verbose, parsedProviders.First().EventLevel); - Assert.Null(parsedProviders.First().Arguments); - } - - [Theory] - [InlineData("DupeProvider:::key=value,DupeProvider:::key=value")] - public void DeDupeProviders_FilterDataThrows(string providersToParse) - { - Assert.Throws(() => Extensions.ToProviders(providersToParse)); - } - } -}