Skip to content

Commit 97de881

Browse files
authored
Add RequestStackwalk parameter to EventPipeSession (#4290)
Client-side of dotnet/runtime#84077 and the implementation of #3696. To simplify the interface I made `EventPipeSessionConfiguration` public and introduced a new method in the DiagnosticsClient: `Task<EventPipeSession> StartEventPipeSessionAsync(EventPipeSessionConfiguration configuration, CancellationToken token)`. This is the only method that supports disabling the stackwalk so no additional overloads with a new bool parameter and no synchronous counterpart. I believe it'd be easier to use and maintain a single async method with the options rather than creating more overloads or default parameters but I may not have all the context here so please correct me if you think it's a bad idea. To deal with the backward compatibility I only use `CollectTracingV3` when necessary i.e. when `RequestStackwalk` option is set to false. I think it's a good compromise between the added complexity and potentially surprising behavior: * when the client is old and the runtime is new everything works because the runtime supports `CollectTracingV2` * when the client is new but the runtime is old everything works until the new option is used. When it's used the session won't start as `CollectTracingV3` doesn't exist server side: there'd be no clear error message but it's documented in the option summary. * when both the client and the runtime are new either `CollectTracingV2` or `CollectTracingV3` may be used transparently for the user * we may use the same trick when we introduce `CollectTracingV4` The alternative is to implement version negotiation of some sort but I'd like to have your opinion before attempting this as handling the errors correctly wouldn't be easy (f.e. in [my current fork](criteo-forks@3946b4a#diff-e8365039cd36eae3dec611784fc7076be7dadeda1007733412aaaa63f40a748fR39) I just hide the exception) The testing turned out to be a bit complex as I needed to convert EventPipe stream to `TraceLog` to be able to read the stacktraces. I couldn't achieve that without writing data to a file. Afaiu the stackwalk may not work correctly without the rundown that only happens at the end of the session so I wonder if looking at the stacktraces with a live session is even possible (though iirc netfw+ETW could do that back in the days) ? Thanks for your time !
1 parent e76bdb8 commit 97de881

File tree

8 files changed

+278
-33
lines changed

8 files changed

+278
-33
lines changed

documentation/design-docs/ipc-protocol.md

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ enum class ProfilerCommandId : uint8_t
370370
AttachProfiler = 0x01,
371371
// future
372372
}
373-
```
373+
```
374374
See: [Profiler Commands](#Profiler-Commands)
375375
376376
```c++
@@ -460,7 +460,7 @@ Payload
460460
array<provider_config> providers
461461
}
462462

463-
provider_config
463+
provider_config
464464
{
465465
ulong keywords,
466466
uint logLevel,
@@ -482,7 +482,7 @@ Followed by an Optional Continuation of a `nettrace` format stream of events.
482482

483483
Command Code: `0x0203`
484484

485-
The `CollectTracing2` Command is an extension of the `CollectTracing` command - its behavior is the same as `CollectTracing` command, except that it has another field that lets you specify whether rundown events should be fired by the runtime.
485+
The `CollectTracing2` command is an extension of the `CollectTracing` command - its behavior is the same as `CollectTracing` command, except that it has another field that lets you specify whether rundown events should be fired by the runtime.
486486

487487
#### Inputs:
488488

@@ -500,7 +500,7 @@ A `provider_config` is composed of the following data:
500500
* `string filter_data` (optional): Filter information
501501

502502
> see ETW documentation for a more detailed explanation of Keywords, Filters, and Log Level.
503-
>
503+
>
504504
#### Returns (as an IPC Message Payload):
505505

506506
Header: `{ Magic; 28; 0xFF00; 0x0000; }`
@@ -520,7 +520,7 @@ Payload
520520
array<provider_config> providers
521521
}
522522
523-
provider_config
523+
provider_config
524524
{
525525
ulong keywords,
526526
uint logLevel,
@@ -538,7 +538,70 @@ Payload
538538
```
539539
Followed by an Optional Continuation of a `nettrace` format stream of events.
540540

541-
### `StopTracing`
541+
### `CollectTracing3`
542+
543+
Command Code: `0x0204`
544+
545+
The `CollectTracing3` command is an extension of the `CollectTracing2` command - its behavior is the same as `CollectTracing2` command, except that it has another field that lets you specify whether the stackwalk should be made for each event.
546+
547+
#### Inputs:
548+
549+
Header: `{ Magic; Size; 0x0203; 0x0000 }`
550+
551+
* `uint circularBufferMB`: The size of the circular buffer used for buffering event data while streaming
552+
* `uint format`: 0 for the legacy NetPerf format and 1 for the NetTrace format
553+
* `bool requestRundown`: Indicates whether rundown should be fired by the runtime.
554+
* `bool requestStackwalk`: Indicates whether stacktrace information should be recorded.
555+
* `array<provider_config> providers`: The providers to turn on for the streaming session
556+
557+
A `provider_config` is composed of the following data:
558+
* `ulong keywords`: The keywords to turn on with this providers
559+
* `uint logLevel`: The level of information to turn on
560+
* `string provider_name`: The name of the provider
561+
* `string filter_data` (optional): Filter information
562+
563+
> see ETW documentation for a more detailed explanation of Keywords, Filters, and Log Level.
564+
>
565+
#### Returns (as an IPC Message Payload):
566+
567+
Header: `{ Magic; 28; 0xFF00; 0x0000; }`
568+
569+
`CollectTracing2` returns:
570+
* `ulong sessionId`: the ID for the stream session starting on the current connection
571+
572+
##### Details:
573+
574+
Input:
575+
```
576+
Payload
577+
{
578+
uint circularBufferMB,
579+
uint format,
580+
bool requestRundown,
581+
bool requestStackwalk,
582+
array<provider_config> providers
583+
}
584+
585+
provider_config
586+
{
587+
ulong keywords,
588+
uint logLevel,
589+
string provider_name,
590+
string filter_data (optional)
591+
}
592+
```
593+
594+
Returns:
595+
```c
596+
Payload
597+
{
598+
ulong sessionId
599+
}
600+
```
601+
Followed by an Optional Continuation of a `nettrace` format stream of events.
602+
603+
604+
### `StopTracing`
542605

543606
Command Code: `0x0201`
544607

src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ internal Task WaitForConnectionAsync(CancellationToken token)
7070
/// </returns>
7171
public EventPipeSession StartEventPipeSession(IEnumerable<EventPipeProvider> providers, bool requestRundown = true, int circularBufferMB = DefaultCircularBufferMB)
7272
{
73-
return EventPipeSession.Start(_endpoint, providers, requestRundown, circularBufferMB);
73+
EventPipeSessionConfiguration config = new(providers, circularBufferMB, requestRundown: requestRundown, requestStackwalk: true);
74+
return EventPipeSession.Start(_endpoint, config);
7475
}
7576

7677
/// <summary>
@@ -84,7 +85,8 @@ public EventPipeSession StartEventPipeSession(IEnumerable<EventPipeProvider> pro
8485
/// </returns>
8586
public EventPipeSession StartEventPipeSession(EventPipeProvider provider, bool requestRundown = true, int circularBufferMB = DefaultCircularBufferMB)
8687
{
87-
return EventPipeSession.Start(_endpoint, new[] { provider }, requestRundown, circularBufferMB);
88+
EventPipeSessionConfiguration config = new(new[] {provider}, circularBufferMB, requestRundown: requestRundown, requestStackwalk: true);
89+
return EventPipeSession.Start(_endpoint, config);
8890
}
8991

9092
/// <summary>
@@ -100,7 +102,8 @@ public EventPipeSession StartEventPipeSession(EventPipeProvider provider, bool r
100102
public Task<EventPipeSession> StartEventPipeSessionAsync(IEnumerable<EventPipeProvider> providers, bool requestRundown,
101103
int circularBufferMB = DefaultCircularBufferMB, CancellationToken token = default)
102104
{
103-
return EventPipeSession.StartAsync(_endpoint, providers, requestRundown, circularBufferMB, token);
105+
EventPipeSessionConfiguration config = new(providers, circularBufferMB, requestRundown: requestRundown, requestStackwalk: true);
106+
return EventPipeSession.StartAsync(_endpoint, config, token);
104107
}
105108

106109
/// <summary>
@@ -116,7 +119,21 @@ public Task<EventPipeSession> StartEventPipeSessionAsync(IEnumerable<EventPipePr
116119
public Task<EventPipeSession> StartEventPipeSessionAsync(EventPipeProvider provider, bool requestRundown,
117120
int circularBufferMB = DefaultCircularBufferMB, CancellationToken token = default)
118121
{
119-
return EventPipeSession.StartAsync(_endpoint, new[] { provider }, requestRundown, circularBufferMB, token);
122+
EventPipeSessionConfiguration config = new(new[] {provider}, circularBufferMB, requestRundown: requestRundown, requestStackwalk: true);
123+
return EventPipeSession.StartAsync(_endpoint, config, token);
124+
}
125+
126+
/// <summary>
127+
/// Start tracing the application and return an EventPipeSession object
128+
/// </summary>
129+
/// <param name="configuration">Configuration of this EventPipeSession</param>
130+
/// <param name="token">The token to monitor for cancellation requests.</param>
131+
/// <returns>
132+
/// An EventPipeSession object representing the EventPipe session that just started.
133+
/// </returns>
134+
public Task<EventPipeSession> StartEventPipeSessionAsync(EventPipeSessionConfiguration configuration, CancellationToken token)
135+
{
136+
return EventPipeSession.StartAsync(_endpoint, configuration, token);
120137
}
121138

122139
/// <summary>

src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSession.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@ private EventPipeSession(IpcEndpoint endpoint, IpcResponse response, ulong sessi
2828

2929
public Stream EventStream => _response.Continuation;
3030

31-
internal static EventPipeSession Start(IpcEndpoint endpoint, IEnumerable<EventPipeProvider> providers, bool requestRundown, int circularBufferMB)
31+
internal static EventPipeSession Start(IpcEndpoint endpoint, EventPipeSessionConfiguration config)
3232
{
33-
IpcMessage requestMessage = CreateStartMessage(providers, requestRundown, circularBufferMB);
33+
IpcMessage requestMessage = CreateStartMessage(config);
3434
IpcResponse? response = IpcClient.SendMessageGetContinuation(endpoint, requestMessage);
3535
return CreateSessionFromResponse(endpoint, ref response, nameof(Start));
3636
}
3737

38-
internal static async Task<EventPipeSession> StartAsync(IpcEndpoint endpoint, IEnumerable<EventPipeProvider> providers, bool requestRundown, int circularBufferMB, CancellationToken cancellationToken)
38+
internal static async Task<EventPipeSession> StartAsync(IpcEndpoint endpoint, EventPipeSessionConfiguration config, CancellationToken cancellationToken)
3939
{
40-
IpcMessage requestMessage = CreateStartMessage(providers, requestRundown, circularBufferMB);
40+
IpcMessage requestMessage = CreateStartMessage(config);
4141
IpcResponse? response = await IpcClient.SendMessageGetContinuationAsync(endpoint, requestMessage, cancellationToken).ConfigureAwait(false);
4242
return CreateSessionFromResponse(endpoint, ref response, nameof(StartAsync));
4343
}
@@ -81,10 +81,14 @@ public async Task StopAsync(CancellationToken cancellationToken)
8181
}
8282
}
8383

84-
private static IpcMessage CreateStartMessage(IEnumerable<EventPipeProvider> providers, bool requestRundown, int circularBufferMB)
84+
private static IpcMessage CreateStartMessage(EventPipeSessionConfiguration config)
8585
{
86-
EventPipeSessionConfiguration config = new(circularBufferMB, EventPipeSerializationFormat.NetTrace, providers, requestRundown);
87-
return new IpcMessage(DiagnosticsServerCommandSet.EventPipe, (byte)EventPipeCommandId.CollectTracing2, config.SerializeV2());
86+
// To keep backward compatibility with older runtimes we only use newer serialization format when needed
87+
// V3 has added support to disable the stacktraces
88+
bool shouldUseV3 = !config.RequestStackwalk;
89+
EventPipeCommandId command = shouldUseV3 ? EventPipeCommandId.CollectTracing3 : EventPipeCommandId.CollectTracing2;
90+
byte[] payload = shouldUseV3 ? config.SerializeV3() : config.SerializeV2();
91+
return new IpcMessage(DiagnosticsServerCommandSet.EventPipe, (byte)command, payload);
8892
}
8993

9094
private static EventPipeSession CreateSessionFromResponse(IpcEndpoint endpoint, ref IpcResponse? response, string operationName)

src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSessionConfiguration.cs

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.IO;
7+
using System.Linq;
78

89
namespace Microsoft.Diagnostics.NETCore.Client
910
{
@@ -13,9 +14,29 @@ internal enum EventPipeSerializationFormat
1314
NetTrace
1415
}
1516

16-
internal class EventPipeSessionConfiguration
17+
public sealed class EventPipeSessionConfiguration
1718
{
18-
public EventPipeSessionConfiguration(int circularBufferSizeMB, EventPipeSerializationFormat format, IEnumerable<EventPipeProvider> providers, bool requestRundown = true)
19+
/// <summary>
20+
/// Creates a new configuration object for the EventPipeSession.
21+
/// For details, see the documentation of each property of this object.
22+
/// </summary>
23+
/// <param name="providers">An IEnumerable containing the list of Providers to turn on.</param>
24+
/// <param name="circularBufferSizeMB">The size of the runtime's buffer for collecting events in MB</param>
25+
/// <param name="requestRundown">If true, request rundown events from the runtime.</param>
26+
/// <param name="requestStackwalk">If true, record a stacktrace for every emitted event.</param>
27+
public EventPipeSessionConfiguration(
28+
IEnumerable<EventPipeProvider> providers,
29+
int circularBufferSizeMB = 256,
30+
bool requestRundown = true,
31+
bool requestStackwalk = true) : this(circularBufferSizeMB, EventPipeSerializationFormat.NetTrace, providers, requestRundown, requestStackwalk)
32+
{}
33+
34+
private EventPipeSessionConfiguration(
35+
int circularBufferSizeMB,
36+
EventPipeSerializationFormat format,
37+
IEnumerable<EventPipeProvider> providers,
38+
bool requestRundown,
39+
bool requestStackwalk)
1940
{
2041
if (circularBufferSizeMB == 0)
2142
{
@@ -35,36 +56,60 @@ public EventPipeSessionConfiguration(int circularBufferSizeMB, EventPipeSerializ
3556
CircularBufferSizeInMB = circularBufferSizeMB;
3657
Format = format;
3758
RequestRundown = requestRundown;
59+
RequestStackwalk = requestStackwalk;
3860
_providers = new List<EventPipeProvider>(providers);
3961
}
4062

63+
/// <summary>
64+
/// If true, request rundown events from the runtime.
65+
/// <list type="bullet">
66+
/// <item>Rundown events are needed to correctly decode the stacktrace information for dynamically generated methods.</item>
67+
/// <item>Rundown happens at the end of the session. It increases the time needed to finish the session and, for large applications, may have important impact on the final trace file size.</item>
68+
/// <item>Consider to set this parameter to false if you don't need stacktrace information or if you're analyzing events on the fly.</item>
69+
/// </list>
70+
/// </summary>
4171
public bool RequestRundown { get; }
72+
73+
/// <summary>
74+
/// The size of the runtime's buffer for collecting events in MB.
75+
/// If the buffer size is too small to accommodate all in-flight events some events may be lost.
76+
/// </summary>
4277
public int CircularBufferSizeInMB { get; }
43-
public EventPipeSerializationFormat Format { get; }
4478

79+
/// <summary>
80+
/// If true, record a stacktrace for every emitted event.
81+
/// <list type="bullet">
82+
/// <item>The support of this parameter only comes with NET 9. Before, the stackwalk is always enabled and if this property is set to false the connection attempt will fail.</item>
83+
/// <item>Disabling the stackwalk makes event collection overhead considerably less</item>
84+
/// <item>Note that some events may choose to omit the stacktrace regardless of this parameter, specifically the events emitted from the native runtime code.</item>
85+
/// <item>If the stacktrace collection is disabled application-wide (using the env variable <c>DOTNET_EventPipeEnableStackwalk</c>) this parameter is ignored.</item>
86+
/// </list>
87+
/// </summary>
88+
public bool RequestStackwalk { get; }
89+
90+
/// <summary>
91+
/// Providers to enable for this session.
92+
/// </summary>
4593
public IReadOnlyCollection<EventPipeProvider> Providers => _providers.AsReadOnly();
4694

4795
private readonly List<EventPipeProvider> _providers;
4896

49-
public byte[] SerializeV2()
97+
internal EventPipeSerializationFormat Format { get; }
98+
}
99+
100+
internal static class EventPipeSessionConfigurationExtensions
101+
{
102+
public static byte[] SerializeV2(this EventPipeSessionConfiguration config)
50103
{
51104
byte[] serializedData = null;
52105
using (MemoryStream stream = new())
53106
using (BinaryWriter writer = new(stream))
54107
{
55-
writer.Write(CircularBufferSizeInMB);
56-
writer.Write((uint)Format);
57-
writer.Write(RequestRundown);
58-
59-
writer.Write(Providers.Count);
60-
foreach (EventPipeProvider provider in Providers)
61-
{
62-
writer.Write(unchecked((ulong)provider.Keywords));
63-
writer.Write((uint)provider.EventLevel);
108+
writer.Write(config.CircularBufferSizeInMB);
109+
writer.Write((uint)config.Format);
110+
writer.Write(config.RequestRundown);
64111

65-
writer.WriteString(provider.Name);
66-
writer.WriteString(provider.GetArgumentString());
67-
}
112+
SerializeProviders(config, writer);
68113

69114
writer.Flush();
70115
serializedData = stream.ToArray();
@@ -73,6 +118,36 @@ public byte[] SerializeV2()
73118
return serializedData;
74119
}
75120

121+
public static byte[] SerializeV3(this EventPipeSessionConfiguration config)
122+
{
123+
byte[] serializedData = null;
124+
using (MemoryStream stream = new())
125+
using (BinaryWriter writer = new(stream))
126+
{
127+
writer.Write(config.CircularBufferSizeInMB);
128+
writer.Write((uint)config.Format);
129+
writer.Write(config.RequestRundown);
130+
writer.Write(config.RequestStackwalk);
131+
132+
SerializeProviders(config, writer);
76133

134+
writer.Flush();
135+
serializedData = stream.ToArray();
136+
}
137+
138+
return serializedData;
139+
}
140+
141+
private static void SerializeProviders(EventPipeSessionConfiguration config, BinaryWriter writer)
142+
{
143+
writer.Write(config.Providers.Count);
144+
foreach (EventPipeProvider provider in config.Providers)
145+
{
146+
writer.Write(unchecked((ulong)provider.Keywords));
147+
writer.Write((uint)provider.EventLevel);
148+
writer.WriteString(provider.Name);
149+
writer.WriteString(provider.GetArgumentString());
150+
}
151+
}
77152
}
78153
}

src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ internal enum EventPipeCommandId : byte
2525
StopTracing = 0x01,
2626
CollectTracing = 0x02,
2727
CollectTracing2 = 0x03,
28+
CollectTracing3 = 0x04,
2829
}
2930

3031
internal enum DumpCommandId : byte

src/tests/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClientApiShim.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,17 @@ public async Task ResumeRuntime(TimeSpan timeout)
6363
}
6464
}
6565

66+
public async Task<EventPipeSession> StartEventPipeSession(EventPipeSessionConfiguration config, TimeSpan timeout)
67+
{
68+
if (_useAsync)
69+
{
70+
CancellationTokenSource cancellation = new(timeout);
71+
return await _client.StartEventPipeSessionAsync(config, cancellation.Token).ConfigureAwait(false);
72+
}
73+
74+
throw new NotSupportedException($"{nameof(StartEventPipeSession)} with config parameter is only supported on async path");
75+
}
76+
6677
public async Task<EventPipeSession> StartEventPipeSession(IEnumerable<EventPipeProvider> providers, TimeSpan timeout)
6778
{
6879
if (_useAsync)

src/tests/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClientApiShimExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ public static Task<EventPipeSession> StartEventPipeSession(this DiagnosticsClien
3737
return shim.StartEventPipeSession(provider, DefaultPositiveVerificationTimeout);
3838
}
3939

40+
public static Task<EventPipeSession> StartEventPipeSession(this DiagnosticsClientApiShim shim, EventPipeSessionConfiguration config)
41+
{
42+
return shim.StartEventPipeSession(config, DefaultPositiveVerificationTimeout);
43+
}
44+
4045
public static Task EnablePerfMap(this DiagnosticsClientApiShim shim, PerfMapType type)
4146
{
4247
return shim.EnablePerfMap(type, DefaultPositiveVerificationTimeout);

0 commit comments

Comments
 (0)