Skip to content

Commit 5baeadb

Browse files
wiktorkjander-msft
andauthored
Support resetting state when connecting to a previously monitored app (#8154)
* Support resetting state when connecting to a previously monitored app * Fix switch build issue * Fix unit tests * PR Feedback * Add exception startup collection enablement * Fix env check * PR feedback * PR feedback * Use an interlocked many reader single writer to synchronize exceptions threads and pipeline stop calls * Fixup PR tests * PR feedback * PR feedback Co-authored-by: Justin Anderson <[email protected]> * Fixup schema * Unit test * (test) --------- Co-authored-by: Justin Anderson <[email protected]>
1 parent 19f6b56 commit 5baeadb

File tree

31 files changed

+520
-81
lines changed

31 files changed

+520
-81
lines changed

cspell.json

+2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"hresult",
5656
"Hsts",
5757
"Impls",
58+
"inproc",
5859
"JITID",
5960
"JWTs",
6061
"LCID",
@@ -107,6 +108,7 @@
107108
"ukwn",
108109
"uninitialize",
109110
"Uninstallation",
111+
"uninstrumented",
110112
"uniquifier",
111113
"Unlocalized",
112114
"Unredacted",

documentation/schema.json

+8
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,14 @@
11231123
"$ref": "#/definitions/ExceptionsConfiguration"
11241124
}
11251125
]
1126+
},
1127+
"CollectOnStartup": {
1128+
"type": [
1129+
"boolean",
1130+
"null"
1131+
],
1132+
"description": "[Experimental] Determines if exception collection should begin immediately",
1133+
"default": true
11261134
}
11271135
}
11281136
},

src/Microsoft.Diagnostics.Monitoring.Options/ExceptionsOptions.cs

+6
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,11 @@ public sealed class ExceptionsOptions :
2727
ResourceType = typeof(OptionsDisplayStrings),
2828
Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_ExceptionsOptions_CollectionFilters))]
2929
public ExceptionsConfiguration? CollectionFilters { get; set; }
30+
31+
[Display(
32+
ResourceType = typeof(OptionsDisplayStrings),
33+
Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_ExceptionsOptions_CollectOnStartup))]
34+
[DefaultValue(ExceptionsOptionsDefaults.CollectOnStartup)]
35+
public bool? CollectOnStartup { get; set; }
3036
}
3137
}

src/Microsoft.Diagnostics.Monitoring.Options/ExceptionsOptionsDefaults.cs

+2
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ internal static class ExceptionsOptionsDefaults
88
public const bool Enabled = true;
99

1010
public const int TopLevelLimit = 20;
11+
12+
public const bool CollectOnStartup = true;
1113
}
1214
}

src/Microsoft.Diagnostics.Monitoring.Options/ExceptionsOptionsExtensions.cs

+5
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,10 @@ public static int GetTopLevelLimit(this ExceptionsOptions options)
1414
{
1515
return options.TopLevelLimit.GetValueOrDefault(ExceptionsOptionsDefaults.TopLevelLimit);
1616
}
17+
18+
public static bool CollectOnStartup(this ExceptionsOptions options)
19+
{
20+
return options.CollectOnStartup.GetValueOrDefault(ExceptionsOptionsDefaults.CollectOnStartup);
21+
}
1722
}
1823
}

src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx

+3
Original file line numberDiff line numberDiff line change
@@ -780,4 +780,7 @@
780780
<value>Performs a match based on the name of the managed entry point assembly of the process.</value>
781781
<comment>The description provided for the ManagedEntryPointAssemblyName parameter on ProcessFilterDescriptor.</comment>
782782
</data>
783+
<data name="DisplayAttributeDescription_ExceptionsOptions_CollectOnStartup" xml:space="preserve">
784+
<value>[Experimental] Determines if exception collection should begin immediately</value>
785+
</data>
783786
</root>

src/Microsoft.Diagnostics.Monitoring.StartupHook/DiagnosticsBootstrapper.cs

+18-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ internal sealed class DiagnosticsBootstrapper :
2323
public DiagnosticsBootstrapper()
2424
{
2525
_exceptionProcessor = new(ToolIdentifiers.IsEnvVarEnabled(InProcessFeaturesIdentifiers.EnvironmentVariables.Exceptions.IncludeMonitorExceptions));
26-
_exceptionProcessor.Start();
26+
27+
// If collectOnStart is not set, assume the default value (true).
28+
string? collectOnStart = Environment.GetEnvironmentVariable(InProcessFeaturesIdentifiers.EnvironmentVariables.Exceptions.CollectOnStartup);
29+
if (collectOnStart == null || ToolIdentifiers.IsEnvVarValueEnabled(collectOnStart))
30+
{
31+
_exceptionProcessor.Start();
32+
}
2733

2834
using IDisposable _ = MonitorExecutionContextTracker.MonitorScope();
2935

@@ -35,6 +41,17 @@ public DiagnosticsBootstrapper()
3541
SharedInternals.MessageDispatcher = new MessageDispatcher.MonitorMessageDispatcher(
3642
new MessageDispatcher.ProfilerMessageSource(CommandSet.StartupHook));
3743
ToolIdentifiers.EnableEnvVar(InProcessFeaturesIdentifiers.EnvironmentVariables.AvailableInfrastructure.ManagedMessaging);
44+
45+
SharedInternals.MessageDispatcher.RegisterCallback<EmptyPayload>(StartupHookCommand.StopAllFeatures, (IpcMessage) =>
46+
{
47+
_exceptionProcessor.Stop();
48+
_parameterCapturingService?.RequestStopAll();
49+
});
50+
51+
SharedInternals.MessageDispatcher.RegisterCallback<EmptyPayload>(StartupHookCommand.StartAllFeatures, (IpcMessage) =>
52+
{
53+
_exceptionProcessor.Start();
54+
});
3855
}
3956

4057
if (ToolIdentifiers.IsEnvVarEnabled(InProcessFeaturesIdentifiers.EnvironmentVariables.ParameterCapturing.Enable))

src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/CurrentAppDomainExceptionProcessor.cs

+18-2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ internal sealed class CurrentAppDomainExceptionProcessor : IDisposable
1414
private readonly ExceptionIdSource _idSource = new();
1515

1616
private readonly CurrentAppDomainFirstChanceExceptionSource _firstChanceSource;
17-
private readonly ExceptionPipeline _firstChancePipeline;
17+
private ExceptionPipeline _firstChancePipeline;
1818

1919
private readonly CurrentAppDomainUnhandledExceptionSource _unhandledSource;
20-
private readonly ExceptionPipeline _unhandledPipeline;
20+
private ExceptionPipeline _unhandledPipeline;
2121

2222
private readonly bool _includeMonitorExceptions;
2323

@@ -35,7 +35,23 @@ public CurrentAppDomainExceptionProcessor(bool includeMonitorExceptions)
3535
public void Start()
3636
{
3737
_firstChancePipeline.Start();
38+
_firstChanceSource.Start();
39+
3840
_unhandledPipeline.Start();
41+
_unhandledSource.Start();
42+
}
43+
44+
public void Stop()
45+
{
46+
// Stop all exception flow from the sources. This will allow draining the pipelines.
47+
_firstChanceSource.Stop();
48+
_unhandledSource.Stop();
49+
_firstChancePipeline.Stop();
50+
_unhandledPipeline.Stop();
51+
52+
// We reset the pipelines entirely to allow their cache to be cleared
53+
_firstChancePipeline = new(_firstChanceSource, ConfigureFirstChancePipeline);
54+
_unhandledPipeline = new(_unhandledSource, ConfigureUnhandledPipeline);
3955
}
4056

4157
private void ConfigureFirstChancePipeline(ExceptionPipelineBuilder builder)

src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/CurrentAppDomainFirstChanceExceptionSource.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@ internal sealed class CurrentAppDomainFirstChanceExceptionSource :
1515
{
1616
private long _disposedState;
1717

18-
public CurrentAppDomainFirstChanceExceptionSource()
18+
public override void Start()
1919
{
2020
AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
2121
}
2222

23+
public override void Stop()
24+
{
25+
AppDomain.CurrentDomain.FirstChanceException -= CurrentDomain_FirstChanceException;
26+
}
27+
2328
private void CurrentDomain_FirstChanceException(object? sender, FirstChanceExceptionEventArgs e)
2429
{
2530
RaiseExceptionGuarded(e.Exception);

src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/CurrentAppDomainUnhandledExceptionSource.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@ internal sealed class CurrentAppDomainUnhandledExceptionSource :
1414
{
1515
private long _disposedState;
1616

17-
public CurrentAppDomainUnhandledExceptionSource()
17+
public override void Start()
1818
{
1919
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
2020
}
2121

22+
public override void Stop()
23+
{
24+
AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException;
25+
}
26+
2227
private void CurrentDomain_UnhandledException(object? sender, UnhandledExceptionEventArgs e)
2328
{
2429
if (e.ExceptionObject is Exception exception)

src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/ExceptionSourceBase.cs

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ protected void RaiseException(Exception ex, DateTime timestamp, string? activity
1616
ExceptionAvailable?.Invoke(this, new ExceptionAvailableEventArgs(ex, timestamp, activityId, format));
1717
}
1818

19+
public virtual void Start() { }
20+
21+
public virtual void Stop() { }
22+
1923
/// <summary>
2024
/// Event that is raised each time an exception is made available.
2125
/// </summary>

src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/Pipeline/ExceptionPipeline.cs

+58-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Threading;
56

67
namespace Microsoft.Diagnostics.Monitoring.StartupHook.Exceptions.Pipeline
78
{
@@ -13,6 +14,9 @@ internal sealed class ExceptionPipeline :
1314

1415
private long _disposedState;
1516

17+
private long _processingState;
18+
private const long WriteFlag = unchecked((long)0x8000000000000000);
19+
1620
public ExceptionPipeline(ExceptionSourceBase exceptionSource, Action<ExceptionPipelineBuilder> configure)
1721
{
1822
ArgumentNullException.ThrowIfNull(exceptionSource);
@@ -28,10 +32,25 @@ public ExceptionPipeline(ExceptionSourceBase exceptionSource, Action<ExceptionPi
2832
public void Start()
2933
{
3034
DisposableHelper.ThrowIfDisposed<ExceptionPipeline>(ref _disposedState);
35+
_processingState = 0;
3136

3237
_exceptionSource.ExceptionAvailable += ExceptionSource_ExceptionAvailable;
3338
}
3439

40+
public void Stop()
41+
{
42+
// We must do this first to prevent any new exception callbacks.
43+
_exceptionSource.ExceptionAvailable -= ExceptionSource_ExceptionAvailable;
44+
45+
// Wait until all outstanding exception callbacks are finished. Once _processingState reaches 0, we will set the WriteFlag.
46+
// This will prevent any further exception handlers from processing.
47+
while (0 != Interlocked.CompareExchange(ref _processingState, WriteFlag, 0) && !DisposableHelper.IsDisposed(ref _disposedState))
48+
{
49+
// Wait for all exceptions to be processed
50+
Thread.Sleep(100);
51+
}
52+
}
53+
3554
private void ExceptionSource_ExceptionAvailable(object? sender, ExceptionAvailableEventArgs args)
3655
{
3756
// DESIGN: While async patterns are typically favored over synchronous patterns,
@@ -40,12 +59,45 @@ private void ExceptionSource_ExceptionAvailable(object? sender, ExceptionAvailab
4059
// (e.g. EventSource provides events but diagnostic pipe events are queued and asynchronously emitted).
4160
// Synchronous execution is required for scenarios where the exception needs to be held
4261
// at the site of where it is thrown before allowing it to unwind (e.g. capturing a dump of the exception).
43-
_exceptionHandler.Invoke(
44-
args.Exception,
45-
new ExceptionPipelineExceptionContext(
46-
args.Timestamp,
47-
args.ActivityId,
48-
args.ActivityIdFormat));
62+
63+
SpinWait spinWait = new();
64+
65+
while (true)
66+
{
67+
long state = Interlocked.Read(ref _processingState);
68+
if ((state & WriteFlag) == WriteFlag)
69+
{
70+
// Stop has been requested but the event handler has already occurred. Return early.
71+
return;
72+
}
73+
if (DisposableHelper.IsDisposed(ref _disposedState))
74+
{
75+
return;
76+
}
77+
// Increment the value by 1 to indicate that we are doing work.
78+
// We cannot do a simple increment since it's possible for a Stop call to attempt to set the write bit
79+
// at this stage.
80+
if (Interlocked.CompareExchange(ref _processingState, state + 1, state) == state)
81+
{
82+
break;
83+
}
84+
spinWait.SpinOnce();
85+
}
86+
87+
try
88+
{
89+
_exceptionHandler.Invoke(
90+
args.Exception,
91+
new ExceptionPipelineExceptionContext(
92+
args.Timestamp,
93+
args.ActivityId,
94+
args.ActivityIdFormat));
95+
96+
}
97+
finally
98+
{
99+
Interlocked.Decrement(ref _processingState);
100+
}
49101
}
50102

51103

src/Microsoft.Diagnostics.Monitoring.StartupHook/ParameterCapturing/ParameterCapturingService.cs

+5
Original file line numberDiff line numberDiff line change
@@ -220,5 +220,10 @@ public override void Dispose()
220220

221221
base.Dispose();
222222
}
223+
224+
public void RequestStopAll()
225+
{
226+
_pipeline?.RequestStopAll();
227+
}
223228
}
224229
}

src/Microsoft.Diagnostics.Monitoring.StartupHook/ParameterCapturing/Pipeline/ParameterCapturingPipeline.cs

+11
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,17 @@ public void SubmitRequest(StartCapturingParametersPayload payload, IFunctionProb
206206
}
207207
}
208208

209+
public void RequestStopAll()
210+
{
211+
foreach (var key in _allRequests.Keys)
212+
{
213+
if (_allRequests.TryRemove(key, out CapturingRequest? request))
214+
{
215+
request?.StopRequest?.TrySetResult();
216+
}
217+
}
218+
}
219+
209220
public void RequestStop(Guid requestId)
210221
{
211222
if (!_allRequests.TryGetValue(requestId, out CapturingRequest? request))

src/Microsoft.Diagnostics.Monitoring.WebApi/IInProcessFeatures.cs

+2
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,7 @@ public interface IInProcessFeatures
1111
bool IsStartupHookRequired { get; }
1212

1313
bool IsLibrarySharingRequired { get; }
14+
15+
bool CollectExceptionsOnStartup { get; }
1416
}
1517
}

src/Microsoft.Diagnostics.Monitoring.WebApi/ProfilerChannel.cs

+7
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ public ProfilerChannel(IOptionsMonitor<StorageOptions> storageOptions)
2727
_storageOptions = storageOptions;
2828
}
2929

30+
public async Task SendMessage(IEndpointInfo endpointInfo, IProfilerMessage message, CancellationToken token, TimeSpan timeout)
31+
{
32+
using CancellationTokenSource cancellationTokenSource = new(timeout);
33+
using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(token, cancellationTokenSource.Token);
34+
await SendMessage(endpointInfo, message, linkedCts.Token);
35+
}
36+
3037
public async Task SendMessage(IEndpointInfo endpointInfo, IProfilerMessage message, CancellationToken token)
3138
{
3239
if (message.Payload.Length > MaxPayloadSize)

src/Microsoft.Diagnostics.Monitoring.WebApi/ProfilerMessage.cs

+6-2
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@ public enum ServerResponseCommand : ushort
2626

2727
public enum ProfilerCommand : ushort
2828
{
29-
Callstack
29+
Callstack,
30+
StopAllFeatures,
31+
StartAllFeatures,
3032
};
3133

3234
public enum StartupHookCommand : ushort
3335
{
3436
StartCapturingParameters,
35-
StopCapturingParameters
37+
StopCapturingParameters,
38+
StopAllFeatures,
39+
StartAllFeatures,
3640
};
3741

3842
public interface IProfilerMessage

0 commit comments

Comments
 (0)