diff --git a/src/Tools/Common/Commands/Utils.cs b/src/Tools/Common/Commands/Utils.cs index 68d2f8241e..e6b303c49d 100644 --- a/src/Tools/Common/Commands/Utils.cs +++ b/src/Tools/Common/Commands/Utils.cs @@ -217,6 +217,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 1702d0213c..2ac31e5434 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 7376b72ca0..c9d4124f78 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -63,7 +63,7 @@ private 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. /// - internal 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) + internal 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; @@ -119,37 +119,35 @@ internal async Task Collect(CancellationToken ct, CommandLineConfiguration 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", Console); + 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) @@ -164,31 +162,6 @@ internal async Task Collect(CancellationToken ct, CommandLineConfiguration } } - // 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); @@ -272,7 +245,7 @@ internal async Task Collect(CancellationToken ct, CommandLineConfiguration } - 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); @@ -374,8 +347,6 @@ internal async Task Collect(CancellationToken ct, CommandLineConfiguration { ConsoleWriteLine($"Trace Duration : {duration:dd\\:hh\\:mm\\:ss}"); } - - ConsoleWriteLine(); ConsoleWriteLine(); EventMonitor eventMonitor = null; @@ -418,10 +389,19 @@ internal async Task Collect(CancellationToken ct, CommandLineConfiguration } FileInfo fileInfo = new(output.FullName); + bool wroteStatus = false; Action printStatus = () => { if (printStatusOverTime && rewriter.IsRewriteConsoleLineSupported) { - rewriter?.RewriteConsoleLine(); + if (wroteStatus) + { + rewriter?.RewriteConsoleLine(); + } + else + { + // First time writing status, so don't rewrite console yet. + wroteStatus = true; + } fileInfo.Refresh(); ConsoleWriteLine($"[{stopwatch.Elapsed:dd\\:hh\\:mm\\:ss}]\tRecording trace {GetSize(fileInfo.Length)}"); ConsoleWriteLine("Press or to exit..."); @@ -536,20 +516,6 @@ internal async Task Collect(CancellationToken ct, CommandLineConfiguration return ret; } - private 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) @@ -577,13 +543,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, @@ -600,17 +566,20 @@ public static Command CollectCommand() collectCommand.SetAction((parseResult, ct) => { CollectCommandHandler handler = new(); + string providersValue = parseResult.GetValue(CommonOptions.ProvidersOption) ?? string.Empty; + string profileValue = parseResult.GetValue(CommonOptions.ProfileOption) ?? string.Empty; + return handler.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), @@ -634,53 +603,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..bdd3ea73bf --- /dev/null +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -0,0 +1,313 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Diagnostics.Tools.Common; +using Microsoft.Internal.Common.Utils; + +namespace Microsoft.Diagnostics.Tools.Trace +{ + internal partial class CollectLinuxCommandHandler + { + private bool s_stopTracing; + private Stopwatch s_stopwatch = new(); + private LineRewriter s_rewriter; + private bool s_printingStatus; + + internal sealed record CollectLinuxArgs( + CancellationToken Ct, + string[] Providers, + string ClrEventLevel, + string ClrEvents, + string[] PerfEvents, + string[] Profiles, + FileInfo Output, + TimeSpan Duration); + + public CollectLinuxCommandHandler(IConsole console = null) + { + Console = console ?? new DefaultConsole(false); + s_rewriter = new LineRewriter(Console) { LineToClear = Console.CursorTop - 1 }; + } + + /// + /// 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. + /// + internal int CollectLinux(CollectLinuxArgs args) + { + if (!OperatingSystem.IsLinux()) + { + Console.Error.WriteLine("The collect-linux command is only supported on Linux."); + return (int)ReturnCode.PlatformNotSupportedError; + } + + args.Ct.Register(() => s_stopTracing = true); + int ret = (int)ReturnCode.TracingError; + string scriptPath = null; + try + { + Console.CursorVisible = false; + byte[] command = BuildRecordTraceArgs(args, out scriptPath); + + if (args.Duration != default) + { + System.Timers.Timer durationTimer = new(args.Duration.TotalMilliseconds); + durationTimer.Elapsed += (sender, e) => + { + durationTimer.Stop(); + s_stopTracing = true; + }; + durationTimer.Start(); + } + s_stopwatch.Start(); + ret = RecordTraceInvoker(command, (UIntPtr)command.Length, OutputHandler); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[ERROR] {ex}"); + ret = (int)ReturnCode.TracingError; + } + finally + { + if (!string.IsNullOrEmpty(scriptPath)) + { + try + { + if (File.Exists(scriptPath)) + { + File.Delete(scriptPath); + } + } catch { } + } + } + + return ret; + } + + public static Command CollectLinuxCommand() + { + Command collectLinuxCommand = new("collect-linux") + { + CommonOptions.ProvidersOption, + CommonOptions.CLREventLevelOption, + CommonOptions.CLREventsOption, + PerfEventsOption, + CommonOptions.ProfileOption, + CommonOptions.OutputPathOption, + CommonOptions.DurationOption, + }; + 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; + CollectLinuxCommandHandler handler = new(); + + int rc = handler.CollectLinux(new CollectLinuxArgs( + Ct: ct, + 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))); + return Task.FromResult(rc); + }); + + return collectLinuxCommand; + } + + private byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath) + { + scriptPath = null; + List recordTraceArgs = new(); + + 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" }; + } + + StringBuilder scriptBuilder = new(); + List providerCollection = ProviderUtils.ComputeProviderConfig(args.Providers, args.ClrEvents, args.ClrEventLevel, profiles, true, "collect-linux", Console); + 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"); + } + + List linuxEventLines = new(); + foreach (string profile in profiles) + { + Profile traceProfile = ListProfilesCommandHandler.TraceProfiles + .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); + + if (traceProfile != null && + !string.IsNullOrEmpty(traceProfile.VerbExclusivity) && + traceProfile.VerbExclusivity.Equals("collect-linux", StringComparison.OrdinalIgnoreCase)) + { + recordTraceArgs.Add(traceProfile.CollectLinuxArgs); + linuxEventLines.Add($"{traceProfile.Name,-80}--profile"); + } + } + + 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]; + linuxEventLines.Add($"{perfEvent,-80}--perf-events"); + scriptBuilder.Append($"let {perfEventName} = event_from_tracefs(\"{perfProvider}\", \"{perfEventName}\");\nrecord_event({perfEventName});\n\n"); + } + + if (linuxEventLines.Count > 0) + { + Console.WriteLine($"{("Linux Perf Events"),-80}Enabled By"); + foreach (string line in linuxEventLines) + { + Console.WriteLine(line); + } + } + else + { + Console.WriteLine("No Linux Perf Events enabled."); + } + Console.WriteLine(); + + FileInfo resolvedOutput = ResolveOutputPath(args.Output); + recordTraceArgs.Add($"--out"); + recordTraceArgs.Add(resolvedOutput.FullName); + Console.WriteLine($"Output File : {resolvedOutput.FullName}"); + Console.WriteLine(); + + string scriptText = scriptBuilder.ToString(); + scriptPath = Path.ChangeExtension(resolvedOutput.FullName, ".script"); + File.WriteAllText(scriptPath, scriptText); + + recordTraceArgs.Add("--script-file"); + recordTraceArgs.Add(scriptPath); + + string options = string.Join(' ', recordTraceArgs); + return Encoding.UTF8.GetBytes(options); + } + + private static FileInfo ResolveOutputPath(FileInfo output) + { + if (!string.Equals(output.Name, CommonOptions.DefaultTraceName, StringComparison.OrdinalIgnoreCase)) + { + return output; + } + + DateTime now = DateTime.Now; + return new FileInfo($"trace_{now:yyyyMMdd}_{now:HHmmss}.nettrace"); + } + + private int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) + { + OutputType ot = (OutputType)type; + if (dataLen != UIntPtr.Zero && (ulong)dataLen <= int.MaxValue) + { + string text = Marshal.PtrToStringUTF8(data, (int)dataLen); + if (!string.IsNullOrEmpty(text) && + !text.StartsWith("Recording started", StringComparison.OrdinalIgnoreCase)) + { + if (ot == OutputType.Error) + { + Console.Error.WriteLine(text); + s_stopTracing = true; + } + else + { + Console.Out.WriteLine(text); + } + } + } + + if (ot == OutputType.Progress) + { + if (s_printingStatus) + { + s_rewriter.RewriteConsoleLine(); + } + else + { + s_printingStatus = true; + } + Console.Out.WriteLine($"[{s_stopwatch.Elapsed:dd\\:hh\\:mm\\:ss}]\tRecording trace."); + Console.Out.WriteLine("Press or to exit..."); + + if (Console.KeyAvailable && Console.ReadKey(true).Key == ConsoleKey.Enter) + { + s_stopTracing = true; + } + } + + return s_stopTracing ? 1 : 0; + } + + 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)] + internal delegate int recordTraceCallback( + [In] uint type, + [In] IntPtr data, + [In] UIntPtr dataLen); + + [LibraryImport("recordtrace", EntryPoint = "RecordTrace")] + private static partial int RunRecordTrace( + byte[] command, + UIntPtr commandLen, + recordTraceCallback callback); + +#region testing seams + internal Func RecordTraceInvoker { get; set; } = RunRecordTrace; + internal IConsole Console { get; set; } +#endregion + } +} 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..432e6599ae 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs @@ -14,13 +14,30 @@ 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, and Compilation events + 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 +59,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 +123,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 54% rename from src/Tools/dotnet-trace/Extensions.cs rename to src/Tools/dotnet-trace/ProviderUtils.cs index b9bd829c70..ed43ba4148 100644 --- a/src/Tools/dotnet-trace/Extensions.cs +++ b/src/Tools/dotnet-trace/ProviderUtils.cs @@ -3,14 +3,17 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.Tracing; +using System.IO; using System.Linq; using System.Text.RegularExpressions; using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Diagnostics.Tools.Common; namespace Microsoft.Diagnostics.Tools.Trace { - internal static class Extensions + internal static class ProviderUtils { public static string CLREventProviderName = "Microsoft-Windows-DotNETRuntime"; @@ -23,6 +26,7 @@ internal static class Extensions { "gc", 0x1 }, { "gchandle", 0x2 }, { "fusion", 0x4 }, + { "assemblyloader", 0x4 }, { "loader", 0x8 }, { "jit", 0x10 }, { "ngen", 0x20 }, @@ -42,6 +46,7 @@ internal static class Extensions { "gcsampledobjectallocationhigh", 0x200000 }, { "gcheapsurvivalandmovement", 0x400000 }, { "gcheapcollect", 0x800000 }, + { "managedheadcollect", 0x800000 }, { "gcheapandtypenames", 0x1000000 }, { "gcsampledobjectallocationlow", 0x2000000 }, { "perftrack", 0x20000000 }, @@ -55,57 +60,145 @@ 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, IConsole console = null) + { + console ??= new DefaultConsole(false); + Dictionary merged = new(StringComparer.OrdinalIgnoreCase); + Dictionary providerSources = new(StringComparer.OrdinalIgnoreCase); + + foreach (string providerArg in providersArg) + { + EventPipeProvider provider = ToProvider(providerArg, console); + 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); + } + } + + foreach (string profile in profiles) + { + 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}`."); + } + + 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 + } + } + + 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, console); + } + + 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 ArgumentNullException(nameof(providersRawInput)); + throw new ArgumentException($"Provider \"{providerConfigA.Name}\" is declared multiple times with filter arguments."); } - if (string.IsNullOrWhiteSpace(providersRawInput)) + return new EventPipeProvider(providerConfigA.Name, level, providerConfigA.Keywords | providerConfigB.Keywords, providerConfigA.Arguments ?? providerConfigB.Arguments); + } + + private static void PrintProviders(IReadOnlyList providers, Dictionary enabledBy, IConsole console) + { + if (providers.Count == 0) { - return new List(); + console.WriteLine("No .NET providers were configured."); + console.WriteLine(""); + return; } - 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(); + 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) { @@ -167,7 +260,7 @@ private static EventLevel GetEventLevel(string token) } } - private static EventPipeProvider ToProvider(string provider) + private static EventPipeProvider ToProvider(string provider, IConsole console) { if (string.IsNullOrWhiteSpace(provider)) { @@ -182,7 +275,7 @@ private static EventPipeProvider ToProvider(string provider) // Check if the supplied provider is a GUID and not a name. if (Guid.TryParse(providerName, out _)) { - Console.WriteLine($"Warning: --provider argument {providerName} appears to be a GUID which is not supported by dotnet-trace. Providers need to be referenced by their textual name."); + console.WriteLine($"Warning: --provider argument {providerName} appears to be a GUID which is not supported by dotnet-trace. Providers need to be referenced by their textual name."); } if (string.IsNullOrWhiteSpace(providerName)) diff --git a/src/Tools/dotnet-trace/dotnet-trace.csproj b/src/Tools/dotnet-trace/dotnet-trace.csproj index 71b6aacb2e..c7ce23bb72 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/Common/MockConsole.cs b/src/tests/Common/MockConsole.cs index f21858dddc..7968edcddd 100644 --- a/src/tests/Common/MockConsole.cs +++ b/src/tests/Common/MockConsole.cs @@ -158,7 +158,7 @@ public void AssertLinesEqual(int startLine, params string[] expectedLines) } } - public void AssertSanitizedLinesEqual(Func sanitizer, params string[] expectedLines) + public void AssertSanitizedLinesEqual(Func sanitizer, bool ignorePastExpected = false, params string[] expectedLines) { string[] actualLines = Lines; if (sanitizer is not null) @@ -178,9 +178,12 @@ public void AssertSanitizedLinesEqual(Func sanitizer, params $"Line {i,2} Actual : {actualLines[i]}"); } } - for (int i = expectedLines.Length; i < actualLines.Length; i++) + if (!ignorePastExpected) { - Assert.True(string.IsNullOrEmpty(actualLines[i]), $"Actual line #{i} beyond expected lines is not empty: {actualLines[i]}"); + for (int i = expectedLines.Length; i < actualLines.Length; i++) + { + Assert.True(string.IsNullOrEmpty(actualLines[i]), $"Actual line #{i} beyond expected lines is not empty: {actualLines[i]}"); + } } } } 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/CollectCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectCommandFunctionalTests.cs index f6de43dfed..c63663b319 100644 --- a/src/tests/dotnet-trace/CollectCommandFunctionalTests.cs +++ b/src/tests/dotnet-trace/CollectCommandFunctionalTests.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Microsoft.Diagnostics.Tests.Common; using Microsoft.Diagnostics.Tools.Trace; +using Microsoft.Internal.Common.Utils; using Xunit; namespace Microsoft.Diagnostics.Tools.Trace @@ -23,8 +24,8 @@ public sealed record CollectArgs( CommandLineConfiguration cliConfig = null, int processId = -1, uint buffersize = 1, - string providers = "", - string profile = "", + string[] providers = null, + string[] profile = null, int formatValue = (int)TraceFileFormat.NetTrace, TimeSpan duration = default, string clrevents = "", @@ -51,14 +52,25 @@ public sealed record CollectArgs( public async Task CollectCommandProviderConfigurationConsolidation(CollectArgs args, string[] expectedSubset) { MockConsole console = new(200, 30); - string[] rawLines = await RunAsync(args, console).ConfigureAwait(true); - console.AssertSanitizedLinesEqual(CollectSanitizer, expectedSubset); + int exitCode = await RunAsync(args, console).ConfigureAwait(true); + Assert.Equal((int)ReturnCode.Ok, exitCode); + console.AssertSanitizedLinesEqual(CollectSanitizer, expectedLines: expectedSubset); byte[] expected = Encoding.UTF8.GetBytes(ExpectedPayload); Assert.Equal(expected, args.EventStream.ToArray()); } - private static async Task RunAsync(CollectArgs config, MockConsole console) + [Theory] + [MemberData(nameof(InvalidProviders))] + public async Task CollectCommandInvalidProviderConfiguration_Throws(CollectArgs args, string[] expectedException) + { + MockConsole console = new(200, 30); + int exitCode = await RunAsync(args, console).ConfigureAwait(true); + Assert.Equal((int)ReturnCode.TracingError, exitCode); + console.AssertSanitizedLinesEqual(CollectSanitizer, true, expectedException); + } + + private static async Task RunAsync(CollectArgs config, MockConsole console) { var handler = new CollectCommandHandler(); handler.StartTraceSessionAsync = (client, cfg, ct) => Task.FromResult(new TestCollectSession()); @@ -66,14 +78,14 @@ private static async Task RunAsync(CollectArgs config, MockConsole con handler.CollectSessionEventStream = (name) => config.EventStream; handler.Console = console; - int exit = await handler.Collect( + return await handler.Collect( config.ct, config.cliConfig, config.ProcessId, config.Output, config.buffersize, - config.providers, - config.profile, + config.providers ?? Array.Empty(), + config.profile ?? Array.Empty(), config.Format, config.duration, config.clrevents, @@ -87,12 +99,7 @@ private static async Task RunAsync(CollectArgs config, MockConsole con config.stoppingEventPayloadFilter, config.rundown, config.dsrouter - ).ConfigureAwait(true); - if (exit != 0) - { - throw new InvalidOperationException($"Collect exited with return code {exit}."); - } - return console.Lines; + ).ConfigureAwait(false); } private static string[] CollectSanitizer(string[] lines) @@ -123,29 +130,28 @@ public void Stop() {} public static IEnumerable BasicCases() { - FileInfo fi = new("trace.nettrace"); yield return new object[] { new CollectArgs(), ExpectProvidersWithMessages( new[] { - "No profile or providers specified, defaulting to trace profile 'cpu-sampling'" + "No profile or providers specified, defaulting to trace profiles 'dotnet-common' + 'dotnet-sampled-thread-time'." }, - FormatProvider("Microsoft-DotNETCore-SampleProfiler", "0000F00000000000", "Informational", 4, "--profile"), - FormatProvider("Microsoft-Windows-DotNETRuntime", "00000014C14FCCBD", "Informational", 4, "--profile")) + FormatProvider("Microsoft-Windows-DotNETRuntime", "000000100003801D", "Informational", 4, "--profile"), + FormatProvider("Microsoft-DotNETCore-SampleProfiler", "0000F00000000000", "Informational", 4, "--profile")) }; yield return new object[] { - new CollectArgs(providers: "Foo:0x1:4"), + new CollectArgs(providers: new[] { "Foo:0x1:4" }), ExpectProviders( FormatProvider("Foo", "0000000000000001", "Informational", 4, "--providers")) }; yield return new object[] { - new CollectArgs(providers: "Foo:0x1:4,Bar:0x2:4"), + new CollectArgs(providers: new[] { "Foo:0x1:4", "Bar:0x2:4" }), ExpectProviders( FormatProvider("Foo", "0000000000000001", "Informational", 4, "--providers"), FormatProvider("Bar", "0000000000000002", "Informational", 4, "--providers")) @@ -153,36 +159,36 @@ public static IEnumerable BasicCases() yield return new object[] { - new CollectArgs(profile: "cpu-sampling"), + new CollectArgs(profile: new[] { "dotnet-common", "dotnet-sampled-thread-time" }), ExpectProviders( - FormatProvider("Microsoft-DotNETCore-SampleProfiler", "0000F00000000000", "Informational", 4, "--profile"), - FormatProvider("Microsoft-Windows-DotNETRuntime", "00000014C14FCCBD", "Informational", 4, "--profile")) + FormatProvider("Microsoft-Windows-DotNETRuntime", "000000100003801D", "Informational", 4, "--profile"), + FormatProvider("Microsoft-DotNETCore-SampleProfiler", "0000F00000000000", "Informational", 4, "--profile")) }; yield return new object[] { - new CollectArgs(profile: "cpu-sampling", providers: "Foo:0x1:4"), + new CollectArgs(profile: new[] { "dotnet-common", "dotnet-sampled-thread-time" }, providers: new[] { "Foo:0x1:4" }), ExpectProviders( FormatProvider("Foo", "0000000000000001", "Informational", 4, "--providers"), - FormatProvider("Microsoft-DotNETCore-SampleProfiler", "0000F00000000000", "Informational", 4, "--profile"), - FormatProvider("Microsoft-Windows-DotNETRuntime", "00000014C14FCCBD", "Informational", 4, "--profile")) + FormatProvider("Microsoft-Windows-DotNETRuntime", "000000100003801D", "Informational", 4, "--profile"), + FormatProvider("Microsoft-DotNETCore-SampleProfiler", "0000F00000000000", "Informational", 4, "--profile")) }; yield return new object[] { - new CollectArgs(profile: "cpu-sampling", clrevents: "gc"), + new CollectArgs(profile: new[] { "dotnet-common", "dotnet-sampled-thread-time" }, clrevents: "gc"), ExpectProvidersWithMessages( new[] { - "The argument --clrevents gc will be ignored because the CLR provider was configured via either --profile or --providers command." + "Warning: The CLR provider was already specified through --providers or --profile. Ignoring --clrevents." }, - FormatProvider("Microsoft-DotNETCore-SampleProfiler", "0000F00000000000", "Informational", 4, "--profile"), - FormatProvider("Microsoft-Windows-DotNETRuntime", "00000014C14FCCBD", "Informational", 4, "--profile")) + FormatProvider("Microsoft-Windows-DotNETRuntime", "000000100003801D", "Informational", 4, "--profile"), + FormatProvider("Microsoft-DotNETCore-SampleProfiler", "0000F00000000000", "Informational", 4, "--profile")) }; yield return new object[] { - new CollectArgs(profile: "cpu-sampling", providers: "Microsoft-Windows-DotNETRuntime:0x1:4"), + new CollectArgs(profile: new[] { "dotnet-common", "dotnet-sampled-thread-time" }, providers: new[] { "Microsoft-Windows-DotNETRuntime:0x1:4" }), ExpectProviders( FormatProvider("Microsoft-Windows-DotNETRuntime", "0000000000000001", "Informational", 4, "--providers"), FormatProvider("Microsoft-DotNETCore-SampleProfiler", "0000F00000000000", "Informational", 4, "--profile")) @@ -190,11 +196,11 @@ public static IEnumerable BasicCases() yield return new object[] { - new CollectArgs(providers: "Microsoft-Windows-DotNETRuntime:0x1:4", clrevents: "gc"), + new CollectArgs(providers: new[] { "Microsoft-Windows-DotNETRuntime:0x1:4" }, clrevents: "gc"), ExpectProvidersWithMessages( new[] { - "The argument --clrevents gc will be ignored because the CLR provider was configured via either --profile or --providers command." + "Warning: The CLR provider was already specified through --providers or --profile. Ignoring --clrevents." }, FormatProvider("Microsoft-Windows-DotNETRuntime", "0000000000000001", "Informational", 4, "--providers")) }; @@ -214,6 +220,39 @@ public static IEnumerable BasicCases() }; } + public static IEnumerable InvalidProviders() + { + yield return new object[] + { + new CollectArgs(profile: new[] { "cpu-sampling" }), + new [] { FormatException("The specified profile 'cpu-sampling' does not apply to `dotnet-trace collect`.", "System.ArgumentException") } + }; + + yield return new object[] + { + new CollectArgs(profile: new[] { "unknown" }), + new [] { FormatException("Invalid profile name: unknown", "System.ArgumentException") } + }; + + yield return new object[] + { + new CollectArgs(providers: new[] { "Foo:::Bar=0", "Foo:::Bar=1" }), + new [] { FormatException($"Provider \"Foo\" is declared multiple times with filter arguments.", "System.ArgumentException") } + }; + + yield return new object[] + { + new CollectArgs(clrevents: "unknown"), + new [] { FormatException("unknown is not a valid CLR event keyword", "System.ArgumentException") } + }; + + yield return new object[] + { + new CollectArgs(clrevents: "gc", clreventlevel: "unknown"), + new [] { FormatException("Unknown EventLevel: unknown", "System.ArgumentException") } + }; + } + private static string outputFile = $"Output File : {Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar}trace.nettrace"; private const string ProviderHeader = "Provider Name Keywords Level Enabled By"; private static readonly string[] CommonTail = [ @@ -221,7 +260,6 @@ public static IEnumerable BasicCases() outputFile, "", "", - "", "Trace completed." ]; @@ -238,5 +276,7 @@ private static string FormatProvider(string name, string keywordsHex, string lev string.Format("{0, -8}", $"{levelName}({levelValue})"); return string.Format("{0, -80}", display) + enabledBy; } + + private static string FormatException(string message, string exceptionType) => $"[ERROR] {exceptionType}: {message}"; } } diff --git a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs new file mode 100644 index 0000000000..41dfee7159 --- /dev/null +++ b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs @@ -0,0 +1,284 @@ +// 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.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; +using Microsoft.Diagnostics.Tests.Common; +using Microsoft.Diagnostics.Tools.Trace; +using Microsoft.Internal.Common.Utils; +using Xunit; + +namespace Microsoft.Diagnostics.Tools.Trace +{ + public class CollectLinuxCommandFunctionalTests + { + private static CollectLinuxCommandHandler.CollectLinuxArgs TestArgs( + CancellationToken ct = default, + string[] providers = null, + string clrEventLevel = "", + string clrEvents = "", + string[] perfEvents = null, + string[] profile = null, + FileInfo output = null, + TimeSpan duration = default) + { + return new CollectLinuxCommandHandler.CollectLinuxArgs(ct, + providers ?? Array.Empty(), + clrEventLevel, + clrEvents, + perfEvents ?? Array.Empty(), + profile ?? Array.Empty(), + output ?? new FileInfo("trace.nettrace"), + duration); + } + + [Theory] + [MemberData(nameof(BasicCases))] + public void CollectLinuxCommandProviderConfigurationConsolidation(object testArgs, string[] expectedLines) + { + MockConsole console = new(200, 30); + int exitCode = Run(testArgs, console); + if (OperatingSystem.IsLinux()) + { + Assert.Equal((int)ReturnCode.Ok, exitCode); + console.AssertSanitizedLinesEqual(CollectLinuxSanitizer, expectedLines: expectedLines); + } + else + { + Assert.Equal((int)ReturnCode.PlatformNotSupportedError, exitCode); + console.AssertSanitizedLinesEqual(null, expectedLines: new string[] { + "The collect-linux command is only supported on Linux.", + }); + } + } + + [Theory] + [MemberData(nameof(InvalidProviders))] + public void CollectLinuxCommandProviderConfigurationConsolidation_Throws(object testArgs, string[] expectedException) + { + MockConsole console = new(200, 30); + int exitCode = Run(testArgs, console); + if (OperatingSystem.IsLinux()) + { + Assert.Equal((int)ReturnCode.TracingError, exitCode); + console.AssertSanitizedLinesEqual(null, true, expectedLines: expectedException); + } + else + { + Assert.Equal((int)ReturnCode.PlatformNotSupportedError, exitCode); + console.AssertSanitizedLinesEqual(null, expectedLines: new string[] { + "The collect-linux command is only supported on Linux.", + }); + } + } + + private static int Run(object args, MockConsole console) + { + var handler = new CollectLinuxCommandHandler(console); + handler.RecordTraceInvoker = (cmd, len, cb) => { + cb(3, IntPtr.Zero, UIntPtr.Zero); + return 0; + }; + return handler.CollectLinux((CollectLinuxCommandHandler.CollectLinuxArgs)args); + } + + private static string[] CollectLinuxSanitizer(string[] lines) + { + List result = new(); + foreach (string line in lines) + { + if (line.Contains("Recording trace.", StringComparison.OrdinalIgnoreCase)) + { + result.Add("[dd:hh:mm:ss]\tRecording trace."); + } + else + { + result.Add(line); + } + } + return result.ToArray(); + } + + public static IEnumerable BasicCases() + { + yield return new object[] { + TestArgs(), + ExpectProvidersAndLinuxWithMessages( + new[]{"No providers, profiles, ClrEvents, or PerfEvents were specified, defaulting to trace profiles 'dotnet-common' + 'cpu-sampling'."}, + new[]{FormatProvider("Microsoft-Windows-DotNETRuntime","000000100003801D","Informational",4,"--profile")}, + new[]{LinuxProfile("cpu-sampling")}) + }; + + yield return new object[] { + TestArgs(providers: new[]{"Foo:0x1:4"}), + ExpectProvidersAndLinux( + new[]{FormatProvider("Foo","0000000000000001","Informational",4,"--providers")}, + Array.Empty()) + }; + + yield return new object[] { + TestArgs(providers: new[]{"Foo:0x1:4","Bar:0x2:4"}), + ExpectProvidersAndLinux( + new[]{ + FormatProvider("Foo","0000000000000001","Informational",4,"--providers"), + FormatProvider("Bar","0000000000000002","Informational",4,"--providers") + }, + Array.Empty()) + }; + + yield return new object[] { + TestArgs(profile: new[]{"cpu-sampling"}), + ExpectProvidersAndLinuxWithMessages( + new[]{"No .NET providers were configured."}, + Array.Empty(), + new[]{LinuxProfile("cpu-sampling")}) + }; + + yield return new object[] { + TestArgs(providers: new[]{"Foo:0x1:4"}, profile: new[]{"cpu-sampling"}), + ExpectProvidersAndLinux( + new[]{FormatProvider("Foo","0000000000000001","Informational",4,"--providers")}, + new[]{LinuxProfile("cpu-sampling")}) + }; + + yield return new object[] { + TestArgs(clrEvents: "gc", profile: new[]{"cpu-sampling"}), + ExpectProvidersAndLinux( + new[]{FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--clrevents")}, + new[]{LinuxProfile("cpu-sampling")}) + }; + + yield return new object[] { + TestArgs(providers: new[]{"Microsoft-Windows-DotNETRuntime:0x1:4"}, profile: new[]{"cpu-sampling"}), + ExpectProvidersAndLinux( + new[]{FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--providers")}, + new[]{LinuxProfile("cpu-sampling")}) + }; + + yield return new object[] { + TestArgs(providers: new[]{"Microsoft-Windows-DotNETRuntime:0x1:4"}, clrEvents: "gc"), + ExpectProvidersAndLinuxWithMessages( + new[]{"Warning: The CLR provider was already specified through --providers or --profile. Ignoring --clrevents."}, + new[]{FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--providers")}, + Array.Empty()) + }; + + yield return new object[] { + TestArgs(clrEvents: "gc+jit"), + ExpectProvidersAndLinux( + new[]{FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000011","Informational",4,"--clrevents")}, + Array.Empty()) + }; + + yield return new object[] { + TestArgs(clrEvents: "gc+jit", clrEventLevel: "5"), + ExpectProvidersAndLinux( + new[]{FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000011","Verbose",5,"--clrevents")}, + Array.Empty()) + }; + + yield return new object[] { + TestArgs(perfEvents: new[]{"sched:sched_switch"}), + ExpectProvidersAndLinuxWithMessages( + new[]{"No .NET providers were configured."}, + Array.Empty(), + new[]{LinuxPerfEvent("sched:sched_switch")}) + }; + } + + public static IEnumerable InvalidProviders() + { + yield return new object[] + { + TestArgs(profile: new[] { "dotnet-sampled-thread-time" }), + new [] { FormatException("The specified profile 'dotnet-sampled-thread-time' does not apply to `dotnet-trace collect-linux`.", "System.ArgumentException") } + }; + + yield return new object[] + { + TestArgs(profile: new[] { "unknown" }), + new [] { FormatException("Invalid profile name: unknown", "System.ArgumentException") } + }; + + yield return new object[] + { + TestArgs(providers: new[] { "Foo:::Bar=0", "Foo:::Bar=1" }), + new [] { FormatException($"Provider \"Foo\" is declared multiple times with filter arguments.", "System.ArgumentException") } + }; + + yield return new object[] + { + TestArgs(clrEvents: "unknown"), + new [] { FormatException("unknown is not a valid CLR event keyword", "System.ArgumentException") } + }; + + yield return new object[] + { + TestArgs(clrEvents: "gc", clrEventLevel: "unknown"), + new [] { FormatException("Unknown EventLevel: unknown", "System.ArgumentException") } + }; + } + + private const string ProviderHeader = "Provider Name Keywords Level Enabled By"; + private static string LinuxHeader => $"{"Linux Perf Events",-80}Enabled By"; + private static string LinuxProfile(string name) => $"{name,-80}--profile"; + private static string LinuxPerfEvent(string spec) => $"{spec,-80}--perf-events"; + private static string FormatProvider(string name, string keywordsHex, string levelName, int levelValue, string enabledBy) + { + string display = string.Format("{0, -40}", name) + + string.Format("0x{0, -18}", keywordsHex) + + string.Format("{0, -8}", $"{levelName}({levelValue})"); + return string.Format("{0, -80}", display) + enabledBy; + } + private static string FormatException(string message, string exceptionType) => $"[ERROR] {exceptionType}: {message}"; + private static string DefaultOutputFile => $"Output File : {Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar}trace.nettrace"; + private static readonly string[] CommonTail = [ + DefaultOutputFile, + "", + "[dd:hh:mm:ss]\tRecording trace.", + "Press or to exit...", + ]; + + private static string[] ExpectProvidersAndLinux(string[] dotnetProviders, string[] linuxPerfEvents) + => ExpectProvidersAndLinuxWithMessages(Array.Empty(), dotnetProviders, linuxPerfEvents); + + private static string[] ExpectProvidersAndLinuxWithMessages(string[] messages, string[] dotnetProviders, string[] linuxPerfEvents) + { + List result = new(); + + if (messages.Length > 0) + { + result.AddRange(messages); + } + result.Add(""); + + if (dotnetProviders.Length > 0) + { + result.Add(ProviderHeader); + result.AddRange(dotnetProviders); + result.Add(""); + } + + if (linuxPerfEvents.Length > 0) + { + result.Add(LinuxHeader); + result.AddRange(linuxPerfEvents); + } + else + { + result.Add("No Linux Perf Events enabled."); + } + result.Add(""); + + result.AddRange(CommonTail); + + return result.ToArray(); + } + } +} 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)); - } - } -}