From 536cd503a4f233758eba24b5a5d24953031f9a69 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Tue, 16 Sep 2025 22:58:21 +0000 Subject: [PATCH 01/25] [DotnetTrace] Update CLREventKeywords --- src/Tools/dotnet-trace/Extensions.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Tools/dotnet-trace/Extensions.cs b/src/Tools/dotnet-trace/Extensions.cs index b9bd829c70..c741591d2a 100644 --- a/src/Tools/dotnet-trace/Extensions.cs +++ b/src/Tools/dotnet-trace/Extensions.cs @@ -23,6 +23,7 @@ internal static class Extensions { "gc", 0x1 }, { "gchandle", 0x2 }, { "fusion", 0x4 }, + { "assemblyloader", 0x4 }, { "loader", 0x8 }, { "jit", 0x10 }, { "ngen", 0x20 }, @@ -42,6 +43,7 @@ internal static class Extensions { "gcsampledobjectallocationhigh", 0x200000 }, { "gcheapsurvivalandmovement", 0x400000 }, { "gcheapcollect", 0x800000 }, + { "managedheadcollect", 0x800000 }, { "gcheapandtypenames", 0x1000000 }, { "gcsampledobjectallocationlow", 0x2000000 }, { "perftrack", 0x20000000 }, @@ -55,7 +57,10 @@ internal static class Extensions { "compilationdiagnostic", 0x2000000000 }, { "methoddiagnostic", 0x4000000000 }, { "typediagnostic", 0x8000000000 }, + { "jitinstrumentationdata", 0x10000000000 }, + { "profiler", 0x20000000000 }, { "waithandle", 0x40000000000 }, + { "allocationsampling", 0x80000000000 }, }; public static List ToProviders(string providersRawInput) From 49a875bc5a6195d6133a79fe39121d3653aecb8a Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Wed, 17 Sep 2025 01:36:54 +0000 Subject: [PATCH 02/25] [DotnetTrace] Move MergeProfileAndProviders to Extensions Extensions houses provider composition helpers, instead of literal extension methods for strings. Centralize all provider parsing + composition logic and keep Profile as a data container. --- .../CommandLine/Commands/CollectCommand.cs | 2 +- src/Tools/dotnet-trace/Extensions.cs | 31 +++++++++++++++++++ src/Tools/dotnet-trace/Profile.cs | 31 ------------------- .../dotnet-trace/ProfileProviderMerging.cs | 2 +- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index 054a6e83a7..e36da3d6dc 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -138,7 +138,7 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur rundownKeyword = selectedProfile.RundownKeyword; retryStrategy = selectedProfile.RetryStrategy; - Profile.MergeProfileAndProviders(selectedProfile, providerCollection, enabledBy); + Extensions.MergeProfileAndProviders(selectedProfile, providerCollection, enabledBy); } if (rundown.HasValue) diff --git a/src/Tools/dotnet-trace/Extensions.cs b/src/Tools/dotnet-trace/Extensions.cs index c741591d2a..9964c33447 100644 --- a/src/Tools/dotnet-trace/Extensions.cs +++ b/src/Tools/dotnet-trace/Extensions.cs @@ -63,6 +63,37 @@ internal static class Extensions { "allocationsampling", 0x80000000000 }, }; + 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 static List ToProviders(string providersRawInput) { if (providersRawInput == null) diff --git a/src/Tools/dotnet-trace/Profile.cs b/src/Tools/dotnet-trace/Profile.cs index 2114ff0540..b5b1afbef6 100644 --- a/src/Tools/dotnet-trace/Profile.cs +++ b/src/Tools/dotnet-trace/Profile.cs @@ -25,36 +25,5 @@ public Profile(string name, IEnumerable providers, string des public long RundownKeyword { get; set; } = EventPipeSession.DefaultRundownKeyword; 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); - } } } diff --git a/src/tests/dotnet-trace/ProfileProviderMerging.cs b/src/tests/dotnet-trace/ProfileProviderMerging.cs index 606fa24e50..de35b7e0b2 100644 --- a/src/tests/dotnet-trace/ProfileProviderMerging.cs +++ b/src/tests/dotnet-trace/ProfileProviderMerging.cs @@ -31,7 +31,7 @@ public void DuplicateProvider_CorrectlyOverrides(string profileName, string prov .FirstOrDefault(p => p.Name.Equals(profileName, StringComparison.OrdinalIgnoreCase)); Assert.NotNull(selectedProfile); - Profile.MergeProfileAndProviders(selectedProfile, parsedProviders, enabledBy); + Extensions.MergeProfileAndProviders(selectedProfile, parsedProviders, enabledBy); EventPipeProvider enabledProvider = parsedProviders.SingleOrDefault(p => p.Name == "Microsoft-Windows-DotNETRuntime"); From b4787fc1644b57ee58b12eebba67a29e2315509b Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Wed, 17 Sep 2025 02:16:15 +0000 Subject: [PATCH 03/25] [DotnetTrace] Rename Extensions to ProviderUtils --- .../CommandLine/Commands/CollectCommand.cs | 10 ++-- .../{Extensions.cs => ProviderUtils.cs} | 2 +- src/tests/dotnet-trace/CLRProviderParsing.cs | 10 ++-- .../dotnet-trace/ProfileProviderMerging.cs | 4 +- src/tests/dotnet-trace/ProviderParsing.cs | 52 +++++++++---------- 5 files changed, 39 insertions(+), 39 deletions(-) rename src/Tools/dotnet-trace/{Extensions.cs => ProviderUtils.cs} (99%) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index e36da3d6dc..ee6ad4f3b3 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -116,7 +116,7 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur Dictionary enabledBy = new(); - List providerCollection = Extensions.ToProviders(providers); + List providerCollection = ProviderUtils.ToProviders(providers); foreach (EventPipeProvider providerCollectionProvider in providerCollection) { enabledBy[providerCollectionProvider.Name] = "--providers "; @@ -138,7 +138,7 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur rundownKeyword = selectedProfile.RundownKeyword; retryStrategy = selectedProfile.RetryStrategy; - Extensions.MergeProfileAndProviders(selectedProfile, providerCollection, enabledBy); + ProviderUtils.MergeProfileAndProviders(selectedProfile, providerCollection, enabledBy); } if (rundown.HasValue) @@ -159,15 +159,15 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur if (clrevents.Length != 0) { // Ignore --clrevents if CLR event provider was already specified via --profile or --providers command. - if (enabledBy.ContainsKey(Extensions.CLREventProviderName)) + if (enabledBy.ContainsKey(ProviderUtils.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); + EventPipeProvider clrProvider = ProviderUtils.ToCLREventPipeProvider(clrevents, clreventlevel); providerCollection.Add(clrProvider); - enabledBy[Extensions.CLREventProviderName] = "--clrevents"; + enabledBy[ProviderUtils.CLREventProviderName] = "--clrevents"; } } diff --git a/src/Tools/dotnet-trace/Extensions.cs b/src/Tools/dotnet-trace/ProviderUtils.cs similarity index 99% rename from src/Tools/dotnet-trace/Extensions.cs rename to src/Tools/dotnet-trace/ProviderUtils.cs index 9964c33447..0b503f9d40 100644 --- a/src/Tools/dotnet-trace/Extensions.cs +++ b/src/Tools/dotnet-trace/ProviderUtils.cs @@ -10,7 +10,7 @@ namespace Microsoft.Diagnostics.Tools.Trace { - internal static class Extensions + internal static class ProviderUtils { public static string CLREventProviderName = "Microsoft-Windows-DotNETRuntime"; diff --git a/src/tests/dotnet-trace/CLRProviderParsing.cs b/src/tests/dotnet-trace/CLRProviderParsing.cs index 01d7b4607c..36d0dc9335 100644 --- a/src/tests/dotnet-trace/CLRProviderParsing.cs +++ b/src/tests/dotnet-trace/CLRProviderParsing.cs @@ -16,7 +16,7 @@ public class CLRProviderParsingTests [InlineData("GC")] public void ValidSingleCLREvent(string providerToParse) { - NETCore.Client.EventPipeProvider provider = Extensions.ToCLREventPipeProvider(providerToParse, "4"); + NETCore.Client.EventPipeProvider provider = ProviderUtils.ToCLREventPipeProvider(providerToParse, "4"); Assert.True(provider.Name == CLRProviderName); Assert.True(provider.Keywords == 1); Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Informational); @@ -29,7 +29,7 @@ public void ValidSingleCLREvent(string providerToParse) [InlineData("haha")] public void InValidSingleCLREvent(string providerToParse) { - Assert.Throws(() => Extensions.ToCLREventPipeProvider(providerToParse, "4")); + Assert.Throws(() => ProviderUtils.ToCLREventPipeProvider(providerToParse, "4")); } [Theory] @@ -38,7 +38,7 @@ public void InValidSingleCLREvent(string providerToParse) [InlineData("GC+GCHandle")] public void ValidManyCLREvents(string providerToParse) { - NETCore.Client.EventPipeProvider provider = Extensions.ToCLREventPipeProvider(providerToParse, "5"); + NETCore.Client.EventPipeProvider provider = ProviderUtils.ToCLREventPipeProvider(providerToParse, "5"); Assert.True(provider.Name == CLRProviderName); Assert.True(provider.Keywords == 3); Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Verbose); @@ -52,7 +52,7 @@ public void ValidManyCLREvents(string providerToParse) [InlineData("InFORMationAL")] public void ValidCLREventLevel(string clreventlevel) { - NETCore.Client.EventPipeProvider provider = Extensions.ToCLREventPipeProvider("gc", clreventlevel); + NETCore.Client.EventPipeProvider provider = ProviderUtils.ToCLREventPipeProvider("gc", clreventlevel); Assert.True(provider.Name == CLRProviderName); Assert.True(provider.Keywords == 1); Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Informational); @@ -64,7 +64,7 @@ public void ValidCLREventLevel(string clreventlevel) [InlineData("hello")] public void InvalidCLREventLevel(string clreventlevel) { - Assert.Throws(() => Extensions.ToCLREventPipeProvider("gc", clreventlevel)); + Assert.Throws(() => ProviderUtils.ToCLREventPipeProvider("gc", clreventlevel)); } } } diff --git a/src/tests/dotnet-trace/ProfileProviderMerging.cs b/src/tests/dotnet-trace/ProfileProviderMerging.cs index de35b7e0b2..742cf13dac 100644 --- a/src/tests/dotnet-trace/ProfileProviderMerging.cs +++ b/src/tests/dotnet-trace/ProfileProviderMerging.cs @@ -20,7 +20,7 @@ public void DuplicateProvider_CorrectlyOverrides(string profileName, string prov { Dictionary enabledBy = new(); - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = ProviderUtils.ToProviders(providerToParse); foreach (EventPipeProvider provider in parsedProviders) { @@ -31,7 +31,7 @@ public void DuplicateProvider_CorrectlyOverrides(string profileName, string prov .FirstOrDefault(p => p.Name.Equals(profileName, StringComparison.OrdinalIgnoreCase)); Assert.NotNull(selectedProfile); - Extensions.MergeProfileAndProviders(selectedProfile, parsedProviders, enabledBy); + ProviderUtils.MergeProfileAndProviders(selectedProfile, parsedProviders, enabledBy); EventPipeProvider enabledProvider = parsedProviders.SingleOrDefault(p => p.Name == "Microsoft-Windows-DotNETRuntime"); diff --git a/src/tests/dotnet-trace/ProviderParsing.cs b/src/tests/dotnet-trace/ProviderParsing.cs index bab3b6fb0d..ddb5d0a7b4 100644 --- a/src/tests/dotnet-trace/ProviderParsing.cs +++ b/src/tests/dotnet-trace/ProviderParsing.cs @@ -16,7 +16,7 @@ public class ProviderParsingTests [InlineData("VeryCoolProvider:1:5:FilterAndPayloadSpecs=\"QuotedValue\"")] public void ValidProvider_CorrectlyParses(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = ProviderUtils.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); EventPipeProvider provider = parsedProviders.First(); Assert.True(provider.Name == "VeryCoolProvider"); @@ -30,7 +30,7 @@ public void ValidProvider_CorrectlyParses(string providerToParse) [InlineData("VeryCoolProvider:0x1:5:FilterAndPayloadSpecs=\"QuotedValue:-\r\nQuoted/Value\"")] public void ValidProviderFilter_CorrectlyParses(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = ProviderUtils.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); EventPipeProvider provider = parsedProviders.First(); Assert.True(provider.Name == "VeryCoolProvider"); @@ -45,7 +45,7 @@ public void ValidProviderFilter_CorrectlyParses(string providerToParse) [InlineData(",")] public void EmptyProvider_CorrectlyThrows(string providerToParse) { - Assert.Throws(() => Extensions.ToProviders(providerToParse)); + Assert.Throws(() => ProviderUtils.ToProviders(providerToParse)); } [Theory] @@ -53,14 +53,14 @@ public void EmptyProvider_CorrectlyThrows(string providerToParse) [InlineData(":1:1")] public void InvalidProvider_CorrectlyThrows(string providerToParse) { - Assert.Throws(() => Extensions.ToProviders(providerToParse)); + Assert.Throws(() => ProviderUtils.ToProviders(providerToParse)); } [Theory] [InlineData("VeryCoolProvider:0xFFFFFFFFFFFFFFFF:5:FilterAndPayloadSpecs=\"QuotedValue\"")] public void ValidProviderKeyword_CorrectlyParses(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = ProviderUtils.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); EventPipeProvider provider = parsedProviders.First(); Assert.True(provider.Name == "VeryCoolProvider"); @@ -75,7 +75,7 @@ public void ValidProviderKeyword_CorrectlyParses(string providerToParse) [InlineData("VeryCoolProvider:::FilterAndPayloadSpecs=\"QuotedValue\"")] public void ValidProviderEventLevel_CorrectlyParses(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = ProviderUtils.ToProviders(providerToParse); Assert.Equal(1, parsedProviders.Count); EventPipeProvider provider = parsedProviders.First(); Assert.Equal("VeryCoolProvider", provider.Name); @@ -90,7 +90,7 @@ public void ValidProviderEventLevel_CorrectlyParses(string providerToParse) [InlineData("VeryCoolProvider:0x10000000000000000::FilterAndPayloadSpecs=\"QuotedValue\"")] public void OutOfRangekeyword_CorrectlyThrows(string providerToParse) { - Assert.Throws(() => Extensions.ToProviders(providerToParse)); + Assert.Throws(() => ProviderUtils.ToProviders(providerToParse)); } [Theory] @@ -98,7 +98,7 @@ public void OutOfRangekeyword_CorrectlyThrows(string providerToParse) [InlineData("VeryCoolProvider:gh::FilterAndPayloadSpecs=\"QuotedValue\"")] public void Invalidkeyword_CorrectlyThrows(string providerToParse) { - Assert.Throws(() => Extensions.ToProviders(providerToParse)); + Assert.Throws(() => ProviderUtils.ToProviders(providerToParse)); } [Theory] @@ -106,7 +106,7 @@ public void Invalidkeyword_CorrectlyThrows(string providerToParse) [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); + List parsedProviders = ProviderUtils.ToProviders(providersToParse); Assert.True(parsedProviders.Count == 3); EventPipeProvider providerOne = parsedProviders[0]; EventPipeProvider providerTwo = parsedProviders[1]; @@ -137,7 +137,7 @@ public void MultipleValidProviders_CorrectlyParses(string providersToParse) [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)); + Assert.Throws(() => ProviderUtils.ToProviders(providersToParse)); } [Theory] @@ -146,7 +146,7 @@ public void MultipleValidProvidersWithOneInvalidProvider_CorrectlyThrows(string [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)); + Assert.Throws(() => ProviderUtils.ToProviders(providersToParse)); } [Theory] @@ -155,14 +155,14 @@ public void MultipleValidProvidersWithOneOutOfRangeKeyword_CorrectlyThrows(strin [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)); + Assert.Throws(() => ProviderUtils.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); + List parsedProviders = ProviderUtils.ToProviders(providersToParse); Assert.True(parsedProviders.Count == 3); EventPipeProvider providerOne = parsedProviders[0]; EventPipeProvider providerTwo = parsedProviders[1]; @@ -191,7 +191,7 @@ public void MultipleProvidersWithComplexFilters_CorrectlyParse(string providersT public void ProvidersWithComplexFilters_CorrectlyParse() { string providersToParse = @"MyProvider:::A=B;C=D"; - List parsedProviders = Extensions.ToProviders(providersToParse); + List parsedProviders = ProviderUtils.ToProviders(providersToParse); Assert.Single(parsedProviders); EventPipeProvider providerOne = parsedProviders[0]; Assert.Equal("MyProvider", providerOne.Name); @@ -200,7 +200,7 @@ public void ProvidersWithComplexFilters_CorrectlyParse() Assert.Equal("D", providerOne.Arguments["C"]); providersToParse = @"MyProvider:::A=B;C=""D"",MyProvider2:::A=1;B=2;"; - parsedProviders = Extensions.ToProviders(providersToParse); + parsedProviders = ProviderUtils.ToProviders(providersToParse); Assert.Equal(2, parsedProviders.Count); providerOne = parsedProviders[0]; EventPipeProvider providerTwo = parsedProviders[1]; @@ -214,7 +214,7 @@ public void ProvidersWithComplexFilters_CorrectlyParse() 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); + parsedProviders = ProviderUtils.ToProviders(providersToParse); Assert.Equal(2, parsedProviders.Count); providerOne = parsedProviders[0]; providerTwo = parsedProviders[1]; @@ -233,7 +233,7 @@ public void ProvidersWithComplexFilters_CorrectlyParse() [InlineData("ProviderOne:0x1:verbose")] public void TextLevelProviderSpecVerbose_CorrectlyParse(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = ProviderUtils.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); Assert.True(parsedProviders[0].Name == "ProviderOne"); Assert.True(parsedProviders[0].Keywords == 1); @@ -245,7 +245,7 @@ public void TextLevelProviderSpecVerbose_CorrectlyParse(string providerToParse) [InlineData("ProviderOne:0x1:INFORMATIONAL")] public void TextLevelProviderSpecInformational_CorrectlyParse(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = ProviderUtils.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); Assert.True(parsedProviders[0].Name == "ProviderOne"); Assert.True(parsedProviders[0].Keywords == 1); @@ -257,7 +257,7 @@ public void TextLevelProviderSpecInformational_CorrectlyParse(string providerToP [InlineData("ProviderOne:0x1:LogAlwayS")] public void TextLevelProviderSpecLogAlways_CorrectlyParse(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = ProviderUtils.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); Assert.True(parsedProviders[0].Name == "ProviderOne"); Assert.True(parsedProviders[0].Keywords == 1); @@ -269,7 +269,7 @@ public void TextLevelProviderSpecLogAlways_CorrectlyParse(string providerToParse [InlineData("ProviderOne:0x1:ERRor")] public void TextLevelProviderSpecError_CorrectlyParse(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = ProviderUtils.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); Assert.True(parsedProviders[0].Name == "ProviderOne"); Assert.True(parsedProviders[0].Keywords == 1); @@ -281,7 +281,7 @@ public void TextLevelProviderSpecError_CorrectlyParse(string providerToParse) [InlineData("ProviderOne:0x1:CRITICAL")] public void TextLevelProviderSpecCritical_CorrectlyParse(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = ProviderUtils.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); Assert.True(parsedProviders[0].Name == "ProviderOne"); Assert.True(parsedProviders[0].Keywords == 1); @@ -293,7 +293,7 @@ public void TextLevelProviderSpecCritical_CorrectlyParse(string providerToParse) [InlineData("ProviderOne:0x1:warning")] public void TextLevelProviderSpecWarning_CorrectlyParse(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = ProviderUtils.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); Assert.True(parsedProviders[0].Name == "ProviderOne"); Assert.True(parsedProviders[0].Keywords == 1); @@ -304,14 +304,14 @@ public void TextLevelProviderSpecWarning_CorrectlyParse(string providerToParse) [InlineData("ProviderOne:0x1:UnknownLevel")] public void TextLevelProviderSpec_CorrectlyThrows(string providerToParse) { - Assert.Throws(() => Extensions.ToProviders(providerToParse)); + Assert.Throws(() => ProviderUtils.ToProviders(providerToParse)); } [Theory] [InlineData("DupeProvider,DupeProvider:0xF:LogAlways")] public void DeDupeProviders_DefaultAndSpecified(string providersToParse) { - List parsedProviders = Extensions.ToProviders(providersToParse); + List parsedProviders = ProviderUtils.ToProviders(providersToParse); Assert.Equal("DupeProvider", parsedProviders.First().Name); Assert.Equal(1, parsedProviders.Count); Assert.Equal(0xF, parsedProviders.First().Keywords); @@ -323,7 +323,7 @@ public void DeDupeProviders_DefaultAndSpecified(string providersToParse) [InlineData("DupeProvider:0xF0:Informational,DupeProvider:0xF:Verbose")] public void DeDupeProviders_BothSpecified(string providersToParse) { - List parsedProviders = Extensions.ToProviders(providersToParse); + List parsedProviders = ProviderUtils.ToProviders(providersToParse); Assert.Equal("DupeProvider", parsedProviders.First().Name); Assert.Equal(1, parsedProviders.Count); Assert.Equal(0xFF, parsedProviders.First().Keywords); @@ -335,7 +335,7 @@ public void DeDupeProviders_BothSpecified(string providersToParse) [InlineData("DupeProvider:::key=value,DupeProvider:::key=value")] public void DeDupeProviders_FilterDataThrows(string providersToParse) { - Assert.Throws(() => Extensions.ToProviders(providersToParse)); + Assert.Throws(() => ProviderUtils.ToProviders(providersToParse)); } } } From c921869f178dd59d09a548deada53510f52b4ccc Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Wed, 17 Sep 2025 16:04:16 +0000 Subject: [PATCH 04/25] [DotnetTrace] Add provider unifying helper --- src/Tools/dotnet-trace/ProviderUtils.cs | 71 +++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/Tools/dotnet-trace/ProviderUtils.cs b/src/Tools/dotnet-trace/ProviderUtils.cs index 0b503f9d40..9f01837b31 100644 --- a/src/Tools/dotnet-trace/ProviderUtils.cs +++ b/src/Tools/dotnet-trace/ProviderUtils.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.Tracing; using System.Linq; using System.Text.RegularExpressions; @@ -143,6 +144,76 @@ public static List ToProviders(string providersRawInput) return providers.ToList(); } + public static List ToProviders(string[] providersArg, string clreventsArg, string clreventlevel, string[] profiles) + { + Dictionary merged = new(StringComparer.OrdinalIgnoreCase); + + foreach (string providerArg in providersArg) + { + EventPipeProvider provider = ToProvider(providerArg); + if (!merged.TryGetValue(provider.Name, out EventPipeProvider existing)) + { + merged[provider.Name] = provider; + } + else + { + merged[provider.Name] = MergeProviderConfigs(existing, provider); + } + } + + 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; + } + else + { + merged[provider.Name] = MergeProviderConfigs(existing, provider); + } + } + } + + foreach (string profile in profiles) + { + Profile dotnetProfile = ListProfilesCommandHandler.DotNETRuntimeProfiles + .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); + if (dotnetProfile == null) + { + // for collect-linux, could be linux perf event profile + continue; + } + + IEnumerable profileProviders = dotnetProfile.Providers; + foreach (EventPipeProvider provider in profileProviders) + { + merged.TryAdd(provider.Name, provider); + // Prefer providers set through --providers and --clrevents over implicit profile configuration + } + } + + return merged.Values.ToList(); + } + + private static EventPipeProvider MergeProviderConfigs(EventPipeProvider providerConfigA, EventPipeProvider providerConfigB) + { + Debug.Assert(string.Equals(providerConfigA.Name, providerConfigB.Name, StringComparison.OrdinalIgnoreCase)); + + EventLevel level = (providerConfigA.EventLevel == EventLevel.LogAlways || providerConfigB.EventLevel == EventLevel.LogAlways) ? + EventLevel.LogAlways : + (providerConfigA.EventLevel > providerConfigB.EventLevel ? providerConfigA.EventLevel : providerConfigB.EventLevel); + + if (providerConfigA.Arguments != null && providerConfigB.Arguments != null) + { + throw new ArgumentException($"Provider \"{providerConfigA.Name}\" is declared multiple times with filter arguments."); + } + + return new EventPipeProvider(providerConfigA.Name, level, providerConfigA.Keywords | providerConfigB.Keywords, providerConfigA.Arguments ?? providerConfigB.Arguments); + } + public static EventPipeProvider ToCLREventPipeProvider(string clreventslist, string clreventlevel) { if (clreventslist == null || clreventslist.Length == 0) From e1c15c716a6b3226e397f1dd5bf355f96c29deb0 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Wed, 17 Sep 2025 18:47:12 +0000 Subject: [PATCH 05/25] [DotnetTrace] Move shared options to CommonOptions --- .../CommandLine/Commands/CollectCommand.cs | 73 ++++--------------- .../CommandLine/Commands/ConvertCommand.cs | 4 +- .../CommandLine/Options/CommonOptions.cs | 61 ++++++++++++++-- 3 files changed, 70 insertions(+), 68 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index ee6ad4f3b3..0623d69683 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -263,7 +263,7 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur } - if (string.Equals(output.Name, DefaultTraceName, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(output.Name, CommonOptions.DefaultTraceName, StringComparison.OrdinalIgnoreCase)) { DateTime now = DateTime.Now; FileInfo processMainModuleFileInfo = new(processMainModuleFileName); @@ -565,13 +565,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, @@ -589,14 +589,14 @@ public static Command CollectCommand() 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: parseResult.GetValue(CommonOptions.ProvidersOption) ?? string.Empty, + profile: parseResult.GetValue(CommonOptions.ProfileOption) ?? string.Empty, 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), @@ -619,53 +619,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/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/Options/CommonOptions.cs b/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs index 98be9c055a..08ed6c4fc7 100644 --- a/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs +++ b/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs @@ -1,22 +1,44 @@ // 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." + // TODO: Can we specify an actual type? }; - 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 +50,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") { From b4112b56cafa261b726703b34634e24eced8547e Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Wed, 17 Sep 2025 19:02:58 +0000 Subject: [PATCH 06/25] [DotnetTrace] Add collect-linux skeleton --- src/Tools/Common/Commands/Utils.cs | 1 + .../Commands/CollectLinuxCommand.cs | 88 +++++++++++++++++++ src/Tools/dotnet-trace/Program.cs | 1 + 3 files changed, 90 insertions(+) create mode 100644 src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs diff --git a/src/Tools/Common/Commands/Utils.cs b/src/Tools/Common/Commands/Utils.cs index 0b28a7ff2e..03b317f606 100644 --- a/src/Tools/Common/Commands/Utils.cs +++ b/src/Tools/Common/Commands/Utils.cs @@ -211,6 +211,7 @@ internal enum ReturnCode SessionCreationError, TracingError, ArgumentError, + PlatformNotSupportedError, UnknownError } } diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs new file mode 100644 index 0000000000..f9d79ff298 --- /dev/null +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -0,0 +1,88 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.Internal.Common.Utils; + +namespace Microsoft.Diagnostics.Tools.Trace +{ + internal static class CollectLinuxCommandHandler + { + internal sealed record CollectLinuxArgs( + string[] Providers, + string ClrEventLevel, + string ClrEvents, + string[] PerfEvents, + string[] Profiles, + FileInfo Output, + TimeSpan Duration, + string Name, + int ProcessId); + + /// + /// Collects diagnostic traces using perf_events, a Linux OS technology. collect-linux requires admin privileges to capture kernel- and user-mode events, and by default, captures events from all processes. + /// This Linux-only command includes the same .NET events as dotnet-trace collect, and it uses the kernel’s user_events mechanism to emit .NET events as perf events, enabling unification of user-space .NET events with kernel-space system events. + /// + private static int CollectLinux(CollectLinuxArgs args) + { + if (!OperatingSystem.IsLinux()) + { + Console.Error.WriteLine("The collect-linux command is only supported on Linux."); + return (int)ReturnCode.PlatformNotSupportedError; + } + + return RunRecordTrace(args); + } + + public static Command CollectLinuxCommand() + { + Command collectLinuxCommand = new("collect-linux") + { + CommonOptions.ProvidersOption, + CommonOptions.CLREventLevelOption, + CommonOptions.CLREventsOption, + PerfEventsOption, + CommonOptions.ProfileOption, + CommonOptions.OutputPathOption, + CommonOptions.DurationOption, + CommonOptions.NameOption, + CommonOptions.ProcessIdOption + }; + collectLinuxCommand.TreatUnmatchedTokensAsErrors = true; // collect-linux currently does not support child process tracing. + collectLinuxCommand.Description = "Collects diagnostic traces using perf_events, a Linux OS technology. collect-linux requires admin privileges to capture kernel- and user-mode events, and by default, captures events from all processes. This Linux-only command includes the same .NET events as dotnet-trace collect, and it uses the kernel’s user_events mechanism to emit .NET events as perf events, enabling unification of user-space .NET events with kernel-space system events."; + + collectLinuxCommand.SetAction((parseResult, ct) => { + string providersValue = parseResult.GetValue(CommonOptions.ProvidersOption) ?? string.Empty; + string perfEventsValue = parseResult.GetValue(PerfEventsOption) ?? string.Empty; + string profilesValue = parseResult.GetValue(CommonOptions.ProfileOption) ?? string.Empty; + + int rc = CollectLinux(new CollectLinuxArgs( + Providers: providersValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), + ClrEventLevel: parseResult.GetValue(CommonOptions.CLREventLevelOption) ?? string.Empty, + ClrEvents: parseResult.GetValue(CommonOptions.CLREventsOption) ?? string.Empty, + PerfEvents: perfEventsValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), + Profiles: profilesValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), + Output: parseResult.GetValue(CommonOptions.OutputPathOption) ?? new FileInfo(CommonOptions.DefaultTraceName), + Duration: parseResult.GetValue(CommonOptions.DurationOption), + Name: parseResult.GetValue(CommonOptions.NameOption) ?? string.Empty, + ProcessId: parseResult.GetValue(CommonOptions.ProcessIdOption))); + return Task.FromResult(rc); + }); + + return collectLinuxCommand; + } + + private static int RunRecordTrace(CollectLinuxArgs args) + { + return (int)ReturnCode.Ok; + } + + private static readonly Option PerfEventsOption = + new("--perf-events") + { + Description = @"Comma-separated list of kernel perf events (e.g. syscalls:sys_enter_execve,sched:sched_switch)." + }; + } +} 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(), From e5f39759b511d5efc220b2d639f4ddfa9267c702 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Wed, 17 Sep 2025 19:29:08 +0000 Subject: [PATCH 07/25] [DotnetTrace][CollectLinux] Start record-trace --- .../Commands/CollectLinuxCommand.cs | 103 +++++++++++++++++- src/Tools/dotnet-trace/dotnet-trace.csproj | 15 +++ 2 files changed, 115 insertions(+), 3 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index f9d79ff298..b280472128 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -1,15 +1,20 @@ // 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.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.Internal.Common.Utils; namespace Microsoft.Diagnostics.Tools.Trace { - internal static class CollectLinuxCommandHandler + internal static partial class CollectLinuxCommandHandler { + private static int s_recordStatus; + internal sealed record CollectLinuxArgs( string[] Providers, string ClrEventLevel, @@ -30,7 +35,7 @@ private static int CollectLinux(CollectLinuxArgs args) if (!OperatingSystem.IsLinux()) { Console.Error.WriteLine("The collect-linux command is only supported on Linux."); - return (int)ReturnCode.PlatformNotSupportedError; + return (int)ReturnCode.ArgumentError; } return RunRecordTrace(args); @@ -76,7 +81,79 @@ public static Command CollectLinuxCommand() private static int RunRecordTrace(CollectLinuxArgs args) { - return (int)ReturnCode.Ok; + s_recordStatus = 0; + + ConsoleCancelEventHandler handler = (sender, e) => + { + e.Cancel = true; + s_recordStatus = 1; + }; + Console.CancelKeyPress += handler; + + IEnumerable recordTraceArgList = BuildRecordTraceArgs(args, out string scriptPath); + + string options = string.Join(' ', recordTraceArgList); + byte[] command = Encoding.UTF8.GetBytes(options); + int rc; + try + { + rc = RecordTrace(command, (UIntPtr)command.Length, OutputHandler); + } + finally + { + Console.CancelKeyPress -= handler; + if (!string.IsNullOrEmpty(scriptPath)) + { + try { + if (File.Exists(scriptPath)) + { + File.Delete(scriptPath); + } + } catch { } + } + } + + return rc; + } + + private static List BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath) + { + Console.WriteLine($"{args.ProcessId}"); + List recordTraceArgs = new(); + + recordTraceArgs.Add("--on-cpu"); + + return recordTraceArgs; + } + + private static int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) + { + OutputType ot = (OutputType)type; + if (ot != OutputType.Progress) + { + int len = checked((int)dataLen); + if (len > 0) + { + byte[] buffer = new byte[len]; + Marshal.Copy(data, buffer, 0, len); + string text = Encoding.UTF8.GetString(buffer); + switch (ot) + { + case OutputType.Normal: + case OutputType.Live: + Console.Out.WriteLine(text); + break; + case OutputType.Error: + Console.Error.WriteLine(text); + break; + default: + Console.Error.WriteLine($"[{ot}] {text}"); + break; + } + } + } + + return s_recordStatus; } private static readonly Option PerfEventsOption = @@ -84,5 +161,25 @@ private static int RunRecordTrace(CollectLinuxArgs args) { Description = @"Comma-separated list of kernel perf events (e.g. syscalls:sys_enter_execve,sched:sched_switch)." }; + + private enum OutputType : uint + { + Normal = 0, + Live = 1, + Error = 2, + Progress = 3, + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate int recordTraceCallback( + [In] uint type, + [In] IntPtr data, + [In] UIntPtr dataLen); + + [LibraryImport("recordtrace")] + private static partial int RecordTrace( + byte[] command, + UIntPtr commandLen, + recordTraceCallback callback); } } diff --git a/src/Tools/dotnet-trace/dotnet-trace.csproj b/src/Tools/dotnet-trace/dotnet-trace.csproj index 71b6aacb2e..64b755ca50 100644 --- a/src/Tools/dotnet-trace/dotnet-trace.csproj +++ b/src/Tools/dotnet-trace/dotnet-trace.csproj @@ -14,6 +14,7 @@ + @@ -29,4 +30,18 @@ + + + <_RecordTraceFromPackage>$(PkgMicrosoft_OneCollect_RecordTrace)/runtimes/$(RuntimeIdentifier)/native/librecordtrace.so + + + + <_RecordTraceResolved Include="$(_RecordTraceLocal)" Condition="'$(RuntimeIdentifier)' != '' AND Exists('$(_RecordTraceLocal)')" /> + <_RecordTraceResolved Include="$(_RecordTraceFromPackage)" Condition="'@(_RecordTraceResolved)' == '' AND '$(RuntimeIdentifier)' != '' AND Exists('$(_RecordTraceFromPackage)')" /> + + + + + + From 677e54e2e7bf4ee457d4b60c60b5989222be13d9 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Wed, 17 Sep 2025 19:38:27 +0000 Subject: [PATCH 08/25] [DotnetTrace][CollectLinux] Build record-trace args --- .../Commands/CollectLinuxCommand.cs | 118 +++++++++++++++++- 1 file changed, 116 insertions(+), 2 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index b280472128..7e244f317e 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -6,7 +6,9 @@ using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; +using System.Text; using System.Threading.Tasks; +using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Internal.Common.Utils; namespace Microsoft.Diagnostics.Tools.Trace @@ -38,6 +40,12 @@ private static int CollectLinux(CollectLinuxArgs args) return (int)ReturnCode.ArgumentError; } + if (args.ProcessId != 0 && !string.IsNullOrEmpty(args.Name)) + { + Console.Error.WriteLine("Only one of --process-id or --name can be specified."); + return (int)ReturnCode.ArgumentError; + } + return RunRecordTrace(args); } @@ -118,14 +126,120 @@ private static int RunRecordTrace(CollectLinuxArgs args) private static List BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath) { - Console.WriteLine($"{args.ProcessId}"); + scriptPath = null; List recordTraceArgs = new(); - recordTraceArgs.Add("--on-cpu"); + foreach (string profile in args.Profiles) + { + if (profile.Equals("kernel-cpu", StringComparison.OrdinalIgnoreCase)) + { + recordTraceArgs.Add("--on-cpu"); + } + if (profile.Equals("kernel-cswitch", StringComparison.OrdinalIgnoreCase)) + { + recordTraceArgs.Add("--off-cpu"); + } + } + + int pid = args.ProcessId; + if (!string.IsNullOrEmpty(args.Name)) + { + pid = CommandUtils.FindProcessIdWithName(args.Name); + } + if (pid > 0) + { + recordTraceArgs.Add($"--pid"); + recordTraceArgs.Add($"{pid}"); + } + + string resolvedOutput = ResolveOutputPath(args.Output, pid); + recordTraceArgs.Add($"--out"); + recordTraceArgs.Add(resolvedOutput); + + if (args.Duration != default(TimeSpan)) + { + recordTraceArgs.Add($"--duration"); + recordTraceArgs.Add(args.Duration.ToString()); + } + + StringBuilder scriptBuilder = new(); + + string[] profiles = args.Profiles; + if (args.Profiles.Length == 0 && args.Providers.Length == 0 && string.IsNullOrEmpty(args.ClrEvents)) + { + Console.WriteLine("No profile or providers specified, defaulting to trace profile 'dotnet-common'"); + profiles = new[] { "dotnet-common" }; + } + + List providerCollection = ProviderUtils.ToProviders(args.Providers, args.ClrEvents, args.ClrEventLevel, profiles, true); + foreach (EventPipeProvider provider in providerCollection) + { + string providerName = provider.Name; + string providerNameSanitized = providerName.Replace('-', '_').Replace('.', '_'); + long keywords = provider.Keywords; + uint eventLevel = (uint)provider.EventLevel; + IDictionary arguments = provider.Arguments; + if (arguments != null && arguments.Count > 0) + { + scriptBuilder.Append($"set_dotnet_filter_args(\n\t\"{providerName}\""); + foreach ((string key, string value) in arguments) + { + scriptBuilder.Append($",\n\t\"{key}={value}\""); + } + scriptBuilder.Append($");\n"); + } + + scriptBuilder.Append($"let {providerNameSanitized}_flags = new_dotnet_provider_flags();\n"); + scriptBuilder.Append($"record_dotnet_provider(\"{providerName}\", 0x{keywords:X}, {eventLevel}, {providerNameSanitized}_flags);\n\n"); + } + + foreach (string perfEvent in args.PerfEvents) + { + if (string.IsNullOrWhiteSpace(perfEvent) || !perfEvent.Contains(':', StringComparison.Ordinal)) + { + throw new ArgumentException($"Invalid perf event specification '{perfEvent}'. Expected format 'provider:event'."); + } + + 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]; + scriptBuilder.Append($"let {perfEventName} = event_from_tracefs(\"{perfProvider}\", \"{perfEventName}\");\nrecord_event({perfEventName});\n\n"); + } + + string scriptText = scriptBuilder.ToString(); + string scriptFileName = $"{Path.GetFileNameWithoutExtension(resolvedOutput)}.script"; + scriptPath = Path.Combine(Environment.CurrentDirectory, scriptFileName); + File.WriteAllText(scriptPath, scriptText); + + recordTraceArgs.Add("--script-file"); + recordTraceArgs.Add(scriptPath); return recordTraceArgs; } + private static string ResolveOutputPath(FileInfo output, int processId) + { + if (!string.Equals(output.Name, CommonOptions.DefaultTraceName, StringComparison.OrdinalIgnoreCase)) + { + return output.Name; + } + + DateTime now = DateTime.Now; + if (processId > 0) + { + Process process = Process.GetProcessById(processId); + FileInfo processMainModuleFileInfo = new(process.MainModule.FileName); + return $"{processMainModuleFileInfo.Name}_{now:yyyyMMdd}_{now:HHmmss}.nettrace"; + } + + return $"collect_linux_{now:yyyyMMdd}_{now:HHmmss}.nettrace"; + } + private static int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) { OutputType ot = (OutputType)type; From 31186a896048f90c718c5aa09a7e614d40ecdcaf Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Wed, 17 Sep 2025 20:10:20 +0000 Subject: [PATCH 09/25] [DotnetTrace] Update profiles --- .../CommandLine/Commands/CollectCommand.cs | 4 +- .../Commands/ListProfilesCommandHandler.cs | 90 ++++++++++++++++++- 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index 0623d69683..6cab798bd3 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -110,8 +110,8 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur if (profile.Length == 0 && providers.Length == 0 && clrevents.Length == 0) { - ConsoleWriteLine("No profile or providers specified, defaulting to trace profile 'cpu-sampling'"); - profile = "cpu-sampling"; + ConsoleWriteLine("No profile or providers specified, defaulting to trace profile 'dotnet-common'"); + profile = "dotnet-common"; } Dictionary enabledBy = new(); diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs b/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs index 7e8c2cea56..f9319b2976 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs @@ -14,13 +14,52 @@ namespace Microsoft.Diagnostics.Tools.Trace { internal sealed class ListProfilesCommandHandler { + private static long defaultKeyword = 0x1 | // GC + 0x4 | // Loader + 0x8 | // AssemblyLoader + 0x10 | // JIT + 0x8000 | // Exceptions + 0x10000 | // Threading + 0x20000 | // JittedMethodILToNativeMap + 0x1000000000; // Contention + + private static string dotnetCommonDescription = """ + Lightweight .NET runtime diagnostics designed to stay low overhead. + Includes: + GC + AssemblyLoader + Jit + Exception + Threading + JittedMethodILToNativeMap + Compilation + Contention + Equivalent to --providers "Microsoft-Windows-DotNETRuntime:0x100003801D:4". + """; + public static int GetProfiles() { try { + Console.Out.WriteLine("dotnet-trace collect profiles:"); + int profileNameWidth = ProfileNamesMaxWidth(DotNETRuntimeProfiles); foreach (Profile profile in DotNETRuntimeProfiles) { - Console.Out.WriteLine($"\t{profile.Name,-16} - {profile.Description}"); + PrintProfile(profile, profileNameWidth); + } + + if (OperatingSystem.IsLinux()) + { + Console.Out.WriteLine("\ndotnet-trace collect-linux profiles:"); + profileNameWidth = Math.Max(profileNameWidth, ProfileNamesMaxWidth(LinuxPerfEventProfiles)); + foreach (Profile profile in DotNETRuntimeProfiles) + { + PrintProfile(profile, profileNameWidth); + } + foreach (Profile profile in LinuxPerfEventProfiles) + { + PrintProfile(profile, profileNameWidth); + } } return 0; @@ -44,12 +83,17 @@ public static Command ListProfilesCommand() internal static IEnumerable DotNETRuntimeProfiles { 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) to identify hotspots over time. Uses the runtime sample profiler with managed stacks."), new Profile( "gc-verbose", new EventPipeProvider[] { @@ -104,6 +148,44 @@ public static Command ListProfilesCommand() "Captures ADO.NET and Entity Framework database commands") }; + internal static IEnumerable LinuxPerfEventProfiles { get; } = new[] { + new Profile( + "kernel-cpu", + providers: Array.Empty(), + description: "Kernel CPU sampling (perf-based), emitted as Universal.Events/cpu, for precise on-CPU attribution."), + new Profile( + "kernel-cswitch", + providers: Array.Empty(), + description: "Kernel thread context switches, emitted as Universal.Events/cswitch, for on/off-CPU and scheduler analysis.") + }; + + private static int ProfileNamesMaxWidth(IEnumerable profiles) + { + int maxWidth = 0; + foreach (Profile profile in profiles) + { + if (profile.Name.Length > maxWidth) + { + maxWidth = profile.Name.Length; + } + } + + return maxWidth; + } + + private static void PrintProfile(Profile profile, int nameColumnWidth) + { + string[] descriptionLines = profile.Description.Replace("\r\n", "\n").Split('\n'); + + Console.Out.WriteLine($"\t{profile.Name.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 /// From 44593bb20b9a49fa68a23dfa10d16333f2d4cd62 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Wed, 17 Sep 2025 21:47:02 +0000 Subject: [PATCH 10/25] [DotnetTrace] Update collect to new provider unifier --- .../CommandLine/Commands/CollectCommand.cs | 88 +++++++------------ src/Tools/dotnet-trace/ProviderUtils.cs | 56 +++++++++++- 2 files changed, 84 insertions(+), 60 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index 6cab798bd3..696334ccfa 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -54,7 +54,7 @@ private static void ConsoleWriteLine(string str) /// A string, parsed as [payload_field_name]:[payload_field_value] pairs separated by commas, that will stop the trace upon hitting an event with a matching payload. Requires `--stopping-event-provider-name` and `--stopping-event-event-name` to be set. /// Collect rundown events. /// - private static async Task Collect(CancellationToken ct, CommandLineConfiguration cliConfig, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string diagnosticPort, bool showchildio, bool resumeRuntime, string stoppingEventProviderName, string stoppingEventEventName, string stoppingEventPayloadFilter, bool? rundown, string dsrouter) + private static async Task Collect(CancellationToken ct, CommandLineConfiguration cliConfig, int processId, FileInfo output, uint buffersize, string[] providers, string[] profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string diagnosticPort, bool showchildio, bool resumeRuntime, string stoppingEventProviderName, string stoppingEventEventName, string stoppingEventPayloadFilter, bool? rundown, string dsrouter) { bool collectionStopped = false; bool cancelOnEnter = true; @@ -111,34 +111,35 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur if (profile.Length == 0 && providers.Length == 0 && clrevents.Length == 0) { ConsoleWriteLine("No profile or providers specified, defaulting to trace profile 'dotnet-common'"); - profile = "dotnet-common"; + profile = new[] { "dotnet-common" }; } - Dictionary enabledBy = new(); - - List providerCollection = ProviderUtils.ToProviders(providers); - foreach (EventPipeProvider providerCollectionProvider in providerCollection) - { - enabledBy[providerCollectionProvider.Name] = "--providers "; - } - - long rundownKeyword = EventPipeSession.DefaultRundownKeyword; + long rundownKeyword = 0; RetryStrategy retryStrategy = RetryStrategy.NothingToRetry; if (profile.Length != 0) { - Profile selectedProfile = ListProfilesCommandHandler.DotNETRuntimeProfiles - .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); - if (selectedProfile == null) + foreach (string prof in profile) { - Console.Error.WriteLine($"Invalid profile name: {profile}"); - return (int)ReturnCode.ArgumentError; - } + Profile selectedProfile = ListProfilesCommandHandler.DotNETRuntimeProfiles + .FirstOrDefault(p => p.Name.Equals(prof, StringComparison.OrdinalIgnoreCase)); + if (selectedProfile == null) + { + Console.Error.WriteLine($"Invalid profile name: {prof}"); + return (int)ReturnCode.ArgumentError; + } - rundownKeyword = selectedProfile.RundownKeyword; - retryStrategy = selectedProfile.RetryStrategy; + rundownKeyword |= selectedProfile.RundownKeyword; + if (selectedProfile.RetryStrategy > retryStrategy) + { + retryStrategy = selectedProfile.RetryStrategy; + } + } + } - ProviderUtils.MergeProfileAndProviders(selectedProfile, providerCollection, enabledBy); + if (rundownKeyword == 0) + { + rundownKeyword = EventPipeSession.DefaultRundownKeyword; } if (rundown.HasValue) @@ -155,31 +156,13 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur } } - // Parse --clrevents parameter - if (clrevents.Length != 0) - { - // Ignore --clrevents if CLR event provider was already specified via --profile or --providers command. - if (enabledBy.ContainsKey(ProviderUtils.CLREventProviderName)) - { - ConsoleWriteLine($"The argument --clrevents {clrevents} will be ignored because the CLR provider was configured via either --profile or --providers command."); - } - else - { - EventPipeProvider clrProvider = ProviderUtils.ToCLREventPipeProvider(clrevents, clreventlevel); - providerCollection.Add(clrProvider); - enabledBy[ProviderUtils.CLREventProviderName] = "--clrevents"; - } - } - - + List providerCollection = ProviderUtils.ToProviders(providers, clrevents, clreventlevel, profile, !IsQuiet); 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); @@ -524,20 +507,6 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur return ret; } - private static void PrintProviders(IReadOnlyList providers, Dictionary enabledBy) - { - ConsoleWriteLine(""); - ConsoleWriteLine(string.Format("{0, -40}", "Provider Name") + string.Format("{0, -20}", "Keywords") + - string.Format("{0, -20}", "Level") + "Enabled By"); // +4 is for the tab - foreach (EventPipeProvider provider in providers) - { - ConsoleWriteLine(string.Format("{0, -80}", $"{GetProviderDisplayString(provider)}") + $"{enabledBy[provider.Name]}"); - } - ConsoleWriteLine(""); - } - private static string GetProviderDisplayString(EventPipeProvider provider) => - string.Format("{0, -40}", provider.Name) + string.Format("0x{0, -18}", $"{provider.Keywords:X16}") + string.Format("{0, -8}", provider.EventLevel.ToString() + $"({(int)provider.EventLevel})"); - private static string GetSize(long length) { if (length > 1e9) @@ -585,14 +554,18 @@ public static Command CollectCommand() collectCommand.TreatUnmatchedTokensAsErrors = false; // see the logic in Program.Main that handles UnmatchedTokens collectCommand.Description = "Collects a diagnostic trace from a currently running process or launch a child process and trace it. Append -- to the collect command to instruct the tool to run a command and trace it immediately. When tracing a child process, the exit code of dotnet-trace shall be that of the traced process unless the trace process encounters an error."; - collectCommand.SetAction((parseResult, ct) => Collect( + collectCommand.SetAction((parseResult, ct) => { + string providersValue = parseResult.GetValue(CommonOptions.ProvidersOption) ?? string.Empty; + string profileValue = parseResult.GetValue(CommonOptions.ProfileOption) ?? string.Empty; + + return Collect( ct, cliConfig: parseResult.Configuration, processId: parseResult.GetValue(CommonOptions.ProcessIdOption), output: parseResult.GetValue(CommonOptions.OutputPathOption), buffersize: parseResult.GetValue(CircularBufferOption), - providers: parseResult.GetValue(CommonOptions.ProvidersOption) ?? string.Empty, - profile: parseResult.GetValue(CommonOptions.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(CommonOptions.DurationOption), clrevents: parseResult.GetValue(CommonOptions.CLREventsOption) ?? string.Empty, @@ -605,7 +578,8 @@ public static Command CollectCommand() stoppingEventEventName: parseResult.GetValue(StoppingEventEventNameOption), stoppingEventPayloadFilter: parseResult.GetValue(StoppingEventPayloadFilterOption), rundown: parseResult.GetValue(RundownOption), - dsrouter: parseResult.GetValue(DSRouterOption))); + dsrouter: parseResult.GetValue(DSRouterOption)); + }); return collectCommand; } diff --git a/src/Tools/dotnet-trace/ProviderUtils.cs b/src/Tools/dotnet-trace/ProviderUtils.cs index 9f01837b31..72fe845b93 100644 --- a/src/Tools/dotnet-trace/ProviderUtils.cs +++ b/src/Tools/dotnet-trace/ProviderUtils.cs @@ -64,6 +64,13 @@ internal static class ProviderUtils { "allocationsampling", 0x80000000000 }, }; + private enum ProviderSource + { + ProvidersArg = 1, + CLREventsArg = 2, + ProfileArg = 4, + } + public static void MergeProfileAndProviders(Profile selectedProfile, List providerCollection, Dictionary enabledBy) { List profileProviders = new(); @@ -144,9 +151,10 @@ public static List ToProviders(string providersRawInput) return providers.ToList(); } - public static List ToProviders(string[] providersArg, string clreventsArg, string clreventlevel, string[] profiles) + public static List ToProviders(string[] providersArg, string clreventsArg, string clreventlevel, string[] profiles, bool shouldPrintProviders) { Dictionary merged = new(StringComparer.OrdinalIgnoreCase); + Dictionary providerSources = new(StringComparer.OrdinalIgnoreCase); foreach (string providerArg in providersArg) { @@ -154,6 +162,7 @@ public static List ToProviders(string[] providersArg, string if (!merged.TryGetValue(provider.Name, out EventPipeProvider existing)) { merged[provider.Name] = provider; + providerSources[provider.Name] = (int)ProviderSource.ProvidersArg; } else { @@ -169,10 +178,12 @@ public static List ToProviders(string[] providersArg, string if (!merged.TryGetValue(provider.Name, out EventPipeProvider existing)) { merged[provider.Name] = provider; + providerSources[provider.Name] = (int)ProviderSource.CLREventsArg; } else { merged[provider.Name] = MergeProviderConfigs(existing, provider); + providerSources[provider.Name] |= (int)ProviderSource.CLREventsArg; } } } @@ -190,12 +201,21 @@ public static List ToProviders(string[] providersArg, string IEnumerable profileProviders = dotnetProfile.Providers; foreach (EventPipeProvider provider in profileProviders) { - merged.TryAdd(provider.Name, provider); + if (merged.TryAdd(provider.Name, provider)) + { + providerSources[provider.Name] = (int)ProviderSource.ProfileArg; + } // Prefer providers set through --providers and --clrevents over implicit profile configuration } } - return merged.Values.ToList(); + List unifiedProviders = merged.Values.ToList(); + if (shouldPrintProviders) + { + PrintProviders(unifiedProviders, providerSources); + } + + return unifiedProviders; } private static EventPipeProvider MergeProviderConfigs(EventPipeProvider providerConfigA, EventPipeProvider providerConfigB) @@ -214,6 +234,36 @@ private static EventPipeProvider MergeProviderConfigs(EventPipeProvider provider return new EventPipeProvider(providerConfigA.Name, level, providerConfigA.Keywords | providerConfigB.Keywords, providerConfigA.Arguments ?? providerConfigB.Arguments); } + private static void PrintProviders(IReadOnlyList providers, Dictionary enabledBy) + { + 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) { if (clreventslist == null || clreventslist.Length == 0) From b190d73b31b9a64b3e248397eeb2372821d415e2 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 19 Sep 2025 01:50:30 -0400 Subject: [PATCH 11/25] [DotnetCounters] Remove Extensions reference --- src/Tools/dotnet-counters/dotnet-counters.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Tools/dotnet-counters/dotnet-counters.csproj b/src/Tools/dotnet-counters/dotnet-counters.csproj index 95bf4c4060..cd660176dd 100644 --- a/src/Tools/dotnet-counters/dotnet-counters.csproj +++ b/src/Tools/dotnet-counters/dotnet-counters.csproj @@ -12,7 +12,6 @@ - From 9f1ef6da657f99264e3597fbf4de8b5b5227137d Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 19 Sep 2025 01:51:19 -0400 Subject: [PATCH 12/25] [DotnetTrace] Update tests --- src/Tools/dotnet-trace/ProviderUtils.cs | 86 +---- .../dotnet-trace/ProfileProviderMerging.cs | 44 --- .../dotnet-trace/ProviderCompositionTests.cs | 336 +++++++++++++++++ src/tests/dotnet-trace/ProviderParsing.cs | 341 ------------------ 4 files changed, 337 insertions(+), 470 deletions(-) delete mode 100644 src/tests/dotnet-trace/ProfileProviderMerging.cs create mode 100644 src/tests/dotnet-trace/ProviderCompositionTests.cs delete mode 100644 src/tests/dotnet-trace/ProviderParsing.cs diff --git a/src/Tools/dotnet-trace/ProviderUtils.cs b/src/Tools/dotnet-trace/ProviderUtils.cs index 72fe845b93..3f8f405925 100644 --- a/src/Tools/dotnet-trace/ProviderUtils.cs +++ b/src/Tools/dotnet-trace/ProviderUtils.cs @@ -71,86 +71,6 @@ private enum ProviderSource ProfileArg = 4, } - 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 static List ToProviders(string providersRawInput) - { - if (providersRawInput == null) - { - throw new ArgumentNullException(nameof(providersRawInput)); - } - - if (string.IsNullOrWhiteSpace(providersRawInput)) - { - return new List(); - } - - 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(); - } - public static List ToProviders(string[] providersArg, string clreventsArg, string clreventlevel, string[] profiles, bool shouldPrintProviders) { Dictionary merged = new(StringComparer.OrdinalIgnoreCase); @@ -180,11 +100,7 @@ public static List ToProviders(string[] providersArg, string merged[provider.Name] = provider; providerSources[provider.Name] = (int)ProviderSource.CLREventsArg; } - else - { - merged[provider.Name] = MergeProviderConfigs(existing, provider); - providerSources[provider.Name] |= (int)ProviderSource.CLREventsArg; - } + // Prefer providers set through --providers over --clrevents } } diff --git a/src/tests/dotnet-trace/ProfileProviderMerging.cs b/src/tests/dotnet-trace/ProfileProviderMerging.cs deleted file mode 100644 index 742cf13dac..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 = ProviderUtils.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); - - ProviderUtils.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..45dbd77d2f --- /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.ToProviders(providers, string.Empty, string.Empty, Array.Empty(), false); + 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.ToProviders(providers, string.Empty, string.Empty, Array.Empty(), false)); + } + + 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.ToProviders(providers, string.Empty, string.Empty, Array.Empty(), false); + 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.ToProviders(providers, string.Empty, string.Empty, Array.Empty(), false)); + } + + 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.ToProviders(providersArg, string.Empty, string.Empty, Array.Empty(), false); + 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.ToProviders(providersArg, string.Empty, string.Empty, Array.Empty(), false)); + } + + 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.Verbose, 0x1), + 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.ToProviders(providersArg, clreventsArg, clreventLevel, profiles, false); + 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.ToProviders(providersArg, clreventsArg, clreventLevel, profiles, false)); + } + + 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-common", "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.ToProviders(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.ToProviders(providersArg, clreventsArg, clreventLevel, profiles, false); + 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.ToProviders(providers, string.Empty, string.Empty, Array.Empty(), false)); + 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.ToProviders(providersArg, clreventsArg, clreventLevel, profiles, false); + 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 ddb5d0a7b4..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 = ProviderUtils.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 = ProviderUtils.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(() => ProviderUtils.ToProviders(providerToParse)); - } - - [Theory] - [InlineData(":::")] - [InlineData(":1:1")] - public void InvalidProvider_CorrectlyThrows(string providerToParse) - { - Assert.Throws(() => ProviderUtils.ToProviders(providerToParse)); - } - - [Theory] - [InlineData("VeryCoolProvider:0xFFFFFFFFFFFFFFFF:5:FilterAndPayloadSpecs=\"QuotedValue\"")] - public void ValidProviderKeyword_CorrectlyParses(string providerToParse) - { - List parsedProviders = ProviderUtils.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 = ProviderUtils.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(() => ProviderUtils.ToProviders(providerToParse)); - } - - [Theory] - [InlineData("VeryCoolProvider:__:5:FilterAndPayloadSpecs=\"QuotedValue\"")] - [InlineData("VeryCoolProvider:gh::FilterAndPayloadSpecs=\"QuotedValue\"")] - public void Invalidkeyword_CorrectlyThrows(string providerToParse) - { - Assert.Throws(() => ProviderUtils.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 = ProviderUtils.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(() => ProviderUtils.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(() => ProviderUtils.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(() => ProviderUtils.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 = ProviderUtils.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 = ProviderUtils.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 = ProviderUtils.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 = ProviderUtils.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 = ProviderUtils.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 = ProviderUtils.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 = ProviderUtils.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 = ProviderUtils.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 = ProviderUtils.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 = ProviderUtils.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(() => ProviderUtils.ToProviders(providerToParse)); - } - - [Theory] - [InlineData("DupeProvider,DupeProvider:0xF:LogAlways")] - public void DeDupeProviders_DefaultAndSpecified(string providersToParse) - { - List parsedProviders = ProviderUtils.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 = ProviderUtils.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(() => ProviderUtils.ToProviders(providersToParse)); - } - } -} From c8e53ea3aa02190bab82cbb5bc2ca3abfa06d38b Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Fri, 19 Sep 2025 14:06:50 -0400 Subject: [PATCH 13/25] Address Feedback --- .../CommandLine/Commands/CollectCommand.cs | 13 ++-- .../Commands/CollectLinuxCommand.cs | 42 ++++++------ .../Commands/ListProfilesCommandHandler.cs | 66 +++++++++---------- .../CommandLine/Options/CommonOptions.cs | 1 - src/Tools/dotnet-trace/Profile.cs | 4 ++ src/Tools/dotnet-trace/ProviderUtils.cs | 47 +++++++------ .../dotnet-trace/ProviderCompositionTests.cs | 28 ++++---- 7 files changed, 105 insertions(+), 96 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index 696334ccfa..1f5c69641c 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -110,8 +110,8 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur if (profile.Length == 0 && providers.Length == 0 && clrevents.Length == 0) { - ConsoleWriteLine("No profile or providers specified, defaulting to trace profile 'dotnet-common'"); - profile = new[] { "dotnet-common" }; + ConsoleWriteLine("No profile or providers specified, defaulting to trace profiles 'dotnet-common' + 'dotnet-sampled-thread-time'."); + profile = new[] { "dotnet-common", "dotnet-sampled-thread-time" }; } long rundownKeyword = 0; @@ -121,13 +121,18 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur { foreach (string prof in profile) { - Profile selectedProfile = ListProfilesCommandHandler.DotNETRuntimeProfiles + Profile selectedProfile = ListProfilesCommandHandler.TraceProfiles .FirstOrDefault(p => p.Name.Equals(prof, StringComparison.OrdinalIgnoreCase)); if (selectedProfile == null) { Console.Error.WriteLine($"Invalid profile name: {prof}"); return (int)ReturnCode.ArgumentError; } + if (!string.IsNullOrEmpty(selectedProfile.VerbExclusivity) && !string.Equals(selectedProfile.VerbExclusivity, "collect", StringComparison.OrdinalIgnoreCase)) + { + Console.Error.WriteLine($"The specified profile '{selectedProfile.Name}' does not apply to `dotnet-trace collect`."); + return (int)ReturnCode.ArgumentError; + } rundownKeyword |= selectedProfile.RundownKeyword; if (selectedProfile.RetryStrategy > retryStrategy) @@ -156,7 +161,7 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur } } - List providerCollection = ProviderUtils.ToProviders(providers, clrevents, clreventlevel, profile, !IsQuiet); + List providerCollection = ProviderUtils.ComputeProviderConfig(providers, clrevents, clreventlevel, profile, !IsQuiet, "collect"); if (providerCollection.Count <= 0) { Console.Error.WriteLine("No providers were specified to start a trace."); diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index 7e244f317e..056d09307f 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -5,6 +5,7 @@ using System.CommandLine; using System.Diagnostics; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; @@ -128,19 +129,6 @@ private static List BuildRecordTraceArgs(CollectLinuxArgs args, out stri { scriptPath = null; List recordTraceArgs = new(); - - foreach (string profile in args.Profiles) - { - if (profile.Equals("kernel-cpu", StringComparison.OrdinalIgnoreCase)) - { - recordTraceArgs.Add("--on-cpu"); - } - if (profile.Equals("kernel-cswitch", StringComparison.OrdinalIgnoreCase)) - { - recordTraceArgs.Add("--off-cpu"); - } - } - int pid = args.ProcessId; if (!string.IsNullOrEmpty(args.Name)) { @@ -156,22 +144,34 @@ private static List BuildRecordTraceArgs(CollectLinuxArgs args, out stri recordTraceArgs.Add($"--out"); recordTraceArgs.Add(resolvedOutput); - if (args.Duration != default(TimeSpan)) + if (args.Duration != default) { recordTraceArgs.Add($"--duration"); recordTraceArgs.Add(args.Duration.ToString()); } - StringBuilder scriptBuilder = new(); - string[] profiles = args.Profiles; if (args.Profiles.Length == 0 && args.Providers.Length == 0 && string.IsNullOrEmpty(args.ClrEvents)) { - Console.WriteLine("No profile or providers specified, defaulting to trace profile 'dotnet-common'"); - profiles = new[] { "dotnet-common" }; + Console.WriteLine("No profile or providers specified, defaulting to trace profiles 'dotnet-common' + 'cpu-sampling'."); + profiles = new[] { "dotnet-common", "cpu-sampling" }; } - List providerCollection = ProviderUtils.ToProviders(args.Providers, args.ClrEvents, args.ClrEventLevel, profiles, true); + foreach (string profile in profiles) + { + Profile traceProfile = ListProfilesCommandHandler.TraceProfiles + .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); + + if (!string.IsNullOrEmpty(traceProfile.VerbExclusivity) && + traceProfile.VerbExclusivity.Equals("collect-linux", StringComparison.OrdinalIgnoreCase)) + { + recordTraceArgs.Add(traceProfile.CollectLinuxArgs); + } + } + + StringBuilder scriptBuilder = new(); + + List providerCollection = ProviderUtils.ComputeProviderConfig(args.Providers, args.ClrEvents, args.ClrEventLevel, profiles, true, "collect-linux"); foreach (EventPipeProvider provider in providerCollection) { string providerName = provider.Name; @@ -237,7 +237,7 @@ private static string ResolveOutputPath(FileInfo output, int processId) return $"{processMainModuleFileInfo.Name}_{now:yyyyMMdd}_{now:HHmmss}.nettrace"; } - return $"collect_linux_{now:yyyyMMdd}_{now:HHmmss}.nettrace"; + return $"trace_{now:yyyyMMdd}_{now:HHmmss}.nettrace"; } private static int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) @@ -273,7 +273,7 @@ private static int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) private static readonly Option PerfEventsOption = new("--perf-events") { - Description = @"Comma-separated list of kernel perf events (e.g. syscalls:sys_enter_execve,sched:sched_switch)." + Description = @"Comma-separated list of perf events (e.g. syscalls:sys_enter_execve,sched:sched_switch)." }; private enum OutputType : uint diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs b/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs index f9319b2976..58b86ede6e 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs @@ -15,25 +15,25 @@ namespace Microsoft.Diagnostics.Tools.Trace internal sealed class ListProfilesCommandHandler { private static long defaultKeyword = 0x1 | // GC - 0x4 | // Loader - 0x8 | // AssemblyLoader + 0x4 | // AssemblyLoader + 0x8 | // Loader 0x10 | // JIT 0x8000 | // Exceptions 0x10000 | // Threading 0x20000 | // JittedMethodILToNativeMap - 0x1000000000; // Contention + 0x1000000000; // Compilation private static string dotnetCommonDescription = """ Lightweight .NET runtime diagnostics designed to stay low overhead. Includes: GC AssemblyLoader - Jit - Exception + Loader + JIT + Exceptions Threading JittedMethodILToNativeMap Compilation - Contention Equivalent to --providers "Microsoft-Windows-DotNETRuntime:0x100003801D:4". """; @@ -41,27 +41,13 @@ public static int GetProfiles() { try { - Console.Out.WriteLine("dotnet-trace collect profiles:"); - int profileNameWidth = ProfileNamesMaxWidth(DotNETRuntimeProfiles); - foreach (Profile profile in DotNETRuntimeProfiles) + Console.Out.WriteLine("dotnet-trace profiles:"); + int profileNameWidth = ProfileNamesMaxWidth(TraceProfiles); + foreach (Profile profile in TraceProfiles) { PrintProfile(profile, profileNameWidth); } - if (OperatingSystem.IsLinux()) - { - Console.Out.WriteLine("\ndotnet-trace collect-linux profiles:"); - profileNameWidth = Math.Max(profileNameWidth, ProfileNamesMaxWidth(LinuxPerfEventProfiles)); - foreach (Profile profile in DotNETRuntimeProfiles) - { - PrintProfile(profile, profileNameWidth); - } - foreach (Profile profile in LinuxPerfEventProfiles) - { - PrintProfile(profile, profileNameWidth); - } - } - return 0; } catch (Exception ex) @@ -81,7 +67,7 @@ public static Command ListProfilesCommand() return listProfilesCommand; } - internal static IEnumerable DotNETRuntimeProfiles { get; } = new[] { + internal static IEnumerable TraceProfiles { get; } = new[] { new Profile( "dotnet-common", new EventPipeProvider[] { @@ -93,7 +79,7 @@ public static Command ListProfilesCommand() new EventPipeProvider[] { new("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational), }, - "Samples .NET thread stacks (~100 Hz) to identify hotspots over time. Uses the runtime sample profiler with managed stacks."), + "Samples .NET thread stacks (~100 Hz) toestimate how much wall clock time code is using.") { VerbExclusivity = "collect" }, new Profile( "gc-verbose", new EventPipeProvider[] { @@ -145,18 +131,15 @@ public static Command ListProfilesCommand() } ) }, - "Captures ADO.NET and Entity Framework database commands") - }; - - internal static IEnumerable LinuxPerfEventProfiles { get; } = new[] { + "Captures ADO.NET and Entity Framework database commands"), new Profile( - "kernel-cpu", + "cpu-sampling", providers: Array.Empty(), - description: "Kernel CPU sampling (perf-based), emitted as Universal.Events/cpu, for precise on-CPU attribution."), + description: "Kernel CPU sampling events for measuring CPU usage.") { VerbExclusivity = "collect-linux", CollectLinuxArgs = "--on-cpu" }, new Profile( - "kernel-cswitch", + "thread-time", providers: Array.Empty(), - description: "Kernel thread context switches, emitted as Universal.Events/cswitch, for on/off-CPU and scheduler analysis.") + 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) @@ -164,9 +147,14 @@ private static int ProfileNamesMaxWidth(IEnumerable profiles) int maxWidth = 0; foreach (Profile profile in profiles) { - if (profile.Name.Length > maxWidth) + int profileNameWidth = profile.Name.Length; + if (!string.IsNullOrEmpty(profile.VerbExclusivity)) { - maxWidth = profile.Name.Length; + profileNameWidth = $"{profile.Name} ({profile.VerbExclusivity})".Length; + } + if (profileNameWidth > maxWidth) + { + maxWidth = profileNameWidth; } } @@ -177,7 +165,13 @@ private static void PrintProfile(Profile profile, int nameColumnWidth) { string[] descriptionLines = profile.Description.Replace("\r\n", "\n").Split('\n'); - Console.Out.WriteLine($"\t{profile.Name.PadRight(nameColumnWidth)} - {descriptionLines[0]}"); + 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++) diff --git a/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs b/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs index 08ed6c4fc7..013e6e5749 100644 --- a/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs +++ b/src/Tools/dotnet-trace/CommandLine/Options/CommonOptions.cs @@ -20,7 +20,6 @@ internal static class CommonOptions @"--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? }; public static readonly Option CLREventLevelOption = diff --git a/src/Tools/dotnet-trace/Profile.cs b/src/Tools/dotnet-trace/Profile.cs index b5b1afbef6..4ba36b2445 100644 --- a/src/Tools/dotnet-trace/Profile.cs +++ b/src/Tools/dotnet-trace/Profile.cs @@ -25,5 +25,9 @@ public Profile(string name, IEnumerable providers, string des public long RundownKeyword { get; set; } = EventPipeSession.DefaultRundownKeyword; public RetryStrategy RetryStrategy { get; set; } = RetryStrategy.NothingToRetry; + + public string VerbExclusivity { get; set; } = string.Empty; + + public string CollectLinuxArgs { get; set; } = string.Empty; } } diff --git a/src/Tools/dotnet-trace/ProviderUtils.cs b/src/Tools/dotnet-trace/ProviderUtils.cs index 3f8f405925..752fbf7046 100644 --- a/src/Tools/dotnet-trace/ProviderUtils.cs +++ b/src/Tools/dotnet-trace/ProviderUtils.cs @@ -71,7 +71,7 @@ private enum ProviderSource ProfileArg = 4, } - public static List ToProviders(string[] providersArg, string clreventsArg, string clreventlevel, string[] profiles, bool shouldPrintProviders) + public static List ComputeProviderConfig(string[] providersArg, string clreventsArg, string clreventlevel, string[] profiles, bool shouldPrintProviders = false, string verbExclusivity = null) { Dictionary merged = new(StringComparer.OrdinalIgnoreCase); Dictionary providerSources = new(StringComparer.OrdinalIgnoreCase); @@ -90,38 +90,45 @@ public static List ToProviders(string[] providersArg, string } } - if (!string.IsNullOrEmpty(clreventsArg)) + foreach (string profile in profiles) { - EventPipeProvider provider = ToCLREventPipeProvider(clreventsArg, clreventlevel); - if (provider is not null) + Profile traceProfile = ListProfilesCommandHandler.TraceProfiles + .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); + + if (traceProfile == null) { - if (!merged.TryGetValue(provider.Name, out EventPipeProvider existing)) - { - merged[provider.Name] = provider; - providerSources[provider.Name] = (int)ProviderSource.CLREventsArg; - } - // Prefer providers set through --providers over --clrevents + throw new ArgumentException($"Invalid profile name: {profile}"); } - } - foreach (string profile in profiles) - { - Profile dotnetProfile = ListProfilesCommandHandler.DotNETRuntimeProfiles - .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); - if (dotnetProfile == null) + if (!string.IsNullOrEmpty(verbExclusivity) && + !string.IsNullOrEmpty(traceProfile.VerbExclusivity) && + !string.Equals(traceProfile.VerbExclusivity, verbExclusivity, StringComparison.OrdinalIgnoreCase)) { - // for collect-linux, could be linux perf event profile - continue; + throw new ArgumentException($"The specified profile '{traceProfile.Name}' does not apply to `dotnet-trace {verbExclusivity}`."); } - IEnumerable profileProviders = dotnetProfile.Providers; + 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 and --clrevents over implicit profile configuration + // 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; + } + // Prefer providers set through --providers or --profile over --clrevents } } diff --git a/src/tests/dotnet-trace/ProviderCompositionTests.cs b/src/tests/dotnet-trace/ProviderCompositionTests.cs index 45dbd77d2f..b51c6d545f 100644 --- a/src/tests/dotnet-trace/ProviderCompositionTests.cs +++ b/src/tests/dotnet-trace/ProviderCompositionTests.cs @@ -58,7 +58,7 @@ public static IEnumerable InvalidProviders() public void ProvidersArg_ParsesCorrectly(string providersArg, EventPipeProvider expected) { string[] providers = providersArg.Split(',', StringSplitOptions.RemoveEmptyEntries); - List parsedProviders = ProviderUtils.ToProviders(providers, string.Empty, string.Empty, Array.Empty(), false); + List parsedProviders = ProviderUtils.ComputeProviderConfig(providers, string.Empty, string.Empty, Array.Empty()); EventPipeProvider actual = Assert.Single(parsedProviders); Assert.Equal(expected, actual); } @@ -68,7 +68,7 @@ public void ProvidersArg_ParsesCorrectly(string providersArg, EventPipeProvider public void InvalidProvidersArg_Throws(string providersArg, Type expectedException) { string[] providers = providersArg.Split(',', StringSplitOptions.RemoveEmptyEntries); - Assert.Throws(expectedException, () => ProviderUtils.ToProviders(providers, string.Empty, string.Empty, Array.Empty(), false)); + Assert.Throws(expectedException, () => ProviderUtils.ComputeProviderConfig(providers, string.Empty, string.Empty, Array.Empty())); } public static IEnumerable MultipleValidProviders() @@ -110,7 +110,7 @@ public static IEnumerable MultipleValidProviders() public void MultipleProviders_Parse_AsExpected(string providersArg, EventPipeProvider[] expected) { string[] providers = providersArg.Split(',', StringSplitOptions.RemoveEmptyEntries); - List parsed = ProviderUtils.ToProviders(providers, string.Empty, string.Empty, Array.Empty(), false); + 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++) { @@ -132,7 +132,7 @@ public static IEnumerable MultipleInvalidProviders() public void MultipleProviders_FailureCases_Throw(string providersArg, Type expectedException) { string[] providers = providersArg.Split(',', StringSplitOptions.RemoveEmptyEntries); - Assert.Throws(expectedException, () => ProviderUtils.ToProviders(providers, string.Empty, string.Empty, Array.Empty(), false)); + Assert.Throws(expectedException, () => ProviderUtils.ComputeProviderConfig(providers, string.Empty, string.Empty, Array.Empty())); } public static IEnumerable DedupeSuccessCases() @@ -153,7 +153,7 @@ public static IEnumerable DedupeFailureCases() [MemberData(nameof(DedupeSuccessCases))] public void DedupeProviders_Success(string[] providersArg, EventPipeProvider expected) { - List list = ProviderUtils.ToProviders(providersArg, string.Empty, string.Empty, Array.Empty(), false); + List list = ProviderUtils.ComputeProviderConfig(providersArg, string.Empty, string.Empty, Array.Empty()); EventPipeProvider actual = Assert.Single(list); Assert.Equal(expected, actual); } @@ -162,7 +162,7 @@ public void DedupeProviders_Success(string[] providersArg, EventPipeProvider exp [MemberData(nameof(DedupeFailureCases))] public void DedupeProviders_Failure(string[] providersArg, Type expectedException) { - Assert.Throws(expectedException, () => ProviderUtils.ToProviders(providersArg, string.Empty, string.Empty, Array.Empty(), false)); + Assert.Throws(expectedException, () => ProviderUtils.ComputeProviderConfig(providersArg, string.Empty, string.Empty, Array.Empty())); } public static IEnumerable PrecedenceCases() @@ -181,7 +181,7 @@ public static IEnumerable PrecedenceCases() "Verbose", new[]{ "dotnet-common", "dotnet-sampled-thread-time" }, new[]{ - new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Verbose, 0x1), + new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Informational, 0x100003801D), new EventPipeProvider("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational, 0xF00000000000) } }; @@ -223,7 +223,7 @@ public static IEnumerable PrecedenceCases() [MemberData(nameof(PrecedenceCases))] public void ProviderSourcePrecedence(string[] providersArg, string clreventsArg, string clreventLevel, string[] profiles, EventPipeProvider[] expected) { - List actual = ProviderUtils.ToProviders(providersArg, clreventsArg, clreventLevel, profiles, false); + List actual = ProviderUtils.ComputeProviderConfig(providersArg, clreventsArg, clreventLevel, profiles); Assert.Equal(expected.Length, actual.Count); for (int i = 0; i < expected.Length; i++) { @@ -240,7 +240,7 @@ public static IEnumerable InvalidClrEvents() [MemberData(nameof(InvalidClrEvents))] public void UnknownClrEvents_Throws(string[] providersArg, string clreventsArg, string clreventLevel, string[] profiles, Type expectedException) { - Assert.Throws(expectedException, () => ProviderUtils.ToProviders(providersArg, clreventsArg, clreventLevel, profiles, false)); + Assert.Throws(expectedException, () => ProviderUtils.ComputeProviderConfig(providersArg, clreventsArg, clreventLevel, profiles)); } public record ProviderSourceExpectation(string Name, bool FromProviders, bool FromClrEvents, bool FromProfile); @@ -251,7 +251,7 @@ public static IEnumerable ProviderSourcePrintCases() new[]{ "MyProvider:0x1:Error" }, "gc", "Informational", - new[]{ "dotnet-common", "dotnet-sampled-thread-time" }, + new[]{ "dotnet-sampled-thread-time" }, new[]{ new ProviderSourceExpectation("MyProvider", true, false, false), new ProviderSourceExpectation("Microsoft-Windows-DotNETRuntime", false, true, false), @@ -269,7 +269,7 @@ public void PrintProviders_Sources(string[] providersArg, string clreventsArg, s try { Console.SetOut(capture); - _ = ProviderUtils.ToProviders(providersArg, clreventsArg, clreventLevel, profiles, true); + _ = ProviderUtils.ComputeProviderConfig(providersArg, clreventsArg, clreventLevel, profiles, true); string output = capture.ToString(); foreach (ProviderSourceExpectation e in expectations) { @@ -295,7 +295,7 @@ public static IEnumerable MergingCases() [MemberData(nameof(MergingCases))] public void MergeDuplicateProviders(string[] providersArg, string clreventsArg, string clreventLevel, string[] profiles, EventPipeProvider expected) { - List actual = ProviderUtils.ToProviders(providersArg, clreventsArg, clreventLevel, profiles, false); + List actual = ProviderUtils.ComputeProviderConfig(providersArg, clreventsArg, clreventLevel, profiles); EventPipeProvider single = Assert.Single(actual); Assert.Equal(expected, single); } @@ -305,7 +305,7 @@ public void MergeDuplicateProviders(string[] providersArg, string clreventsArg, public void ProviderEventLevel_Clamps(string providersArg, EventLevel expected) { string[] providers = providersArg.Split(',', StringSplitOptions.RemoveEmptyEntries); - EventPipeProvider actual = Assert.Single(ProviderUtils.ToProviders(providers, string.Empty, string.Empty, Array.Empty(), false)); + EventPipeProvider actual = Assert.Single(ProviderUtils.ComputeProviderConfig(providers, string.Empty, string.Empty, Array.Empty())); Assert.Equal(expected, actual.EventLevel); } @@ -318,7 +318,7 @@ public static IEnumerable ClrEventLevelCases() [MemberData(nameof(ClrEventLevelCases))] public void CLREvents_NumericLevel_Parses(string[] providersArg, string clreventsArg, string clreventLevel, string[] profiles, EventPipeProvider expected) { - List actual = ProviderUtils.ToProviders(providersArg, clreventsArg, clreventLevel, profiles, false); + List actual = ProviderUtils.ComputeProviderConfig(providersArg, clreventsArg, clreventLevel, profiles); EventPipeProvider single = Assert.Single(actual, p => p.Name == "Microsoft-Windows-DotNETRuntime"); Assert.Equal(expected, single); } From 590a2035cfcf1f122d25e62baad1b4d28d320be5 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Fri, 19 Sep 2025 19:41:57 +0000 Subject: [PATCH 14/25] [DotnetTrace] Print profile effects and clrevents ignore warning --- .../CommandLine/Commands/CollectCommand.cs | 45 +++++++------------ src/Tools/dotnet-trace/ProviderUtils.cs | 16 ++++++- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index 1f5c69641c..29e7adf713 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -114,39 +114,31 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur profile = new[] { "dotnet-common", "dotnet-sampled-thread-time" }; } + List providerCollection = ProviderUtils.ComputeProviderConfig(providers, clrevents, clreventlevel, profile, !IsQuiet, "collect"); + if (providerCollection.Count <= 0) + { + Console.Error.WriteLine("No providers were specified to start a trace."); + return (int)ReturnCode.ArgumentError; + } + long rundownKeyword = 0; RetryStrategy retryStrategy = RetryStrategy.NothingToRetry; - - if (profile.Length != 0) + foreach (string prof in profile) { - foreach (string prof in profile) - { - Profile selectedProfile = ListProfilesCommandHandler.TraceProfiles - .FirstOrDefault(p => p.Name.Equals(prof, StringComparison.OrdinalIgnoreCase)); - if (selectedProfile == null) - { - Console.Error.WriteLine($"Invalid profile name: {prof}"); - return (int)ReturnCode.ArgumentError; - } - if (!string.IsNullOrEmpty(selectedProfile.VerbExclusivity) && !string.Equals(selectedProfile.VerbExclusivity, "collect", StringComparison.OrdinalIgnoreCase)) - { - Console.Error.WriteLine($"The specified profile '{selectedProfile.Name}' does not apply to `dotnet-trace collect`."); - return (int)ReturnCode.ArgumentError; - } + // 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) - { - retryStrategy = selectedProfile.RetryStrategy; - } + rundownKeyword |= selectedProfile.RundownKeyword; + if (selectedProfile.RetryStrategy > retryStrategy) + { + retryStrategy = selectedProfile.RetryStrategy; } } - if (rundownKeyword == 0) { rundownKeyword = EventPipeSession.DefaultRundownKeyword; } - if (rundown.HasValue) { if (rundown.Value) @@ -161,13 +153,6 @@ private static async Task Collect(CancellationToken ct, CommandLineConfigur } } - List providerCollection = ProviderUtils.ComputeProviderConfig(providers, clrevents, clreventlevel, profile, !IsQuiet, "collect"); - if (providerCollection.Count <= 0) - { - Console.Error.WriteLine("No providers were specified to start a trace."); - return (int)ReturnCode.ArgumentError; - } - // Validate and parse stoppingEvent parameters: stoppingEventProviderName, stoppingEventEventName, stoppingEventPayloadFilter bool hasStoppingEventProviderName = !string.IsNullOrEmpty(stoppingEventProviderName); diff --git a/src/Tools/dotnet-trace/ProviderUtils.cs b/src/Tools/dotnet-trace/ProviderUtils.cs index 752fbf7046..762f7e607a 100644 --- a/src/Tools/dotnet-trace/ProviderUtils.cs +++ b/src/Tools/dotnet-trace/ProviderUtils.cs @@ -107,6 +107,17 @@ public static List ComputeProviderConfig(string[] providersAr throw new ArgumentException($"The specified profile '{traceProfile.Name}' does not apply to `dotnet-trace {verbExclusivity}`."); } + if (shouldPrintProviders) + { + string profileEffect = string.Join(", ", traceProfile.Providers.Select(p => p.ToString())); + if (!string.IsNullOrEmpty(traceProfile.VerbExclusivity) && + traceProfile.VerbExclusivity.Equals("collect-linux", StringComparison.OrdinalIgnoreCase)) + { + profileEffect = traceProfile.CollectLinuxArgs; + } + Console.WriteLine($"Applying profile '{traceProfile.Name}': {profileEffect}"); + } + IEnumerable profileProviders = traceProfile.Providers; foreach (EventPipeProvider provider in profileProviders) { @@ -128,7 +139,10 @@ public static List ComputeProviderConfig(string[] providersAr merged[provider.Name] = provider; providerSources[provider.Name] = (int)ProviderSource.CLREventsArg; } - // Prefer providers set through --providers or --profile over --clrevents + else if (shouldPrintProviders) + { + Console.WriteLine($"Warning: The CLR provider was already specified through --providers or --profile. Ignoring --clrevents."); + } } } From 8e2453f088c325c532d7bd5e697afb840993a1db Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Fri, 19 Sep 2025 22:20:29 +0000 Subject: [PATCH 15/25] [DotnetTrace] Print PerfEvents and include in default condition --- .../CommandLine/Commands/CollectLinuxCommand.cs | 10 +++------- src/Tools/dotnet-trace/ProviderUtils.cs | 6 ++++++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index 056d09307f..ff6f38dee4 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -151,9 +151,9 @@ private static List BuildRecordTraceArgs(CollectLinuxArgs args, out stri } string[] profiles = args.Profiles; - if (args.Profiles.Length == 0 && args.Providers.Length == 0 && string.IsNullOrEmpty(args.ClrEvents)) + if (args.Profiles.Length == 0 && args.Providers.Length == 0 && string.IsNullOrEmpty(args.ClrEvents) && args.PerfEvents.Length == 0) { - Console.WriteLine("No profile or providers specified, defaulting to trace profiles 'dotnet-common' + 'cpu-sampling'."); + Console.WriteLine("No providers, profiles, ClrEvents, or PerfEvents were specified, defaulting to trace profiles 'dotnet-common' + 'cpu-sampling'."); profiles = new[] { "dotnet-common", "cpu-sampling" }; } @@ -195,11 +195,6 @@ private static List BuildRecordTraceArgs(CollectLinuxArgs args, out stri foreach (string perfEvent in args.PerfEvents) { - if (string.IsNullOrWhiteSpace(perfEvent) || !perfEvent.Contains(':', StringComparison.Ordinal)) - { - throw new ArgumentException($"Invalid perf event specification '{perfEvent}'. Expected format 'provider:event'."); - } - string[] split = perfEvent.Split(':', 2, StringSplitOptions.TrimEntries); if (split.Length != 2 || string.IsNullOrEmpty(split[0]) || string.IsNullOrEmpty(split[1])) { @@ -208,6 +203,7 @@ private static List BuildRecordTraceArgs(CollectLinuxArgs args, out stri string perfProvider = split[0]; string perfEventName = split[1]; + Console.WriteLine($"Enabling perf event '{perfEvent}'"); scriptBuilder.Append($"let {perfEventName} = event_from_tracefs(\"{perfProvider}\", \"{perfEventName}\");\nrecord_event({perfEventName});\n\n"); } diff --git a/src/Tools/dotnet-trace/ProviderUtils.cs b/src/Tools/dotnet-trace/ProviderUtils.cs index 762f7e607a..d9c31cbc82 100644 --- a/src/Tools/dotnet-trace/ProviderUtils.cs +++ b/src/Tools/dotnet-trace/ProviderUtils.cs @@ -173,6 +173,12 @@ private static EventPipeProvider MergeProviderConfigs(EventPipeProvider provider private static void PrintProviders(IReadOnlyList providers, Dictionary enabledBy) { + if (providers.Count == 0) + { + Console.WriteLine("No providers were configured."); + return; + } + Console.WriteLine(""); Console.WriteLine(string.Format("{0, -40}", "Provider Name") + string.Format("{0, -20}", "Keywords") + string.Format("{0, -20}", "Level") + "Enabled By"); // +4 is for the tab From 573d7690b3b066b953ef928888db78379460c072 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Tue, 30 Sep 2025 16:14:05 +0000 Subject: [PATCH 16/25] [DotnetTrace] Update OneCollect package with FFI --- src/Tools/dotnet-trace/dotnet-trace.csproj | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/Tools/dotnet-trace/dotnet-trace.csproj b/src/Tools/dotnet-trace/dotnet-trace.csproj index 64b755ca50..0a303ce663 100644 --- a/src/Tools/dotnet-trace/dotnet-trace.csproj +++ b/src/Tools/dotnet-trace/dotnet-trace.csproj @@ -14,7 +14,8 @@ - + @@ -30,18 +31,8 @@ - - - <_RecordTraceFromPackage>$(PkgMicrosoft_OneCollect_RecordTrace)/runtimes/$(RuntimeIdentifier)/native/librecordtrace.so - - - - <_RecordTraceResolved Include="$(_RecordTraceLocal)" Condition="'$(RuntimeIdentifier)' != '' AND Exists('$(_RecordTraceLocal)')" /> - <_RecordTraceResolved Include="$(_RecordTraceFromPackage)" Condition="'@(_RecordTraceResolved)' == '' AND '$(RuntimeIdentifier)' != '' AND Exists('$(_RecordTraceFromPackage)')" /> - - - - - - + + + From 1b3b583f6df811ef344eea7f177955b7445af321 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Fri, 3 Oct 2025 18:49:14 +0000 Subject: [PATCH 17/25] Fix dotnet-trace build for repo root build --- src/Tools/dotnet-trace/dotnet-trace.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Tools/dotnet-trace/dotnet-trace.csproj b/src/Tools/dotnet-trace/dotnet-trace.csproj index 0a303ce663..c7ce23bb72 100644 --- a/src/Tools/dotnet-trace/dotnet-trace.csproj +++ b/src/Tools/dotnet-trace/dotnet-trace.csproj @@ -15,7 +15,7 @@ + Condition="'$(TargetRid)' != '' and $([System.String]::Copy('$(TargetRid)').StartsWith('linux'))" /> @@ -31,8 +31,8 @@ - - + + From e6d9de914156a407af7e3b8c12c0dccc170ba96f Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Mon, 6 Oct 2025 17:16:17 +0000 Subject: [PATCH 18/25] Add Linux events table --- .../Commands/CollectLinuxCommand.cs | 30 ++++++++++--------- src/Tools/dotnet-trace/ProviderUtils.cs | 14 ++------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index ff6f38dee4..20081b9262 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -157,20 +157,7 @@ private static List BuildRecordTraceArgs(CollectLinuxArgs args, out stri profiles = new[] { "dotnet-common", "cpu-sampling" }; } - foreach (string profile in profiles) - { - Profile traceProfile = ListProfilesCommandHandler.TraceProfiles - .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); - - if (!string.IsNullOrEmpty(traceProfile.VerbExclusivity) && - traceProfile.VerbExclusivity.Equals("collect-linux", StringComparison.OrdinalIgnoreCase)) - { - recordTraceArgs.Add(traceProfile.CollectLinuxArgs); - } - } - StringBuilder scriptBuilder = new(); - List providerCollection = ProviderUtils.ComputeProviderConfig(args.Providers, args.ClrEvents, args.ClrEventLevel, profiles, true, "collect-linux"); foreach (EventPipeProvider provider in providerCollection) { @@ -193,6 +180,20 @@ private static List BuildRecordTraceArgs(CollectLinuxArgs args, out stri scriptBuilder.Append($"record_dotnet_provider(\"{providerName}\", 0x{keywords:X}, {eventLevel}, {providerNameSanitized}_flags);\n\n"); } + Console.WriteLine($"{("Linux Events"),-80}Enabled By"); + foreach (string profile in profiles) + { + Profile traceProfile = ListProfilesCommandHandler.TraceProfiles + .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); + + if (!string.IsNullOrEmpty(traceProfile.VerbExclusivity) && + traceProfile.VerbExclusivity.Equals("collect-linux", StringComparison.OrdinalIgnoreCase)) + { + recordTraceArgs.Add(traceProfile.CollectLinuxArgs); + Console.WriteLine($"{traceProfile.Name,-80}--profile"); + } + } + foreach (string perfEvent in args.PerfEvents) { string[] split = perfEvent.Split(':', 2, StringSplitOptions.TrimEntries); @@ -203,9 +204,10 @@ private static List BuildRecordTraceArgs(CollectLinuxArgs args, out stri string perfProvider = split[0]; string perfEventName = split[1]; - Console.WriteLine($"Enabling perf event '{perfEvent}'"); + Console.WriteLine($"{perfEvent,-80}--perf-events"); scriptBuilder.Append($"let {perfEventName} = event_from_tracefs(\"{perfProvider}\", \"{perfEventName}\");\nrecord_event({perfEventName});\n\n"); } + Console.WriteLine(); string scriptText = scriptBuilder.ToString(); string scriptFileName = $"{Path.GetFileNameWithoutExtension(resolvedOutput)}.script"; diff --git a/src/Tools/dotnet-trace/ProviderUtils.cs b/src/Tools/dotnet-trace/ProviderUtils.cs index d9c31cbc82..3bfc11a179 100644 --- a/src/Tools/dotnet-trace/ProviderUtils.cs +++ b/src/Tools/dotnet-trace/ProviderUtils.cs @@ -107,17 +107,6 @@ public static List ComputeProviderConfig(string[] providersAr throw new ArgumentException($"The specified profile '{traceProfile.Name}' does not apply to `dotnet-trace {verbExclusivity}`."); } - if (shouldPrintProviders) - { - string profileEffect = string.Join(", ", traceProfile.Providers.Select(p => p.ToString())); - if (!string.IsNullOrEmpty(traceProfile.VerbExclusivity) && - traceProfile.VerbExclusivity.Equals("collect-linux", StringComparison.OrdinalIgnoreCase)) - { - profileEffect = traceProfile.CollectLinuxArgs; - } - Console.WriteLine($"Applying profile '{traceProfile.Name}': {profileEffect}"); - } - IEnumerable profileProviders = traceProfile.Providers; foreach (EventPipeProvider provider in profileProviders) { @@ -175,7 +164,8 @@ private static void PrintProviders(IReadOnlyList providers, D { if (providers.Count == 0) { - Console.WriteLine("No providers were configured."); + Console.WriteLine("No .NET providers were configured."); + Console.WriteLine(""); return; } From 676efa97a10708fa604ebdab341c64ca0b883fc8 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Mon, 6 Oct 2025 17:16:41 +0000 Subject: [PATCH 19/25] Add Progress status and Address feedback --- .../Commands/CollectLinuxCommand.cs | 144 ++++++++++-------- .../Commands/ListProfilesCommandHandler.cs | 10 +- 2 files changed, 79 insertions(+), 75 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index 20081b9262..b4d2ee4740 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Internal.Common.Utils; @@ -16,9 +17,13 @@ namespace Microsoft.Diagnostics.Tools.Trace { internal static partial class CollectLinuxCommandHandler { - private static int s_recordStatus; + private static bool s_stopTracing; + private static Stopwatch s_stopwatch = new(); + private static LineRewriter s_rewriter = new() { LineToClear = Console.CursorTop - 1 }; + private static bool s_printingStatus; internal sealed record CollectLinuxArgs( + CancellationToken Ct, string[] Providers, string ClrEventLevel, string ClrEvents, @@ -47,7 +52,42 @@ private static int CollectLinux(CollectLinuxArgs args) return (int)ReturnCode.ArgumentError; } - return RunRecordTrace(args); + 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 = RunRecordTrace(command, (UIntPtr)command.Length, OutputHandler); + } + finally + { + if (!string.IsNullOrEmpty(scriptPath)) + { + try + { + if (File.Exists(scriptPath)) + { + File.Delete(scriptPath); + } + } catch { } + } + } + + return ret; } public static Command CollectLinuxCommand() @@ -73,6 +113,7 @@ public static Command CollectLinuxCommand() string profilesValue = parseResult.GetValue(CommonOptions.ProfileOption) ?? string.Empty; int rc = 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, @@ -88,44 +129,7 @@ public static Command CollectLinuxCommand() return collectLinuxCommand; } - private static int RunRecordTrace(CollectLinuxArgs args) - { - s_recordStatus = 0; - - ConsoleCancelEventHandler handler = (sender, e) => - { - e.Cancel = true; - s_recordStatus = 1; - }; - Console.CancelKeyPress += handler; - - IEnumerable recordTraceArgList = BuildRecordTraceArgs(args, out string scriptPath); - - string options = string.Join(' ', recordTraceArgList); - byte[] command = Encoding.UTF8.GetBytes(options); - int rc; - try - { - rc = RecordTrace(command, (UIntPtr)command.Length, OutputHandler); - } - finally - { - Console.CancelKeyPress -= handler; - if (!string.IsNullOrEmpty(scriptPath)) - { - try { - if (File.Exists(scriptPath)) - { - File.Delete(scriptPath); - } - } catch { } - } - } - - return rc; - } - - private static List BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath) + private static byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath) { scriptPath = null; List recordTraceArgs = new(); @@ -144,12 +148,6 @@ private static List BuildRecordTraceArgs(CollectLinuxArgs args, out stri recordTraceArgs.Add($"--out"); recordTraceArgs.Add(resolvedOutput); - if (args.Duration != default) - { - recordTraceArgs.Add($"--duration"); - recordTraceArgs.Add(args.Duration.ToString()); - } - string[] profiles = args.Profiles; if (args.Profiles.Length == 0 && args.Providers.Length == 0 && string.IsNullOrEmpty(args.ClrEvents) && args.PerfEvents.Length == 0) { @@ -217,7 +215,8 @@ private static List BuildRecordTraceArgs(CollectLinuxArgs args, out stri recordTraceArgs.Add("--script-file"); recordTraceArgs.Add(scriptPath); - return recordTraceArgs; + string options = string.Join(' ', recordTraceArgs); + return Encoding.UTF8.GetBytes(options); } private static string ResolveOutputPath(FileInfo output, int processId) @@ -241,31 +240,44 @@ private static string ResolveOutputPath(FileInfo output, int processId) private static int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) { OutputType ot = (OutputType)type; - if (ot != OutputType.Progress) + if (dataLen != UIntPtr.Zero && (ulong)dataLen <= int.MaxValue) { - int len = checked((int)dataLen); - if (len > 0) + string text = Marshal.PtrToStringUTF8(data, (int)dataLen); + if (!string.IsNullOrEmpty(text) && + !text.StartsWith("Recording started", StringComparison.OrdinalIgnoreCase)) { - byte[] buffer = new byte[len]; - Marshal.Copy(data, buffer, 0, len); - string text = Encoding.UTF8.GetString(buffer); - switch (ot) + if (ot == OutputType.Error) { - case OutputType.Normal: - case OutputType.Live: - Console.Out.WriteLine(text); - break; - case OutputType.Error: Console.Error.WriteLine(text); - break; - default: - Console.Error.WriteLine($"[{ot}] {text}"); - break; + s_stopTracing = true; + } + else + { + Console.Out.WriteLine(text); } } } - return s_recordStatus; + 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 = @@ -288,8 +300,8 @@ private delegate int recordTraceCallback( [In] IntPtr data, [In] UIntPtr dataLen); - [LibraryImport("recordtrace")] - private static partial int RecordTrace( + [LibraryImport("recordtrace", EntryPoint = "RecordTrace")] + private static partial int RunRecordTrace( byte[] command, UIntPtr commandLen, recordTraceCallback callback); diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs b/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs index 58b86ede6e..432e6599ae 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs @@ -25,15 +25,7 @@ internal sealed class ListProfilesCommandHandler private static string dotnetCommonDescription = """ Lightweight .NET runtime diagnostics designed to stay low overhead. - Includes: - GC - AssemblyLoader - Loader - JIT - Exceptions - Threading - JittedMethodILToNativeMap - Compilation + Includes GC, AssemblyLoader, Loader, JIT, Exceptions, Threading, JittedMethodILToNativeMap, and Compilation events Equivalent to --providers "Microsoft-Windows-DotNETRuntime:0x100003801D:4". """; From 096f325a5a718fa1adf214b33ab3915a15185b80 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Mon, 6 Oct 2025 21:26:18 +0000 Subject: [PATCH 20/25] Update collect functional tests for ProviderUtils refactor --- .../CommandLine/Commands/CollectCommand.cs | 2 +- src/Tools/dotnet-trace/ProviderUtils.cs | 29 ++++++------ .../CollectCommandFunctionalTests.cs | 44 +++++++++---------- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index 57804a5c4c..405b495cc9 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -123,7 +123,7 @@ internal async Task Collect(CancellationToken ct, CommandLineConfiguration profile = new[] { "dotnet-common", "dotnet-sampled-thread-time" }; } - List providerCollection = ProviderUtils.ComputeProviderConfig(providers, clrevents, clreventlevel, profile, !IsQuiet, "collect"); + List providerCollection = ProviderUtils.ComputeProviderConfig(providers, clrevents, clreventlevel, profile, !IsQuiet, "collect", Console); if (providerCollection.Count <= 0) { Console.Error.WriteLine("No providers were specified to start a trace."); diff --git a/src/Tools/dotnet-trace/ProviderUtils.cs b/src/Tools/dotnet-trace/ProviderUtils.cs index 3bfc11a179..ed43ba4148 100644 --- a/src/Tools/dotnet-trace/ProviderUtils.cs +++ b/src/Tools/dotnet-trace/ProviderUtils.cs @@ -5,9 +5,11 @@ 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 { @@ -71,14 +73,15 @@ private enum ProviderSource ProfileArg = 4, } - public static List ComputeProviderConfig(string[] providersArg, string clreventsArg, string clreventlevel, string[] profiles, bool shouldPrintProviders = false, string verbExclusivity = null) + 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); + EventPipeProvider provider = ToProvider(providerArg, console); if (!merged.TryGetValue(provider.Name, out EventPipeProvider existing)) { merged[provider.Name] = provider; @@ -130,7 +133,7 @@ public static List ComputeProviderConfig(string[] providersAr } else if (shouldPrintProviders) { - Console.WriteLine($"Warning: The CLR provider was already specified through --providers or --profile. Ignoring --clrevents."); + console.WriteLine($"Warning: The CLR provider was already specified through --providers or --profile. Ignoring --clrevents."); } } } @@ -138,7 +141,7 @@ public static List ComputeProviderConfig(string[] providersAr List unifiedProviders = merged.Values.ToList(); if (shouldPrintProviders) { - PrintProviders(unifiedProviders, providerSources); + PrintProviders(unifiedProviders, providerSources, console); } return unifiedProviders; @@ -160,17 +163,17 @@ private static EventPipeProvider MergeProviderConfigs(EventPipeProvider provider return new EventPipeProvider(providerConfigA.Name, level, providerConfigA.Keywords | providerConfigB.Keywords, providerConfigA.Arguments ?? providerConfigB.Arguments); } - private static void PrintProviders(IReadOnlyList providers, Dictionary enabledBy) + private static void PrintProviders(IReadOnlyList providers, Dictionary enabledBy, IConsole console) { if (providers.Count == 0) { - Console.WriteLine("No .NET providers were configured."); - Console.WriteLine(""); + console.WriteLine("No .NET providers were configured."); + console.WriteLine(""); return; } - Console.WriteLine(""); - Console.WriteLine(string.Format("{0, -40}", "Provider Name") + string.Format("{0, -20}", "Keywords") + + 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) { @@ -190,9 +193,9 @@ private static void PrintProviders(IReadOnlyList providers, D providerSources.Add("--profile"); } } - Console.WriteLine(string.Format("{0, -80}", $"{GetProviderDisplayString(provider)}") + string.Join(", ", providerSources)); + console.WriteLine(string.Format("{0, -80}", $"{GetProviderDisplayString(provider)}") + string.Join(", ", providerSources)); } - Console.WriteLine(""); + 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})"); @@ -257,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)) { @@ -272,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/tests/dotnet-trace/CollectCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectCommandFunctionalTests.cs index f6de43dfed..463c8ff676 100644 --- a/src/tests/dotnet-trace/CollectCommandFunctionalTests.cs +++ b/src/tests/dotnet-trace/CollectCommandFunctionalTests.cs @@ -23,8 +23,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 = "", @@ -72,8 +72,8 @@ private static async Task RunAsync(CollectArgs config, MockConsole con config.ProcessId, config.Output, config.buffersize, - config.providers, - config.profile, + config.providers ?? Array.Empty(), + config.profile ?? Array.Empty(), config.Format, config.duration, config.clrevents, @@ -130,22 +130,22 @@ public static IEnumerable BasicCases() 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 +153,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 +190,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")) }; From 1f75125a96217b89b5feaf5e7ffa3f97e9c297c1 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Mon, 6 Oct 2025 22:44:15 +0000 Subject: [PATCH 21/25] [DotnetTrace] Add Collect Linux Functional Tests --- .../Commands/CollectLinuxCommand.cs | 31 ++- .../CollectLinuxCommandFunctionalTests.cs | 200 ++++++++++++++++++ 2 files changed, 222 insertions(+), 9 deletions(-) create mode 100644 src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index b4d2ee4740..795865717e 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -11,15 +11,16 @@ 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 static partial class CollectLinuxCommandHandler + internal partial class CollectLinuxCommandHandler { private static bool s_stopTracing; private static Stopwatch s_stopwatch = new(); - private static LineRewriter s_rewriter = new() { LineToClear = Console.CursorTop - 1 }; + private static LineRewriter s_rewriter; private static bool s_printingStatus; internal sealed record CollectLinuxArgs( @@ -34,11 +35,17 @@ internal sealed record CollectLinuxArgs( string Name, int ProcessId); + public CollectLinuxCommandHandler() + { + 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. /// - private static int CollectLinux(CollectLinuxArgs args) + internal int CollectLinux(CollectLinuxArgs args) { if (!OperatingSystem.IsLinux()) { @@ -71,7 +78,7 @@ private static int CollectLinux(CollectLinuxArgs args) durationTimer.Start(); } s_stopwatch.Start(); - ret = RunRecordTrace(command, (UIntPtr)command.Length, OutputHandler); + ret = RecordTraceInvoker(command, (UIntPtr)command.Length, OutputHandler); } finally { @@ -111,8 +118,9 @@ public static Command CollectLinuxCommand() 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 = CollectLinux(new CollectLinuxArgs( + int rc = handler.CollectLinux(new CollectLinuxArgs( Ct: ct, Providers: providersValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), ClrEventLevel: parseResult.GetValue(CommonOptions.CLREventLevelOption) ?? string.Empty, @@ -129,7 +137,7 @@ public static Command CollectLinuxCommand() return collectLinuxCommand; } - private static byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath) + private byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath) { scriptPath = null; List recordTraceArgs = new(); @@ -156,7 +164,7 @@ private static byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scr } StringBuilder scriptBuilder = new(); - List providerCollection = ProviderUtils.ComputeProviderConfig(args.Providers, args.ClrEvents, args.ClrEventLevel, profiles, true, "collect-linux"); + List providerCollection = ProviderUtils.ComputeProviderConfig(args.Providers, args.ClrEvents, args.ClrEventLevel, profiles, true, "collect-linux", Console); foreach (EventPipeProvider provider in providerCollection) { string providerName = provider.Name; @@ -237,7 +245,7 @@ private static string ResolveOutputPath(FileInfo output, int processId) return $"trace_{now:yyyyMMdd}_{now:HHmmss}.nettrace"; } - private static int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) + private int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) { OutputType ot = (OutputType)type; if (dataLen != UIntPtr.Zero && (ulong)dataLen <= int.MaxValue) @@ -295,7 +303,7 @@ private enum OutputType : uint } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate int recordTraceCallback( + internal delegate int recordTraceCallback( [In] uint type, [In] IntPtr data, [In] UIntPtr dataLen); @@ -305,5 +313,10 @@ 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/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs new file mode 100644 index 0000000000..2971fafae8 --- /dev/null +++ b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs @@ -0,0 +1,200 @@ +// 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 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[] profiles = null, + FileInfo output = null, + TimeSpan duration = default, + string name = "", + int processId = -1) + { + return new CollectLinuxCommandHandler.CollectLinuxArgs(ct, + providers ?? Array.Empty(), + clrEventLevel, + clrEvents, + perfEvents ?? Array.Empty(), + profiles ?? Array.Empty(), + output ?? new FileInfo(CommonOptions.DefaultTraceName), + duration, + name, + processId); + } + + [Theory] + [MemberData(nameof(BasicCases))] + public void CollectLinuxCommandProviderConfigurationConsolidation(object testArgs, string[] expectedLines) + { + MockConsole console = new(200, 30); + RunAsync((CollectLinuxCommandHandler.CollectLinuxArgs)testArgs, console); + console.AssertSanitizedLinesEqual(null, expectedLines); + } + + private static string[] RunAsync(CollectLinuxCommandHandler.CollectLinuxArgs args, MockConsole console) + { + var handler = new CollectLinuxCommandHandler(); + handler.Console = console; + handler.RecordTraceInvoker = (cmd, len, cb) => 0; + int exit = handler.CollectLinux(args); + if (exit != 0) + { + throw new InvalidOperationException($"Collect exited with return code {exit}."); + } + return console.Lines; + } + + public static IEnumerable BasicCases() + { + yield return new object[] { + TestArgs(), + new string[] { + "No providers, profiles, ClrEvents, or PerfEvents were specified, defaulting to trace profiles 'dotnet-common' + 'cpu-sampling'.", + "", + ProviderHeader, + FormatProvider("Microsoft-Windows-DotNETRuntime","000000100003801D","Informational",4,"--profile"), + "", + LinuxHeader, + LinuxProfile("cpu-sampling"), + "" + } + }; + yield return new object[] { + TestArgs(providers: new[]{"Foo:0x1:4"}), + new string[] { + "", ProviderHeader, + FormatProvider("Foo","0000000000000001","Informational",4,"--providers"), + "", + LinuxHeader, + "" + } + }; + yield return new object[] { + TestArgs(providers: new[]{"Foo:0x1:4","Bar:0x2:4"}), + new string[] { + "", ProviderHeader, + FormatProvider("Foo","0000000000000001","Informational",4,"--providers"), + FormatProvider("Bar","0000000000000002","Informational",4,"--providers"), + "", + LinuxHeader, + "" + } + }; + yield return new object[] { + TestArgs(profiles: new[]{"cpu-sampling"}), + new string[] { + "No .NET providers were configured.", + "", + LinuxHeader, + LinuxProfile("cpu-sampling"), + "" + } + }; + yield return new object[] { + TestArgs(providers: new[]{"Foo:0x1:4"}, profiles: new[]{"cpu-sampling"}), + new string[] { + "", ProviderHeader, + FormatProvider("Foo","0000000000000001","Informational",4,"--providers"), + "", + LinuxHeader, + LinuxProfile("cpu-sampling"), + "" + } + }; + yield return new object[] { + TestArgs(clrEvents: "gc", profiles: new[]{"cpu-sampling"}), + new string[] { + "", ProviderHeader, + FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--clrevents"), + "", + LinuxHeader, + LinuxProfile("cpu-sampling"), + "" + } + }; + yield return new object[] { + TestArgs(providers: new[]{"Microsoft-Windows-DotNETRuntime:0x1:4"}, profiles: new[]{"cpu-sampling"}), + new string[] { + "", ProviderHeader, + FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--providers"), + "", + LinuxHeader, + LinuxProfile("cpu-sampling"), + "" + } + }; + yield return new object[] { + TestArgs(providers: new[]{"Microsoft-Windows-DotNETRuntime:0x1:4"}, clrEvents: "gc"), + new string[] { + "Warning: The CLR provider was already specified through --providers or --profile. Ignoring --clrevents.", + "", ProviderHeader, + FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--providers"), + "", + LinuxHeader, + "" + } + }; + yield return new object[] { + TestArgs(clrEvents: "gc+jit"), + new string[] { + "", ProviderHeader, + FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000011","Informational",4,"--clrevents"), + "", + LinuxHeader, + "" + } + }; + yield return new object[] { + TestArgs(clrEvents: "gc+jit", clrEventLevel: "5"), + new string[] { + "", ProviderHeader, + FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000011","Verbose",5,"--clrevents"), + "", + LinuxHeader, + "" + } + }; + yield return new object[] { + TestArgs(perfEvents: new[]{"sched:sched_switch"}), + new string[] { + "No .NET providers were configured.", + "", + LinuxHeader, + LinuxPerfEvent("sched:sched_switch"), + "" + } + }; + } + + private const string ProviderHeader = "Provider Name Keywords Level Enabled By"; + private static string LinuxHeader => $"{"Linux 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; + } + } +} From 55bc84a7f66a866701e0ff42c42762687d1e7274 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 7 Oct 2025 10:44:38 -0400 Subject: [PATCH 22/25] Adjust CollectLinux tests for non-Linux platforms --- .../Commands/CollectLinuxCommand.cs | 4 ++-- .../CollectLinuxCommandFunctionalTests.cs | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index 795865717e..e2bb2b1cca 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -35,9 +35,9 @@ internal sealed record CollectLinuxArgs( string Name, int ProcessId); - public CollectLinuxCommandHandler() + public CollectLinuxCommandHandler(IConsole console = null) { - Console = new DefaultConsole(false); + Console = console ?? new DefaultConsole(false); s_rewriter = new LineRewriter(Console) { LineToClear = Console.CursorTop - 1 }; } diff --git a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs index 2971fafae8..0231a19e02 100644 --- a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs +++ b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs @@ -46,21 +46,21 @@ private static CollectLinuxCommandHandler.CollectLinuxArgs TestArgs( public void CollectLinuxCommandProviderConfigurationConsolidation(object testArgs, string[] expectedLines) { MockConsole console = new(200, 30); - RunAsync((CollectLinuxCommandHandler.CollectLinuxArgs)testArgs, console); - console.AssertSanitizedLinesEqual(null, expectedLines); - } - - private static string[] RunAsync(CollectLinuxCommandHandler.CollectLinuxArgs args, MockConsole console) - { - var handler = new CollectLinuxCommandHandler(); - handler.Console = console; + var handler = new CollectLinuxCommandHandler(console); handler.RecordTraceInvoker = (cmd, len, cb) => 0; - int exit = handler.CollectLinux(args); - if (exit != 0) + int exit = handler.CollectLinux((CollectLinuxCommandHandler.CollectLinuxArgs)testArgs); + if (OperatingSystem.IsLinux()) + { + Assert.Equal(0, exit); + console.AssertSanitizedLinesEqual(null, expectedLines); + } + else { - throw new InvalidOperationException($"Collect exited with return code {exit}."); + Assert.Equal(3, exit); + console.AssertSanitizedLinesEqual(null, new string[] { + "The collect-linux command is only supported on Linux.", + }); } - return console.Lines; } public static IEnumerable BasicCases() From 4c1a36106aaa05751ca727d9c9f70ee658c2b7ae Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Tue, 7 Oct 2025 15:03:08 +0000 Subject: [PATCH 23/25] [DotnetTrace][CollectLinux] Remove process specifier Record-Trace has a bug for multithreaded .NET apps when specifying a process to trace. https://github.com/microsoft/one-collect/issues/192 Until that is resolved, the scenario is broken. --- .../Commands/CollectLinuxCommand.cs | 37 ++----------------- .../CollectLinuxCommandFunctionalTests.cs | 32 +++++++++------- 2 files changed, 22 insertions(+), 47 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index e2bb2b1cca..d04e85cf03 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -31,9 +31,7 @@ internal sealed record CollectLinuxArgs( string[] PerfEvents, string[] Profiles, FileInfo Output, - TimeSpan Duration, - string Name, - int ProcessId); + TimeSpan Duration); public CollectLinuxCommandHandler(IConsole console = null) { @@ -53,12 +51,6 @@ internal int CollectLinux(CollectLinuxArgs args) return (int)ReturnCode.ArgumentError; } - if (args.ProcessId != 0 && !string.IsNullOrEmpty(args.Name)) - { - Console.Error.WriteLine("Only one of --process-id or --name can be specified."); - return (int)ReturnCode.ArgumentError; - } - args.Ct.Register(() => s_stopTracing = true); int ret = (int)ReturnCode.TracingError; string scriptPath = null; @@ -108,8 +100,6 @@ public static Command CollectLinuxCommand() CommonOptions.ProfileOption, CommonOptions.OutputPathOption, CommonOptions.DurationOption, - CommonOptions.NameOption, - CommonOptions.ProcessIdOption }; collectLinuxCommand.TreatUnmatchedTokensAsErrors = true; // collect-linux currently does not support child process tracing. collectLinuxCommand.Description = "Collects diagnostic traces using perf_events, a Linux OS technology. collect-linux requires admin privileges to capture kernel- and user-mode events, and by default, captures events from all processes. This Linux-only command includes the same .NET events as dotnet-trace collect, and it uses the kernel’s user_events mechanism to emit .NET events as perf events, enabling unification of user-space .NET events with kernel-space system events."; @@ -128,9 +118,7 @@ public static Command CollectLinuxCommand() PerfEvents: perfEventsValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), Profiles: profilesValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), Output: parseResult.GetValue(CommonOptions.OutputPathOption) ?? new FileInfo(CommonOptions.DefaultTraceName), - Duration: parseResult.GetValue(CommonOptions.DurationOption), - Name: parseResult.GetValue(CommonOptions.NameOption) ?? string.Empty, - ProcessId: parseResult.GetValue(CommonOptions.ProcessIdOption))); + Duration: parseResult.GetValue(CommonOptions.DurationOption))); return Task.FromResult(rc); }); @@ -141,18 +129,8 @@ private byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath { scriptPath = null; List recordTraceArgs = new(); - int pid = args.ProcessId; - if (!string.IsNullOrEmpty(args.Name)) - { - pid = CommandUtils.FindProcessIdWithName(args.Name); - } - if (pid > 0) - { - recordTraceArgs.Add($"--pid"); - recordTraceArgs.Add($"{pid}"); - } - string resolvedOutput = ResolveOutputPath(args.Output, pid); + string resolvedOutput = ResolveOutputPath(args.Output); recordTraceArgs.Add($"--out"); recordTraceArgs.Add(resolvedOutput); @@ -227,7 +205,7 @@ private byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath return Encoding.UTF8.GetBytes(options); } - private static string ResolveOutputPath(FileInfo output, int processId) + private static string ResolveOutputPath(FileInfo output) { if (!string.Equals(output.Name, CommonOptions.DefaultTraceName, StringComparison.OrdinalIgnoreCase)) { @@ -235,13 +213,6 @@ private static string ResolveOutputPath(FileInfo output, int processId) } DateTime now = DateTime.Now; - if (processId > 0) - { - Process process = Process.GetProcessById(processId); - FileInfo processMainModuleFileInfo = new(process.MainModule.FileName); - return $"{processMainModuleFileInfo.Name}_{now:yyyyMMdd}_{now:HHmmss}.nettrace"; - } - return $"trace_{now:yyyyMMdd}_{now:HHmmss}.nettrace"; } diff --git a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs index 0231a19e02..88626e29ce 100644 --- a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs +++ b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs @@ -25,9 +25,7 @@ private static CollectLinuxCommandHandler.CollectLinuxArgs TestArgs( string[] perfEvents = null, string[] profiles = null, FileInfo output = null, - TimeSpan duration = default, - string name = "", - int processId = -1) + TimeSpan duration = default) { return new CollectLinuxCommandHandler.CollectLinuxArgs(ct, providers ?? Array.Empty(), @@ -36,9 +34,7 @@ private static CollectLinuxCommandHandler.CollectLinuxArgs TestArgs( perfEvents ?? Array.Empty(), profiles ?? Array.Empty(), output ?? new FileInfo(CommonOptions.DefaultTraceName), - duration, - name, - processId); + duration); } [Theory] @@ -81,7 +77,8 @@ public static IEnumerable BasicCases() yield return new object[] { TestArgs(providers: new[]{"Foo:0x1:4"}), new string[] { - "", ProviderHeader, + "", + ProviderHeader, FormatProvider("Foo","0000000000000001","Informational",4,"--providers"), "", LinuxHeader, @@ -91,7 +88,8 @@ public static IEnumerable BasicCases() yield return new object[] { TestArgs(providers: new[]{"Foo:0x1:4","Bar:0x2:4"}), new string[] { - "", ProviderHeader, + "", + ProviderHeader, FormatProvider("Foo","0000000000000001","Informational",4,"--providers"), FormatProvider("Bar","0000000000000002","Informational",4,"--providers"), "", @@ -112,7 +110,8 @@ public static IEnumerable BasicCases() yield return new object[] { TestArgs(providers: new[]{"Foo:0x1:4"}, profiles: new[]{"cpu-sampling"}), new string[] { - "", ProviderHeader, + "", + ProviderHeader, FormatProvider("Foo","0000000000000001","Informational",4,"--providers"), "", LinuxHeader, @@ -123,7 +122,8 @@ public static IEnumerable BasicCases() yield return new object[] { TestArgs(clrEvents: "gc", profiles: new[]{"cpu-sampling"}), new string[] { - "", ProviderHeader, + "", + ProviderHeader, FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--clrevents"), "", LinuxHeader, @@ -134,7 +134,8 @@ public static IEnumerable BasicCases() yield return new object[] { TestArgs(providers: new[]{"Microsoft-Windows-DotNETRuntime:0x1:4"}, profiles: new[]{"cpu-sampling"}), new string[] { - "", ProviderHeader, + "", + ProviderHeader, FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--providers"), "", LinuxHeader, @@ -146,7 +147,8 @@ public static IEnumerable BasicCases() TestArgs(providers: new[]{"Microsoft-Windows-DotNETRuntime:0x1:4"}, clrEvents: "gc"), new string[] { "Warning: The CLR provider was already specified through --providers or --profile. Ignoring --clrevents.", - "", ProviderHeader, + "", + ProviderHeader, FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--providers"), "", LinuxHeader, @@ -156,7 +158,8 @@ public static IEnumerable BasicCases() yield return new object[] { TestArgs(clrEvents: "gc+jit"), new string[] { - "", ProviderHeader, + "", + ProviderHeader, FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000011","Informational",4,"--clrevents"), "", LinuxHeader, @@ -166,7 +169,8 @@ public static IEnumerable BasicCases() yield return new object[] { TestArgs(clrEvents: "gc+jit", clrEventLevel: "5"), new string[] { - "", ProviderHeader, + "", + ProviderHeader, FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000011","Verbose",5,"--clrevents"), "", LinuxHeader, From 208086f040f745c4f0bde7dbafeae016d19a4178 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Wed, 8 Oct 2025 15:57:59 +0000 Subject: [PATCH 24/25] Adjust functional tests and add failure cases --- .../CommandLine/Commands/CollectCommand.cs | 13 ++- .../Commands/CollectLinuxCommand.cs | 15 ++-- src/tests/Common/MockConsole.cs | 9 +- .../CollectCommandFunctionalTests.cs | 64 +++++++++++--- .../CollectLinuxCommandFunctionalTests.cs | 87 ++++++++++++++++--- 5 files changed, 152 insertions(+), 36 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index 405b495cc9..c9d4124f78 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -347,8 +347,6 @@ internal async Task Collect(CancellationToken ct, CommandLineConfiguration { ConsoleWriteLine($"Trace Duration : {duration:dd\\:hh\\:mm\\:ss}"); } - - ConsoleWriteLine(); ConsoleWriteLine(); EventMonitor eventMonitor = null; @@ -391,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..."); diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index d04e85cf03..5ff5415195 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -18,10 +18,10 @@ namespace Microsoft.Diagnostics.Tools.Trace { internal partial class CollectLinuxCommandHandler { - private static bool s_stopTracing; - private static Stopwatch s_stopwatch = new(); - private static LineRewriter s_rewriter; - private static bool s_printingStatus; + private bool s_stopTracing; + private Stopwatch s_stopwatch = new(); + private LineRewriter s_rewriter; + private bool s_printingStatus; internal sealed record CollectLinuxArgs( CancellationToken Ct, @@ -48,7 +48,7 @@ internal int CollectLinux(CollectLinuxArgs args) if (!OperatingSystem.IsLinux()) { Console.Error.WriteLine("The collect-linux command is only supported on Linux."); - return (int)ReturnCode.ArgumentError; + return (int)ReturnCode.PlatformNotSupportedError; } args.Ct.Register(() => s_stopTracing = true); @@ -72,6 +72,11 @@ internal int CollectLinux(CollectLinuxArgs args) 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)) 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/CollectCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectCommandFunctionalTests.cs index 463c8ff676..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 @@ -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,7 +78,7 @@ 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, @@ -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,7 +130,6 @@ public void Stop() {} public static IEnumerable BasicCases() { - FileInfo fi = new("trace.nettrace"); yield return new object[] { new CollectArgs(), @@ -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 index 88626e29ce..8d8add907b 100644 --- a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs +++ b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs @@ -11,6 +11,7 @@ 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 @@ -23,7 +24,7 @@ private static CollectLinuxCommandHandler.CollectLinuxArgs TestArgs( string clrEventLevel = "", string clrEvents = "", string[] perfEvents = null, - string[] profiles = null, + string[] profile = null, FileInfo output = null, TimeSpan duration = default) { @@ -32,7 +33,7 @@ private static CollectLinuxCommandHandler.CollectLinuxArgs TestArgs( clrEventLevel, clrEvents, perfEvents ?? Array.Empty(), - profiles ?? Array.Empty(), + profile ?? Array.Empty(), output ?? new FileInfo(CommonOptions.DefaultTraceName), duration); } @@ -42,23 +43,49 @@ private static CollectLinuxCommandHandler.CollectLinuxArgs TestArgs( public void CollectLinuxCommandProviderConfigurationConsolidation(object testArgs, string[] expectedLines) { MockConsole console = new(200, 30); - var handler = new CollectLinuxCommandHandler(console); - handler.RecordTraceInvoker = (cmd, len, cb) => 0; - int exit = handler.CollectLinux((CollectLinuxCommandHandler.CollectLinuxArgs)testArgs); + int exitCode = Run(testArgs, console); if (OperatingSystem.IsLinux()) { - Assert.Equal(0, exit); - console.AssertSanitizedLinesEqual(null, expectedLines); + Assert.Equal((int)ReturnCode.Ok, exitCode); + console.AssertSanitizedLinesEqual(null, expectedLines: expectedLines); } else { - Assert.Equal(3, exit); - console.AssertSanitizedLinesEqual(null, new string[] { + 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) => 0; + return handler.CollectLinux((CollectLinuxCommandHandler.CollectLinuxArgs)args); + } + + public static IEnumerable BasicCases() { yield return new object[] { @@ -98,7 +125,7 @@ public static IEnumerable BasicCases() } }; yield return new object[] { - TestArgs(profiles: new[]{"cpu-sampling"}), + TestArgs(profile: new[]{"cpu-sampling"}), new string[] { "No .NET providers were configured.", "", @@ -108,7 +135,7 @@ public static IEnumerable BasicCases() } }; yield return new object[] { - TestArgs(providers: new[]{"Foo:0x1:4"}, profiles: new[]{"cpu-sampling"}), + TestArgs(providers: new[]{"Foo:0x1:4"}, profile: new[]{"cpu-sampling"}), new string[] { "", ProviderHeader, @@ -120,7 +147,7 @@ public static IEnumerable BasicCases() } }; yield return new object[] { - TestArgs(clrEvents: "gc", profiles: new[]{"cpu-sampling"}), + TestArgs(clrEvents: "gc", profile: new[]{"cpu-sampling"}), new string[] { "", ProviderHeader, @@ -132,7 +159,7 @@ public static IEnumerable BasicCases() } }; yield return new object[] { - TestArgs(providers: new[]{"Microsoft-Windows-DotNETRuntime:0x1:4"}, profiles: new[]{"cpu-sampling"}), + TestArgs(providers: new[]{"Microsoft-Windows-DotNETRuntime:0x1:4"}, profile: new[]{"cpu-sampling"}), new string[] { "", ProviderHeader, @@ -189,6 +216,39 @@ public static IEnumerable BasicCases() }; } + 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 Events",-80}Enabled By"; private static string LinuxProfile(string name) => $"{name,-80}--profile"; @@ -200,5 +260,6 @@ 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}"; } } From 40c59057c029f0a167c2f23146ef8be4190ef101 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Wed, 8 Oct 2025 18:01:35 +0000 Subject: [PATCH 25/25] [DotnetTrace][CollectLinux] Add ProgressStatus and output file to tests --- .../Commands/CollectLinuxCommand.cs | 41 ++-- .../CollectLinuxCommandFunctionalTests.cs | 213 ++++++++++-------- 2 files changed, 144 insertions(+), 110 deletions(-) diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index 5ff5415195..bdd3ea73bf 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -135,10 +135,6 @@ private byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath scriptPath = null; List recordTraceArgs = new(); - string resolvedOutput = ResolveOutputPath(args.Output); - recordTraceArgs.Add($"--out"); - recordTraceArgs.Add(resolvedOutput); - string[] profiles = args.Profiles; if (args.Profiles.Length == 0 && args.Providers.Length == 0 && string.IsNullOrEmpty(args.ClrEvents) && args.PerfEvents.Length == 0) { @@ -169,17 +165,18 @@ private byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath scriptBuilder.Append($"record_dotnet_provider(\"{providerName}\", 0x{keywords:X}, {eventLevel}, {providerNameSanitized}_flags);\n\n"); } - Console.WriteLine($"{("Linux Events"),-80}Enabled By"); + List linuxEventLines = new(); foreach (string profile in profiles) { Profile traceProfile = ListProfilesCommandHandler.TraceProfiles .FirstOrDefault(p => p.Name.Equals(profile, StringComparison.OrdinalIgnoreCase)); - if (!string.IsNullOrEmpty(traceProfile.VerbExclusivity) && + if (traceProfile != null && + !string.IsNullOrEmpty(traceProfile.VerbExclusivity) && traceProfile.VerbExclusivity.Equals("collect-linux", StringComparison.OrdinalIgnoreCase)) { recordTraceArgs.Add(traceProfile.CollectLinuxArgs); - Console.WriteLine($"{traceProfile.Name,-80}--profile"); + linuxEventLines.Add($"{traceProfile.Name,-80}--profile"); } } @@ -193,14 +190,32 @@ private byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath string perfProvider = split[0]; string perfEventName = split[1]; - Console.WriteLine($"{perfEvent,-80}--perf-events"); + 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(); - string scriptFileName = $"{Path.GetFileNameWithoutExtension(resolvedOutput)}.script"; - scriptPath = Path.Combine(Environment.CurrentDirectory, scriptFileName); + scriptPath = Path.ChangeExtension(resolvedOutput.FullName, ".script"); File.WriteAllText(scriptPath, scriptText); recordTraceArgs.Add("--script-file"); @@ -210,15 +225,15 @@ private byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath return Encoding.UTF8.GetBytes(options); } - private static string ResolveOutputPath(FileInfo output) + private static FileInfo ResolveOutputPath(FileInfo output) { if (!string.Equals(output.Name, CommonOptions.DefaultTraceName, StringComparison.OrdinalIgnoreCase)) { - return output.Name; + return output; } DateTime now = DateTime.Now; - return $"trace_{now:yyyyMMdd}_{now:HHmmss}.nettrace"; + return new FileInfo($"trace_{now:yyyyMMdd}_{now:HHmmss}.nettrace"); } private int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) diff --git a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs index 8d8add907b..41dfee7159 100644 --- a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs +++ b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs @@ -34,7 +34,7 @@ private static CollectLinuxCommandHandler.CollectLinuxArgs TestArgs( clrEvents, perfEvents ?? Array.Empty(), profile ?? Array.Empty(), - output ?? new FileInfo(CommonOptions.DefaultTraceName), + output ?? new FileInfo("trace.nettrace"), duration); } @@ -47,7 +47,7 @@ public void CollectLinuxCommandProviderConfigurationConsolidation(object testArg if (OperatingSystem.IsLinux()) { Assert.Equal((int)ReturnCode.Ok, exitCode); - console.AssertSanitizedLinesEqual(null, expectedLines: expectedLines); + console.AssertSanitizedLinesEqual(CollectLinuxSanitizer, expectedLines: expectedLines); } else { @@ -81,138 +81,114 @@ public void CollectLinuxCommandProviderConfigurationConsolidation_Throws(object private static int Run(object args, MockConsole console) { var handler = new CollectLinuxCommandHandler(console); - handler.RecordTraceInvoker = (cmd, len, cb) => 0; + 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(), - new string[] { - "No providers, profiles, ClrEvents, or PerfEvents were specified, defaulting to trace profiles 'dotnet-common' + 'cpu-sampling'.", - "", - ProviderHeader, - FormatProvider("Microsoft-Windows-DotNETRuntime","000000100003801D","Informational",4,"--profile"), - "", - LinuxHeader, - LinuxProfile("cpu-sampling"), - "" - } + 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"}), - new string[] { - "", - ProviderHeader, - FormatProvider("Foo","0000000000000001","Informational",4,"--providers"), - "", - LinuxHeader, - "" - } + ExpectProvidersAndLinux( + new[]{FormatProvider("Foo","0000000000000001","Informational",4,"--providers")}, + Array.Empty()) }; + yield return new object[] { TestArgs(providers: new[]{"Foo:0x1:4","Bar:0x2:4"}), - new string[] { - "", - ProviderHeader, - FormatProvider("Foo","0000000000000001","Informational",4,"--providers"), - FormatProvider("Bar","0000000000000002","Informational",4,"--providers"), - "", - LinuxHeader, - "" - } + 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"}), - new string[] { - "No .NET providers were configured.", - "", - LinuxHeader, - LinuxProfile("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"}), - new string[] { - "", - ProviderHeader, - FormatProvider("Foo","0000000000000001","Informational",4,"--providers"), - "", - LinuxHeader, - LinuxProfile("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"}), - new string[] { - "", - ProviderHeader, - FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--clrevents"), - "", - LinuxHeader, - LinuxProfile("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"}), - new string[] { - "", - ProviderHeader, - FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--providers"), - "", - LinuxHeader, - LinuxProfile("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"), - new string[] { - "Warning: The CLR provider was already specified through --providers or --profile. Ignoring --clrevents.", - "", - ProviderHeader, - FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000001","Informational",4,"--providers"), - "", - LinuxHeader, - "" - } + 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"), - new string[] { - "", - ProviderHeader, - FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000011","Informational",4,"--clrevents"), - "", - LinuxHeader, - "" - } + ExpectProvidersAndLinux( + new[]{FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000011","Informational",4,"--clrevents")}, + Array.Empty()) }; + yield return new object[] { TestArgs(clrEvents: "gc+jit", clrEventLevel: "5"), - new string[] { - "", - ProviderHeader, - FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000011","Verbose",5,"--clrevents"), - "", - LinuxHeader, - "" - } + ExpectProvidersAndLinux( + new[]{FormatProvider("Microsoft-Windows-DotNETRuntime","0000000000000011","Verbose",5,"--clrevents")}, + Array.Empty()) }; + yield return new object[] { TestArgs(perfEvents: new[]{"sched:sched_switch"}), - new string[] { - "No .NET providers were configured.", - "", - LinuxHeader, - LinuxPerfEvent("sched:sched_switch"), - "" - } + ExpectProvidersAndLinuxWithMessages( + new[]{"No .NET providers were configured."}, + Array.Empty(), + new[]{LinuxPerfEvent("sched:sched_switch")}) }; } @@ -250,7 +226,7 @@ public static IEnumerable InvalidProviders() } private const string ProviderHeader = "Provider Name Keywords Level Enabled By"; - private static string LinuxHeader => $"{"Linux Events",-80}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) @@ -261,5 +237,48 @@ private static string FormatProvider(string name, string keywordsHex, string lev 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(); + } } }