-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow multiple EventCounters sessions and make sure EventListeners always send a Disable command #82970
Allow multiple EventCounters sessions and make sure EventListeners always send a Disable command #82970
Changes from all commits
8d03d71
532d85e
ffab8e6
a47450a
84654a6
c945d04
0f76e59
0313b10
664ae9c
5b39139
b1e3a66
f826cbc
9612947
5249e7f
df69115
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// 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 System.Text; | ||
using System.Threading.Tasks; | ||
using Xunit; | ||
#if USE_MDT_EVENTSOURCE | ||
using Microsoft.Diagnostics.Tracing; | ||
#else | ||
using System.Diagnostics.Tracing; | ||
#endif | ||
using System.Reflection; | ||
|
||
namespace BasicEventSourceTests | ||
{ | ||
public class TestsEventSourceCallbacks | ||
{ | ||
/// <summary> | ||
/// Validates that the EventProvider AppDomain.ProcessExit handler does not keep the EventProvider instance | ||
/// alive. | ||
/// </summary> | ||
[Fact] | ||
public void Test_EventSource_Lifetime() | ||
{ | ||
using (var source = new CallbacksTestEventSource()) | ||
{ | ||
bool isDisabledInDelegate = false; | ||
source.EventCommandExecuted += (sender, args) => | ||
{ | ||
if (args.Command == EventCommand.Disable) | ||
{ | ||
EventSource eventSource = (EventSource)sender; | ||
isDisabledInDelegate = !eventSource.IsEnabled(); | ||
} | ||
}; | ||
|
||
using (var listener = new CallbacksEventListener()) | ||
{ | ||
source.Event(); | ||
} | ||
|
||
if (!source._isDisabledInCallback) | ||
{ | ||
Assert.Fail("EventSource was still enabled in OnEventCommand callback"); | ||
} | ||
|
||
if (!isDisabledInDelegate) | ||
{ | ||
Assert.Fail("EventSource was still enabled in EventCommandExecuted delegate"); | ||
} | ||
} | ||
} | ||
|
||
private class CallbacksEventListener : EventListener | ||
{ | ||
protected override void OnEventSourceCreated(EventSource eventSource) | ||
{ | ||
base.OnEventSourceCreated(eventSource); | ||
|
||
if (eventSource.Name.Equals("TestsEventSourceCallbacks.CallbacksTestEventSource")) | ||
{ | ||
EnableEvents(eventSource, EventLevel.Verbose); | ||
} | ||
} | ||
} | ||
|
||
[EventSource(Name = "TestsEventSourceCallbacks.CallbacksTestEventSource")] | ||
private class CallbacksTestEventSource : EventSource | ||
{ | ||
internal bool _isDisabledInCallback; | ||
|
||
[Event(1)] | ||
public void Event() | ||
{ | ||
WriteEvent(1); | ||
} | ||
|
||
[NonEvent] | ||
protected override void OnEventCommand(EventCommandEventArgs command) | ||
{ | ||
base.OnEventCommand(command); | ||
|
||
_isDisabledInCallback = !IsEnabled(); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2629,9 +2629,6 @@ internal void DoCommand(EventCommandEventArgs commandArgs) | |
m_eventSourceEnabled = true; | ||
} | ||
|
||
this.OnEventCommand(commandArgs); | ||
this.m_eventCommandExecuted?.Invoke(this, commandArgs); | ||
|
||
if (!commandArgs.enable) | ||
{ | ||
// If we are disabling, maybe we can turn on 'quick checks' to filter | ||
|
@@ -2663,6 +2660,9 @@ internal void DoCommand(EventCommandEventArgs commandArgs) | |
m_eventSourceEnabled = false; | ||
} | ||
} | ||
|
||
this.OnEventCommand(commandArgs); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The previous asymmetry in when the callback ran relative to when the IsEnabled() bit updated puzzled me and this certainly seems more intuitive. However its possible the previous behavior was designed to let people send events during the disable callback and now they would be unable to do so. You should probably test it. If we are lucky sending events during the Disable callback already didn't work in which case this re-ordering is probably safe. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verified that in the existing code you cannot send events during a Disable command, the event is already turned off |
||
this.m_eventCommandExecuted?.Invoke(this, commandArgs); | ||
} | ||
else | ||
{ | ||
|
@@ -4219,6 +4219,38 @@ internal static void DisposeOnShutdown() | |
} | ||
} | ||
|
||
// If an EventListener calls Dispose without calling DisableEvents first we want to issue the Disable command now | ||
private static void CallDisableEventsIfNecessary(EventDispatcher eventDispatcher, EventSource eventSource) | ||
{ | ||
#if DEBUG | ||
// Disable validation of EventSource/EventListener connections in case a call to EventSource.AddListener | ||
// causes a recursive call into this method. | ||
bool previousValue = s_ConnectingEventSourcesAndListener; | ||
s_ConnectingEventSourcesAndListener = true; | ||
try | ||
{ | ||
#endif | ||
if (eventDispatcher.m_EventEnabled == null) | ||
{ | ||
return; | ||
} | ||
|
||
for (int i = 0; i < eventDispatcher.m_EventEnabled.Length; ++i) | ||
{ | ||
if (eventDispatcher.m_EventEnabled[i]) | ||
{ | ||
eventDispatcher.m_Listener.DisableEvents(eventSource); | ||
} | ||
} | ||
#if DEBUG | ||
} | ||
finally | ||
{ | ||
s_ConnectingEventSourcesAndListener = previousValue; | ||
} | ||
#endif | ||
} | ||
|
||
/// <summary> | ||
/// Helper used in code:Dispose that removes any references to 'listenerToRemove' in any of the | ||
/// eventSources in the appdomain. | ||
|
@@ -4230,14 +4262,38 @@ private static void RemoveReferencesToListenerInEventSources(EventListener liste | |
Debug.Assert(Monitor.IsEntered(EventListener.EventListenersLock)); | ||
// Foreach existing EventSource in the appdomain | ||
Debug.Assert(s_EventSources != null); | ||
foreach (WeakReference<EventSource> eventSourceRef in s_EventSources) | ||
|
||
// First pass to call DisableEvents | ||
WeakReference<EventSource>[] eventSourcesSnapshot = s_EventSources.ToArray(); | ||
foreach (WeakReference<EventSource> eventSourceRef in eventSourcesSnapshot) | ||
{ | ||
if (eventSourceRef.TryGetTarget(out EventSource? eventSource)) | ||
{ | ||
Debug.Assert(eventSource.m_Dispatchers != null); | ||
EventDispatcher? cur = eventSource.m_Dispatchers; | ||
while (cur != null) | ||
{ | ||
if (cur.m_Listener == listenerToRemove) | ||
{ | ||
CallDisableEventsIfNecessary(cur!, eventSource); | ||
} | ||
|
||
cur = cur.m_Next; | ||
} | ||
} | ||
} | ||
|
||
// DisableEvents can call back to user code and we have to start over since s_EventSources and | ||
// eventSource.m_Dispatchers could have mutated | ||
foreach (WeakReference<EventSource> eventSourceRef in s_EventSources) | ||
{ | ||
if (eventSourceRef.TryGetTarget(out EventSource? eventSource) | ||
&& eventSource.m_Dispatchers != null) | ||
{ | ||
// Is the first output dispatcher the dispatcher we are removing? | ||
if (eventSource.m_Dispatchers.m_Listener == listenerToRemove) | ||
{ | ||
eventSource.m_Dispatchers = eventSource.m_Dispatchers.m_Next; | ||
} | ||
else | ||
{ | ||
// Remove 'listenerToRemove' from the eventSource.m_Dispatchers linked list. | ||
|
@@ -4267,6 +4323,7 @@ private static void RemoveReferencesToListenerInEventSources(EventListener liste | |
#endif // FEATURE_PERFTRACING | ||
} | ||
|
||
|
||
/// <summary> | ||
/// Checks internal consistency of EventSources/Listeners. | ||
/// </summary> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// 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.IO; | ||
using System.Diagnostics.Tracing; | ||
using System.Threading; | ||
using Tracing.Tests.Common; | ||
|
||
namespace Tracing.Tests | ||
{ | ||
[EventSource(Name = "Tracing.Tests.EnableDisableEventSource")] | ||
class EnableDisableEventSource : EventSource | ||
{ | ||
internal int _enables; | ||
internal int _disables; | ||
|
||
public EnableDisableEventSource() : base(true) { } | ||
|
||
[Event(1)] | ||
internal void TestEvent() { this.WriteEvent(1); } | ||
|
||
[NonEvent] | ||
protected override void OnEventCommand(EventCommandEventArgs command) | ||
{ | ||
base.OnEventCommand(command); | ||
|
||
if (command.Command == EventCommand.Enable) | ||
{ | ||
Interlocked.Increment(ref _enables); | ||
} | ||
else if (command.Command == EventCommand.Disable) | ||
{ | ||
Interlocked.Increment(ref _disables); | ||
} | ||
else | ||
{ | ||
throw new Exception($"Saw unexpected command {command.Command} in OnEventCommand"); | ||
} | ||
} | ||
} | ||
|
||
internal sealed class EnableDisableListener : EventListener | ||
{ | ||
private EventSource? _target; | ||
|
||
protected override void OnEventSourceCreated(EventSource source) | ||
{ | ||
if (source.Name.Equals("Tracing.Tests.EnableDisableEventSource")) | ||
{ | ||
_target = source; | ||
EnableEvents(_target, EventLevel.Verbose); | ||
} | ||
} | ||
|
||
public void Disable() | ||
{ | ||
DisableEvents(_target); | ||
} | ||
|
||
public void Dispose(bool disableEvents) | ||
{ | ||
base.Dispose(); | ||
|
||
if (disableEvents) | ||
{ | ||
// Dispose should call DisableEvents for us, this should be ignored after Dispose() | ||
Disable(); | ||
} | ||
} | ||
} | ||
|
||
class EventListenerEnableDisableTest | ||
{ | ||
static int Main() | ||
{ | ||
bool pass = false; | ||
using(var source = new EnableDisableEventSource()) | ||
{ | ||
// Testing three scenarios: | ||
// listener1 calls EnableEvents but never calls DisableEvents | ||
// listener2 calls EnableEvents and calls DisableEvents outside of Dispose | ||
// listener3 calls EnableEvents and calls DisableEvents inside of Dispose | ||
// | ||
// We should get an Enable and Disable for all of them | ||
using (var listener1 = new EnableDisableListener()) | ||
using (var listener2 = new EnableDisableListener()) | ||
{ | ||
var listener3 = new EnableDisableListener(); | ||
source.TestEvent(); | ||
listener3.Dispose(true); | ||
|
||
listener2.Disable(); | ||
} | ||
|
||
if (source._enables == 3 && source._disables == 3) | ||
{ | ||
return 100; | ||
} | ||
|
||
Console.WriteLine($"Unexpected enable/disable count _enables={source._enables} _disables={source._disables}"); | ||
return -1; | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||
</PropertyGroup> | ||
<ItemGroup> | ||
<Compile Include="eventlistenerenabledisable.cs" /> | ||
<ProjectReference Include="../common/common.csproj" /> | ||
</ItemGroup> | ||
</Project> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the behavior here is probably the best we can do within the current limits of EventSource, but it still seems a little odd and merits comments. In particular I am comparing these two scenarios:
Scenario A - first the user enables a session with IntervalCounterIntervalSec=1, then they do an ETW update with new filter args "Foo=6" (which presumably gets translated into an Enable)
Scenario B - user enables a session with filter args Foo=6
Both scenarios end with ETW believing the current filter args are "Foo=6", but in A the counters are on whereas in B they are off. We are effectively saying not only does the current state matter, but also the sequence of steps you took to get there matters. In an ideal world where EventSource could enumerate the current filter args of each session we would know that scenario A shouldn't have the counters enabled anymore, but we don't have that info. Given the ambiguity I agree its probably better to guess that they are still on rather than guessing that they are off now.