diff --git a/documentation/design-docs/diagnostics-client-library.md b/documentation/design-docs/diagnostics-client-library.md index 842035ba6f..980ee62139 100644 --- a/documentation/design-docs/diagnostics-client-library.md +++ b/documentation/design-docs/diagnostics-client-library.md @@ -327,6 +327,15 @@ namespace Microsoft.Diagnostics.Client long keywords = 0, IDictionary arguments = null) + // Adds a per-provider Event ID filter. Using this overload starts the session with + // CollectTracing5 (requires a .NET 10+ target runtime). + public EventPipeProvider( + string name, + EventLevel eventLevel, + long keywords, + IDictionary arguments, + EventPipeProviderEventFilter eventFilter) + public long Keywords { get; } public EventLevel EventLevel { get; } @@ -335,6 +344,8 @@ namespace Microsoft.Diagnostics.Client public IDictionary Arguments { get; } + public EventPipeProviderEventFilter EventFilter { get; } + public override string ToString(); public override bool Equals(object obj); @@ -345,6 +356,20 @@ namespace Microsoft.Diagnostics.Client public static bool operator !=(Provider left, Provider right); } + + // An optional per-provider filter on Event IDs, applied by the runtime after the keyword/level + // filter. Available on runtimes that support CollectTracing5 (.NET 10+). + public class EventPipeProviderEventFilter + { + // enable=true: eventIds is an allow-list (only those IDs are enabled). + // enable=false: eventIds is a deny-list (every ID except those is enabled; an empty + // deny-list therefore enables all events). + public EventPipeProviderEventFilter(bool enable, IReadOnlyList eventIds) + + public bool Enable { get; } + + public IReadOnlyList EventIds { get; } + } } ``` diff --git a/documentation/design-docs/ipc-protocol.md b/documentation/design-docs/ipc-protocol.md index f4334e396d..3e76186866 100644 --- a/documentation/design-docs/ipc-protocol.md +++ b/documentation/design-docs/ipc-protocol.md @@ -287,6 +287,7 @@ enum class EventPipeCommandId : uint8_t CollectTracing3 = 0x04, // create/start a given session with/without collecting stacks CollectTracing4 = 0x05, // create/start a given session with specific rundown keyword CollectTracing5 = 0x06, // create/start a given session with/without user_events + CollectTracing6 = 0x07, // create/start a given session with a specific buffering mode } ``` See: [EventPipe Commands](#EventPipe-Commands) @@ -348,6 +349,7 @@ enum class EventPipeCommandId : uint8_t CollectTracing3 = 0x04, // create/start a given session with/without collecting stacks CollectTracing4 = 0x05, // create/start a given session with specific rundown keyword CollectTracing5 = 0x06, // create/start a given session with/without user_events + CollectTracing6 = 0x07, // create/start a given session with a specific buffering mode } ``` EventPipe Payloads are encoded with the following rules: @@ -732,6 +734,44 @@ A Streaming Session started with `CollectTracing5` is followed by an Optional Co A User_events Session started with `CollectTracing5` expects the Optional Continuation to contain another message passing along the SCM_RIGHTS `user_events_data` file descriptor. See [details](#passing_file_descriptor) +### `CollectTracing6` + +Command Code: `0x0207` + +The `CollectTracing6` command is an extension of the `CollectTracing5` command. It has all the capabilities of `CollectTracing5` and adds a trailing `sessionBufferMode` field to the **streaming session payload** that selects how the runtime's per-session event buffer behaves when it fills faster than the session is drained. The user_events session payload is unchanged from `CollectTracing5`, since a user_events session does not use the buffer manager. + +> Note available for .NET 11.0 and later. + +#### Inputs: + +Header: `{ Magic; 20 + Payload Size; 0x0207; 0x0000 }` + +#### Streaming Session Payload: +* `uint session_type`: 0 +* `uint streaming_circularBufferMB`: Specifies the size of the Streaming session's circular buffer used for buffering event data. +* `uint streaming_format`: 0 for the legacy NetPerf format and 1 for the NetTrace V4 format. Specifies the format in which event data will be serialized into the IPC Stream +* `ulong rundownKeyword`: Indicates the keyword for the rundown provider +* `bool requestStackwalk`: Indicates whether stacktrace information should be recorded. +* `array providers`: The providers to turn on for the session +* `uint sessionBufferMode`: Selects the session's buffering behavior. `0` = Drop (default): the circular buffer drops events when it overflows (lossy). `1` = Block: producers block until the reader frees buffer capacity instead of dropping events (non-lossy). + +The `streaming_provider_config` and its `event_filter` are encoded exactly as in [`CollectTracing5`](#collecttracing5). + +#### User_events Session Payload: + +Identical to the [`CollectTracing5`](#collecttracing5) user_events session payload; it has no `sessionBufferMode` field. + +#### Returns (as an IPC Message Payload): + +Header: `{ Magic; 28; 0xFF00; 0x0000; }` + +`CollectTracing6` returns: +* `ulong sessionId`: the ID for the EventPipe Session started + +A Streaming Session started with `CollectTracing6` is followed by an Optional Continuation of a `nettrace` format stream of events. + +A User_events Session started with `CollectTracing6` expects the Optional Continuation to contain another message passing along the SCM_RIGHTS `user_events_data` file descriptor. See [details](#passing_file_descriptor) + ## EventPipe Payload Serialization Examples ### Event_filter diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeProvider.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeProvider.cs index 8fc8bab6eb..c5bdbe1f54 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeProvider.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeProvider.cs @@ -7,14 +7,52 @@ namespace Microsoft.Diagnostics.NETCore.Client { + /// + /// An optional per-provider filter on Event IDs, applied by the runtime after the keyword/level + /// filter. Requires a target runtime that supports CollectTracing5 (.NET 10+). + /// + public sealed class EventPipeProviderEventFilter + { + /// + /// When true, is an allow-list: only those Event IDs are enabled. + /// When false, it is a deny-list: every Event ID except those is enabled (so an empty list with + /// enable=false enables all events). + /// + /// The Event IDs to enable or disable, per . + public EventPipeProviderEventFilter(bool enable, IReadOnlyList eventIds) + { + Enable = enable; + EventIds = eventIds ?? (IReadOnlyList)System.Array.Empty(); + } + + public bool Enable { get; } + + public IReadOnlyList EventIds { get; } + } + public sealed class EventPipeProvider { public EventPipeProvider(string name, EventLevel eventLevel, long keywords = 0xF00000000000, IDictionary arguments = null) + : this(name, eventLevel, keywords, arguments, eventFilter: null) + { + } + + /// + /// Creates a provider that additionally filters which Event IDs are enabled. Using this overload + /// starts the session with CollectTracing5 (requires a .NET 10+ target runtime). + /// + /// The provider name. + /// The verbosity level to enable. + /// A bitmask of keywords to enable. + /// Optional provider arguments, or null. + /// The per-provider Event ID filter applied after the keyword/level filter. + public EventPipeProvider(string name, EventLevel eventLevel, long keywords, IDictionary arguments, EventPipeProviderEventFilter eventFilter) { Name = name; EventLevel = eventLevel; Keywords = keywords; Arguments = arguments; + EventFilter = eventFilter; } public long Keywords { get; } @@ -25,6 +63,12 @@ public EventPipeProvider(string name, EventLevel eventLevel, long keywords = 0xF public IDictionary Arguments { get; } + /// + /// An optional filter on this provider's Event IDs, applied after the keyword/level filter. + /// Setting it causes the session to be started with CollectTracing5 (requires a .NET 10+ target). + /// + public EventPipeProviderEventFilter EventFilter { get; } + public override string ToString() { return $"{Name}:0x{Keywords:X16}:{(uint)EventLevel}{(Arguments == null ? "" : $":{GetArgumentString()}")}"; diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSession.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSession.cs index fd7508c24a..fc70e35d47 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSession.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSession.cs @@ -92,12 +92,25 @@ public async Task StopAsync(CancellationToken cancellationToken) } } - private static IpcMessage CreateStartMessage(EventPipeSessionConfiguration config) + // Internal for unit testing of the version/command selection logic. + internal static IpcMessage CreateStartMessage(EventPipeSessionConfiguration config) { // To keep backward compatibility with older runtimes we only use newer serialization format when needed EventPipeCommandId command; byte[] payload; - if (config.RundownKeyword != DefaultRundownKeyword && config.RundownKeyword != 0) + if (config.BufferingMode != EventPipeBufferingMode.Default) + { + // V6 adds an opt-in session buffering mode (its payload also carries any event filters) + command = EventPipeCommandId.CollectTracing6; + payload = config.SerializeV6(); + } + else if (HasEventFilter(config)) + { + // V5 adds a per-provider event-id filter (and a session-type prefix) + command = EventPipeCommandId.CollectTracing5; + payload = config.SerializeV5(); + } + else if (config.RundownKeyword != DefaultRundownKeyword && config.RundownKeyword != 0) { // V4 has added support to specify rundown keyword command = EventPipeCommandId.CollectTracing4; @@ -118,6 +131,20 @@ private static IpcMessage CreateStartMessage(EventPipeSessionConfiguration confi return new IpcMessage(DiagnosticsServerCommandSet.EventPipe, (byte)command, payload); } + // A per-provider Event ID filter is available on CollectTracing5 and later. + private static bool HasEventFilter(EventPipeSessionConfiguration config) + { + foreach (EventPipeProvider provider in config.Providers) + { + if (provider.EventFilter != null) + { + return true; + } + } + + return false; + } + private static EventPipeSession CreateSessionFromResponse(IpcEndpoint endpoint, ref IpcResponse? response, string operationName) { try diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSessionConfiguration.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSessionConfiguration.cs index 76ba1d27e3..886fc29616 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSessionConfiguration.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSessionConfiguration.cs @@ -14,6 +14,37 @@ internal enum EventPipeSerializationFormat NetTrace } + // Session type as encoded on the CollectTracing5+ wire. This is NOT the runtime's internal + // EventPipeSessionType; the IPC collect command maps wire 0 => IpcStream, 1 => UserEvents. + internal enum EventPipeSessionType : uint + { + IpcStream = 0, + + // This client does not support starting/tracing a user_events session (that path needs an + // out-of-band file descriptor, see the IPC protocol docs); the value exists only for wire + // correctness when describing the CollectTracing5+ session-type field. + UserEvents = 1 + } + + /// + /// Controls how the runtime's per-session event buffer behaves when it fills faster than the + /// session is drained. + /// + public enum EventPipeBufferingMode + { + /// + /// The runtime default: a circular buffer that drops events when it overflows (lossy). + /// + Default = 0, + + /// + /// Non-lossy: producers block until the reader frees buffer capacity rather than dropping + /// events. Available on .NET 11+; useful for collections that must be complete (e.g. a heap + /// snapshot on a large heap). + /// + Block = 1 + } + public sealed class EventPipeSessionConfiguration { /// @@ -46,12 +77,30 @@ public EventPipeSessionConfiguration( bool requestStackwalk = true) : this(circularBufferSizeMB, EventPipeSerializationFormat.NetTrace, providers, requestStackwalk, rundownKeyword) {} + /// + /// Creates a new configuration object for the EventPipeSession with a specific buffering mode. + /// For details, see the documentation of each property of this object. + /// + /// An IEnumerable containing the list of Providers to turn on. + /// The size of the runtime's buffer for collecting events in MB + /// If true, request rundown events from the runtime. + /// If true, record a stacktrace for every emitted event. + /// The session buffering mode; Block requests non-lossy collection (CollectTracing6, .NET 11+). + public EventPipeSessionConfiguration( + IEnumerable providers, + int circularBufferSizeMB, + bool requestRundown, + bool requestStackwalk, + EventPipeBufferingMode bufferingMode) : this(circularBufferSizeMB, EventPipeSerializationFormat.NetTrace, providers, requestStackwalk, (requestRundown ? EventPipeSession.DefaultRundownKeyword : 0), bufferingMode) + {} + private EventPipeSessionConfiguration( int circularBufferSizeMB, EventPipeSerializationFormat format, IEnumerable providers, bool requestStackwalk, - long rundownKeyword) + long rundownKeyword, + EventPipeBufferingMode bufferingMode = EventPipeBufferingMode.Default) { if (circularBufferSizeMB == 0) { @@ -78,6 +127,7 @@ private EventPipeSessionConfiguration( Format = format; RequestStackwalk = requestStackwalk; RundownKeyword = rundownKeyword; + BufferingMode = bufferingMode; } /// @@ -112,6 +162,12 @@ private EventPipeSessionConfiguration( /// public long RundownKeyword { get; internal set; } + /// + /// Buffering mode for the session. requests non-lossy + /// collection (sent as CollectTracing6); the default keeps the runtime's lossy circular buffer. + /// + public EventPipeBufferingMode BufferingMode { get; } + /// /// Providers to enable for this session. /// @@ -183,6 +239,51 @@ public static byte[] SerializeV4(this EventPipeSessionConfiguration config) return serializedData; } + public static byte[] SerializeV5(this EventPipeSessionConfiguration config) + { + byte[] serializedData = null; + using (MemoryStream stream = new()) + using (BinaryWriter writer = new(stream)) + { + // This client only creates streaming (IpcStream) sessions. + writer.Write((uint)EventPipeSessionType.IpcStream); + writer.Write(config.CircularBufferSizeInMB); + writer.Write((uint)config.Format); + writer.Write(config.RundownKeyword); + writer.Write(config.RequestStackwalk); + + SerializeProvidersV5(config, writer); + + writer.Flush(); + serializedData = stream.ToArray(); + } + + return serializedData; + } + + public static byte[] SerializeV6(this EventPipeSessionConfiguration config) + { + byte[] serializedData = null; + using (MemoryStream stream = new()) + using (BinaryWriter writer = new(stream)) + { + writer.Write((uint)EventPipeSessionType.IpcStream); + writer.Write(config.CircularBufferSizeInMB); + writer.Write((uint)config.Format); + writer.Write(config.RundownKeyword); + writer.Write(config.RequestStackwalk); + + SerializeProvidersV5(config, writer); + + writer.Write((uint)config.BufferingMode); + + writer.Flush(); + serializedData = stream.ToArray(); + } + + return serializedData; + } + private static void SerializeProviders(EventPipeSessionConfiguration config, BinaryWriter writer) { writer.Write(config.Providers.Count); @@ -194,5 +295,39 @@ private static void SerializeProviders(EventPipeSessionConfiguration config, Bin writer.WriteString(provider.GetArgumentString()); } } + + // CollectTracing5+ per-provider layout: the V4 fields plus a trailing event filter. + private static void SerializeProvidersV5(EventPipeSessionConfiguration config, BinaryWriter writer) + { + writer.Write(config.Providers.Count); + foreach (EventPipeProvider provider in config.Providers) + { + writer.Write(unchecked((ulong)provider.Keywords)); + writer.Write((uint)provider.EventLevel); + writer.WriteString(provider.Name); + writer.WriteString(provider.GetArgumentString()); + SerializeEventFilter(writer, provider.EventFilter); + } + } + + // Serializes a provider's CollectTracing5+ event filter. A null filter (no explicit Event ID + // filter) is written as a disabled, empty filter (enable=false, count=0), which the runtime + // interprets as "allow all events". + private static void SerializeEventFilter(BinaryWriter writer, EventPipeProviderEventFilter filter) + { + if (filter == null) + { + writer.Write(false); + writer.Write(0u); + return; + } + + writer.Write(filter.Enable); + writer.Write((uint)filter.EventIds.Count); + foreach (uint eventId in filter.EventIds) + { + writer.Write(eventId); + } + } } } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs index a92e79b542..82edb56ae7 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs @@ -27,6 +27,8 @@ internal enum EventPipeCommandId : byte CollectTracing2 = 0x03, CollectTracing3 = 0x04, CollectTracing4 = 0x05, + CollectTracing5 = 0x06, + CollectTracing6 = 0x07, } internal enum DumpCommandId : byte diff --git a/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs b/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs index b5a44b6895..3c82f80262 100644 --- a/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs +++ b/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs @@ -28,7 +28,7 @@ internal static class CollectCommandHandler /// The diagnostic IPC channel to collect the gcdump from. /// The dsrouter command to use for collecting the gcdump. /// - private static async Task Collect(CancellationToken ct, int processId, string output, int timeout, bool verbose, string name, string diagnosticPort, string dsrouter) + private static async Task Collect(CancellationToken ct, int processId, string output, int timeout, bool verbose, string name, string diagnosticPort, string dsrouter, bool nonLossy) { try { @@ -66,7 +66,7 @@ private static async Task Collect(CancellationToken ct, int processId, stri Console.Out.WriteLine($"Writing gcdump to '{outputFileInfo.FullName}'..."); Task dumpTask = Task.Run(() => { - if (TryCollectMemoryGraph(ct, processId, diagnosticPort, timeout, verbose, out MemoryGraph memoryGraph)) + if (TryCollectMemoryGraph(ct, processId, diagnosticPort, timeout, verbose, nonLossy, out MemoryGraph memoryGraph)) { GCHeapDump.WriteMemoryGraph(memoryGraph, outputFileInfo.FullName, "dotnet-gcdump"); return true; @@ -104,6 +104,16 @@ private static async Task Collect(CancellationToken ct, int processId, stri Console.Error.WriteLine($"--diagnostic-port argument error: {fe.Message}"); return -1; } + catch (UnsupportedCommandException) when (nonLossy) + { + // The target runtime is too old to understand CollectTracing6 (the non-lossy/Block command). + // TODO: Once .NET 11 has shipped, proactively probe support before starting the session via + // DiagnosticsClient.GetProcessInfo()/TryGetProcessClrVersion() (>= 11.0), like dotnet-trace + // collect-linux, so we can fail fast without first attempting the command. + Console.Error.WriteLine("[ERROR] The target process does not support non-lossy gcdump collection, which requires a .NET 11+ runtime."); + Console.Error.WriteLine("Collect without the --non-lossy option to capture a gcdump using the default (lossy) buffering."); + return -1; + } catch (Exception ex) { Console.Error.WriteLine($"[ERROR] {ex}"); @@ -115,14 +125,14 @@ private static async Task Collect(CancellationToken ct, int processId, stri } } - internal static bool TryCollectMemoryGraph(CancellationToken ct, int processId, string diagnosticPort, int timeout, bool verbose, out MemoryGraph memoryGraph) + internal static bool TryCollectMemoryGraph(CancellationToken ct, int processId, string diagnosticPort, int timeout, bool verbose, bool nonLossy, out MemoryGraph memoryGraph) { DotNetHeapInfo heapInfo = new(); TextWriter log = verbose ? Console.Out : TextWriter.Null; memoryGraph = new MemoryGraph(50_000); - if (!EventPipeDotNetHeapDumper.DumpFromEventPipe(ct, processId, diagnosticPort, memoryGraph, log, timeout, heapInfo)) + if (!EventPipeDotNetHeapDumper.DumpFromEventPipe(ct, processId, diagnosticPort, memoryGraph, log, timeout, heapInfo, nonLossy)) { return false; } @@ -143,7 +153,8 @@ public static Command CollectCommand() TimeoutOption, NameOption, DiagnosticPortOption, - DsRouterOption + DsRouterOption, + NonLossyOption }; collectCommand.SetAction(static (parseResult, ct) => Collect(ct, @@ -153,7 +164,8 @@ public static Command CollectCommand() verbose: parseResult.GetValue(VerboseOption), name: parseResult.GetValue(NameOption), diagnosticPort: parseResult.GetValue(DiagnosticPortOption) ?? string.Empty, - dsrouter: parseResult.GetValue(DsRouterOption) ?? string.Empty)); + dsrouter: parseResult.GetValue(DsRouterOption) ?? string.Empty, + nonLossy: parseResult.GetValue(NonLossyOption))); return collectCommand; } @@ -201,5 +213,11 @@ public static Command CollectCommand() { Description = "The dsrouter command to use for collecting the gcdump. If specified, the --process-id, --name, or --diagnostic-port options cannot be used." }; + + private static readonly Option NonLossyOption = + new("--non-lossy") + { + Description = "Collect without dropping events: the runtime blocks producers until the buffer is drained rather than overwriting events when the buffer fills. This produces a complete gcdump on large heaps, but requires a target runtime that supports it (.NET 11+) and can make collection slower." + }; } } diff --git a/src/Tools/dotnet-gcdump/CommandLine/ReportCommandHandler.cs b/src/Tools/dotnet-gcdump/CommandLine/ReportCommandHandler.cs index 092a8c592a..64a3ef1cea 100644 --- a/src/Tools/dotnet-gcdump/CommandLine/ReportCommandHandler.cs +++ b/src/Tools/dotnet-gcdump/CommandLine/ReportCommandHandler.cs @@ -124,7 +124,7 @@ private static Task ReportFromProcess(int processId, string diagnosticPort, } if (!CollectCommandHandler - .TryCollectMemoryGraph(ct, processId, diagnosticPort, CollectCommandHandler.DefaultTimeout, false, out Graphs.MemoryGraph mg)) + .TryCollectMemoryGraph(ct, processId, diagnosticPort, CollectCommandHandler.DefaultTimeout, false, false, out Graphs.MemoryGraph mg)) { Console.Error.WriteLine("An error occured while collecting gcdump."); return Task.FromResult(-1); diff --git a/src/Tools/dotnet-gcdump/DotNetHeapDump/EventPipeDotNetHeapDumper.cs b/src/Tools/dotnet-gcdump/DotNetHeapDump/EventPipeDotNetHeapDumper.cs index 5eaf9bce4a..06d47ed686 100644 --- a/src/Tools/dotnet-gcdump/DotNetHeapDump/EventPipeDotNetHeapDumper.cs +++ b/src/Tools/dotnet-gcdump/DotNetHeapDump/EventPipeDotNetHeapDumper.cs @@ -115,7 +115,7 @@ public static bool DumpFromEventPipeFile(string path, MemoryGraph memoryGraph, T /// /// /// - public static bool DumpFromEventPipe(CancellationToken ct, int processId, string diagnosticPort, MemoryGraph memoryGraph, TextWriter log, int timeout, DotNetHeapInfo dotNetInfo) + public static bool DumpFromEventPipe(CancellationToken ct, int processId, string diagnosticPort, MemoryGraph memoryGraph, TextWriter log, int timeout, DotNetHeapInfo dotNetInfo, bool nonLossy = false) { DateTime start = DateTime.Now; Func getElapsed = () => DateTime.Now - start; @@ -158,7 +158,7 @@ public static bool DumpFromEventPipe(CancellationToken ct, int processId, string using EventPipeSessionController gcDumpSession = new(processId, diagnosticPort, new List { new("Microsoft-Windows-DotNETRuntime", EventLevel.Verbose, (long)(ClrTraceEventParser.Keywords.GCHeapSnapshot)) - }); + }, nonLossy: nonLossy); log.WriteLine("{0,5:n1}s: gcdump EventPipe Session started", getElapsed().TotalSeconds); int gcNum = -1; @@ -299,6 +299,12 @@ public static bool DumpFromEventPipe(CancellationToken ct, int processId, string dumper.ConvertHeapDataToGraph(); // Finish the conversion. } } + catch (UnsupportedCommandException) when (nonLossy) + { + // The runtime is too old for the non-lossy (CollectTracing6) command. Surface this to the + // caller rather than swallowing it like other gcdump errors, so a helpful message is shown. + throw; + } catch (Exception e) { log.WriteLine($"{getElapsed().TotalSeconds,5:n1}s: [Error] Exception during gcdump: {e}"); @@ -324,7 +330,7 @@ internal sealed class EventPipeSessionController : IDisposable public bool UseWildcardProcessId => _diagnosticPort != null; - public EventPipeSessionController(int pid, string diagnosticPort, List providers, bool requestRundown = true) + public EventPipeSessionController(int pid, string diagnosticPort, List providers, bool requestRundown = true, bool nonLossy = false) { if (string.IsNullOrEmpty(diagnosticPort)) { @@ -360,7 +366,10 @@ public EventPipeSessionController(int pid, string diagnosticPort, List + /// Tests for EventPipe IPC command selection (CollectTracing2..6) and the CollectTracing5/6 payload + /// serialization, including per-provider event filters and the trailing buffering mode. This is + /// wire-format logic that affects compatibility across runtime versions, so it is covered directly. + /// + public class EventPipeSessionConfigurationSerializationTests + { + private static List Providers(EventPipeProviderEventFilter filter = null) => + new() { new EventPipeProvider("My-Test-Source", EventLevel.Verbose, keywords: -1, arguments: null, eventFilter: filter) }; + + private static byte CommandId(EventPipeSessionConfiguration config) => + EventPipeSession.CreateStartMessage(config).Header.CommandId; + + // ---- Command selection ---- + + [Fact] + public void Selects_CollectTracing2_ForDefaultConfig() + { + EventPipeSessionConfiguration config = new(Providers()); + Assert.Equal((byte)EventPipeCommandId.CollectTracing2, CommandId(config)); + } + + [Fact] + public void Selects_CollectTracing3_WhenStackwalkDisabled() + { + EventPipeSessionConfiguration config = new(Providers(), circularBufferSizeMB: 256, requestRundown: true, requestStackwalk: false); + Assert.Equal((byte)EventPipeCommandId.CollectTracing3, CommandId(config)); + } + + [Fact] + public void Selects_CollectTracing4_ForCustomRundownKeyword() + { + EventPipeSessionConfiguration config = new(Providers(), circularBufferSizeMB: 256, rundownKeyword: 0x1); + Assert.Equal((byte)EventPipeCommandId.CollectTracing4, CommandId(config)); + } + + [Fact] + public void Selects_CollectTracing5_WhenProviderHasEventFilter() + { + EventPipeSessionConfiguration config = new(Providers(new EventPipeProviderEventFilter(enable: true, new uint[] { 1 }))); + Assert.Equal((byte)EventPipeCommandId.CollectTracing5, CommandId(config)); + } + + [Fact] + public void Selects_CollectTracing6_ForBlockBufferingMode() + { + EventPipeSessionConfiguration config = new(Providers(), circularBufferSizeMB: 256, requestRundown: true, requestStackwalk: true, bufferingMode: EventPipeBufferingMode.Block); + Assert.Equal((byte)EventPipeCommandId.CollectTracing6, CommandId(config)); + } + + [Fact] + public void Selects_CollectTracing6_WhenBlockBufferingCombinedWithEventFilter() + { + EventPipeSessionConfiguration config = new( + Providers(new EventPipeProviderEventFilter(enable: false, new uint[] { 2 })), + circularBufferSizeMB: 256, requestRundown: true, requestStackwalk: true, bufferingMode: EventPipeBufferingMode.Block); + Assert.Equal((byte)EventPipeCommandId.CollectTracing6, CommandId(config)); + } + + // ---- Payload serialization ---- + + [Fact] + public void SerializeV5_StreamingPayload_BeginsWithIpcStreamSessionType() + { + EventPipeSessionConfiguration config = new(Providers()); + byte[] payload = config.SerializeV5(); + // The CollectTracing5 streaming payload is prefixed with the session type; 0 == IpcStream. + Assert.Equal(0u, BitConverter.ToUInt32(payload, 0)); + } + + [Fact] + public void SerializeV6_IsV5PayloadPlusTrailingBufferingMode() + { + EventPipeSessionConfiguration config = new(Providers(), circularBufferSizeMB: 256, requestRundown: true, requestStackwalk: true, bufferingMode: EventPipeBufferingMode.Block); + byte[] v5 = config.SerializeV5(); + byte[] v6 = config.SerializeV6(); + + // V6 is the V5 streaming payload plus a trailing uint buffering mode. + Assert.Equal(v5.Length + sizeof(uint), v6.Length); + Assert.Equal(v5, v6.Take(v5.Length).ToArray()); + Assert.Equal((uint)EventPipeBufferingMode.Block, BitConverter.ToUInt32(v6, v5.Length)); + } + + [Fact] + public void SerializeV5_WritesAllowListEventFilter() + { + uint[] ids = { 2, 4, 6 }; + EventPipeSessionConfiguration config = new(Providers(new EventPipeProviderEventFilter(enable: true, ids))); + + ParsedEventFilter filter = ParseProviderEventFilter(config.SerializeV5()); + + Assert.True(filter.Enable); + Assert.Equal(ids, filter.EventIds); + } + + [Fact] + public void SerializeV5_NullEventFilter_WritesAllowAll() + { + EventPipeSessionConfiguration config = new(Providers()); + + ParsedEventFilter filter = ParseProviderEventFilter(config.SerializeV5()); + + // "allow all" is encoded as a disabled, empty filter (enable=false, count=0). + Assert.False(filter.Enable); + Assert.Empty(filter.EventIds); + } + + [Fact] + public void OriginalEventPipeProviderConstructor_LeavesEventFilterNull() + { + // The original (binary-compatible) constructor must still exist and yield no filter. + EventPipeProvider provider = new("My-Test-Source", EventLevel.Verbose); + Assert.Null(provider.EventFilter); + } + + // ---- helpers ---- + + private struct ParsedEventFilter + { + public bool Enable; + public uint[] EventIds; + } + + // Parses the single provider out of a CollectTracing5 streaming payload, returning its event filter. + private static ParsedEventFilter ParseProviderEventFilter(byte[] payload) + { + int i = 0; + Assert.Equal(0u, BitConverter.ToUInt32(payload, i)); i += sizeof(uint); // session_type (IpcStream) + i += sizeof(int); // circular buffer size MB + i += sizeof(uint); // format + i += sizeof(long); // rundown keyword + i += sizeof(bool); // request stackwalk + + int providerCount = BitConverter.ToInt32(payload, i); i += sizeof(int); + Assert.Equal(1, providerCount); + + i += sizeof(ulong); // keywords + i += sizeof(uint); // level + IpcHelpers.ReadString(payload, ref i); // name + IpcHelpers.ReadString(payload, ref i); // arguments + + ParsedEventFilter filter = default; + filter.Enable = payload[i] != 0; i += sizeof(bool); + uint count = BitConverter.ToUInt32(payload, i); i += sizeof(uint); + filter.EventIds = new uint[count]; + for (int k = 0; k < count; k++) + { + filter.EventIds[k] = BitConverter.ToUInt32(payload, i); i += sizeof(uint); + } + + return filter; + } + } +}