Skip to content

Commit 9e7ee56

Browse files
committed
Improve connection and disconnection from EventPipe session
1 parent 0431ed3 commit 9e7ee56

8 files changed

+313
-226
lines changed
+278
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
// Copyright (c) Alexandre Mutel. All rights reserved.
2+
// Licensed under the BSD-Clause 2 license.
3+
// See license.txt file in the project root for full license information.
4+
5+
using System.Diagnostics;
6+
using System.Diagnostics.Tracing;
7+
using System.Net.Sockets;
8+
using Microsoft.Diagnostics.NETCore.Client;
9+
using Microsoft.Diagnostics.Tracing.Parsers;
10+
using Ultra.Sampler;
11+
12+
namespace Ultra.Core;
13+
14+
/// <summary>
15+
/// Handles an EventPipe session to a diagnostic port.
16+
/// </summary>
17+
internal class DiagnosticPortSession
18+
{
19+
private readonly int _pid;
20+
private readonly bool _sampler;
21+
private readonly string _baseName;
22+
private readonly Task _connectTask;
23+
private readonly object _sessionLock = new();
24+
private Task? _profilingTask;
25+
private readonly CancellationTokenSource _cancelConnectSource;
26+
private DiagnosticsClient? _diagnosticsClient;
27+
private EventPipeSession? _eventPipeSession;
28+
private string? _nettraceFilePath;
29+
private FileStream? _nettraceFileStream;
30+
private Task? _eventStreamCopyTask;
31+
private bool _disposed;
32+
33+
public DiagnosticPortSession(int pid, bool sampler, string baseName, CancellationToken token)
34+
{
35+
_pid = pid;
36+
_sampler = sampler;
37+
_baseName = baseName;
38+
_cancelConnectSource = new CancellationTokenSource();
39+
_connectTask = ConnectAndStartProfilingImpl(pid, sampler, baseName, token);
40+
}
41+
42+
private async Task ConnectAndStartProfilingImpl(int pid, bool sampler, string baseName, CancellationToken token)
43+
{
44+
CancellationTokenSource linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, _cancelConnectSource.Token);
45+
try
46+
{
47+
48+
var connectCancellationToken = linkedCancellationTokenSource.Token;
49+
50+
if (sampler)
51+
{
52+
_cancelConnectSource.CancelAfter(500);
53+
}
54+
55+
var connectionAddress = await TryFindConnectionAddress(pid, sampler, connectCancellationToken).ConfigureAwait(false);
56+
if (connectionAddress is null) return;
57+
58+
_diagnosticsClient = (await DiagnosticsClientConnector.FromDiagnosticPort(connectionAddress, connectCancellationToken).ConfigureAwait(false))?.Instance;
59+
if (_diagnosticsClient is null) return;
60+
61+
await _diagnosticsClient.WaitForConnectionAsync(connectCancellationToken).ConfigureAwait(false);
62+
}
63+
catch (OperationCanceledException ex)
64+
{
65+
if (sampler && _cancelConnectSource is not null && _cancelConnectSource.IsCancellationRequested)
66+
{
67+
throw new InvalidOperationException($"Cannot connect to the diagnostic port socket for pid {pid}", ex);
68+
}
69+
return;
70+
}
71+
finally
72+
{
73+
linkedCancellationTokenSource.Dispose();
74+
}
75+
}
76+
77+
public void StartProfiling(CancellationToken token)
78+
{
79+
// We want to make sure that we are not disposing while we are connecting
80+
Monitor.Enter(_sessionLock);
81+
try
82+
{
83+
if (_disposed)
84+
{
85+
return;
86+
}
87+
88+
_profilingTask = _connectTask.ContinueWith(async task =>
89+
{
90+
91+
_nettraceFilePath = Path.Combine(Environment.CurrentDirectory, $"{_baseName}_{(_sampler ? "sampler" : "main")}_{_pid}.nettrace");
92+
_nettraceFileStream = new FileStream(_nettraceFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, 65536, FileOptions.Asynchronous);
93+
94+
long keywords = -1;
95+
var providerName = UltraSamplerParser.Name;
96+
var level = EventLevel.Verbose;
97+
98+
if (!_sampler)
99+
{
100+
providerName = "Microsoft-Windows-DotNETRuntime";
101+
keywords = (long)(
102+
ClrTraceEventParser.Keywords.JITSymbols |
103+
ClrTraceEventParser.Keywords.Exception |
104+
ClrTraceEventParser.Keywords.GC |
105+
ClrTraceEventParser.Keywords.GCHeapAndTypeNames |
106+
ClrTraceEventParser.Keywords.Interop |
107+
ClrTraceEventParser.Keywords.JITSymbols |
108+
ClrTraceEventParser.Keywords.Jit |
109+
ClrTraceEventParser.Keywords.JittedMethodILToNativeMap |
110+
ClrTraceEventParser.Keywords.Loader |
111+
ClrTraceEventParser.Keywords.Stack |
112+
ClrTraceEventParser.Keywords.StartEnumeration
113+
);
114+
}
115+
116+
var ultraEventProvider = new EventPipeProvider(providerName, level, (long)keywords);
117+
var config = new EventPipeSessionConfiguration([ultraEventProvider], 512, !_sampler, true);
118+
_eventPipeSession = await _diagnosticsClient!.StartEventPipeSessionAsync(config, token).ConfigureAwait(false);
119+
_eventStreamCopyTask = _eventPipeSession.EventStream.CopyToAsync(_nettraceFileStream, token);
120+
}, token);
121+
}
122+
finally
123+
{
124+
Monitor.Exit(_sessionLock);
125+
}
126+
}
127+
128+
public long GetNettraceFileLength() => _nettraceFileStream?.Length ?? 0;
129+
130+
public async Task WaitForConnectAndStartSession()
131+
{
132+
await _connectTask.ConfigureAwait(false);
133+
}
134+
135+
private static async Task<string?> TryFindConnectionAddress(int pid, bool sampler, CancellationToken token)
136+
{
137+
var tempFolder = Path.GetTempPath();
138+
tempFolder = sampler ? Path.Combine(tempFolder, ".ultra") : tempFolder;
139+
140+
var pattern = $"dotnet-diagnostic-{pid}-*-socket";
141+
string? diagnosticPortSocket = null;
142+
143+
while (true)
144+
{
145+
if (Directory.Exists(tempFolder))
146+
{
147+
DateTime lastWriteTime = default;
148+
foreach (var file in Directory.EnumerateFiles(tempFolder, pattern))
149+
{
150+
var fileInfo = new FileInfo(file);
151+
if (fileInfo.LastWriteTime > lastWriteTime)
152+
{
153+
diagnosticPortSocket = file;
154+
lastWriteTime = fileInfo.LastWriteTime;
155+
}
156+
}
157+
158+
if (diagnosticPortSocket != null)
159+
{
160+
// Force connect mode
161+
diagnosticPortSocket = $"{diagnosticPortSocket},connect";
162+
break;
163+
}
164+
}
165+
166+
await Task.Delay(10, token).ConfigureAwait(false);
167+
}
168+
169+
return diagnosticPortSocket;
170+
}
171+
172+
public async ValueTask StopAndDisposeAsync()
173+
{
174+
Monitor.Enter(_sessionLock);
175+
try
176+
{
177+
if (_profilingTask is null)
178+
{
179+
// We cancel any pending connection
180+
await _cancelConnectSource.CancelAsync();
181+
}
182+
else
183+
{
184+
try
185+
{
186+
// We wait for the session to start (we will close it right after below
187+
await _profilingTask.ConfigureAwait(false);
188+
}
189+
catch
190+
{
191+
// Ignore
192+
}
193+
}
194+
195+
Debug.Assert(_eventStreamCopyTask is not null);
196+
try
197+
{
198+
await _eventStreamCopyTask.ConfigureAwait(false);
199+
}
200+
catch
201+
{
202+
// Ignore
203+
}
204+
205+
Debug.Assert(_nettraceFileStream is not null);
206+
try
207+
{
208+
await _nettraceFileStream.DisposeAsync().ConfigureAwait(false);
209+
}
210+
catch
211+
{
212+
// Ignore
213+
}
214+
215+
Debug.Assert(_eventPipeSession is not null);
216+
try
217+
{
218+
await _eventPipeSession.StopAsync(CancellationToken.None).ConfigureAwait(false);
219+
}
220+
catch
221+
{
222+
// Ignore
223+
}
224+
finally
225+
{
226+
try
227+
{
228+
_eventPipeSession.Dispose();
229+
}
230+
catch
231+
{
232+
// Ignore
233+
}
234+
}
235+
}
236+
finally
237+
{
238+
_disposed = true;
239+
Monitor.Exit(_sessionLock);
240+
241+
_cancelConnectSource.Dispose();
242+
}
243+
}
244+
245+
private async Task StopAsync(CancellationToken token)
246+
{
247+
if (_eventPipeSession is null) return;
248+
249+
try
250+
{
251+
await _eventPipeSession.StopAsync(token).ConfigureAwait(false);
252+
}
253+
catch (EndOfStreamException)
254+
{
255+
256+
}
257+
catch (TimeoutException)
258+
{
259+
260+
}
261+
catch (OperationCanceledException)
262+
{
263+
264+
}
265+
catch (PlatformNotSupportedException)
266+
{
267+
268+
}
269+
catch (ServerNotAvailableException)
270+
{
271+
272+
}
273+
catch (SocketException)
274+
{
275+
276+
}
277+
}
278+
}

src/Ultra.Core/Ultra.Core.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
<Link>libUltraSampler.dylib</Link>
5050
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
5151
</Content>
52-
<Content Include="..\Ultra.Sampler\libUltraSamplerIndirect.dyld">
53-
<Link>libUltraSamplerIndirect.dyld</Link>
52+
<Content Include="..\Ultra.Sampler\libUltraSamplerHook.dylib">
53+
<Link>libUltraSamplerHook.dylib</Link>
5454
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
5555
</Content>
5656
</ItemGroup>

0 commit comments

Comments
 (0)