EventListeners for Hardware Resources
10/25/2020
8 minutes
EventSources and EventListeners
.net core infrastructure for event counters consists of a good couple of classes. This post will details the classes and their responbility when it comes to in-process event counting. I am writing this post by reverse engineering part of System.Diagnostics
and System.Diagnostics.Tracing
from their GitHub repo.
EventSource
, EventListener
, EventCounter
(DiagnosticCounter
), CounterGroup
, EventDispatcher
. Not all of these classes are relevant when simply creating or consuming counters, but we need a good understanding when it comes to testing. The purpose of this post is to detail their responsibilities and their internals.
Generally, to create our own set of counters, we would need to derive from EventSource
class. In this class typically in the constructor we can create counters such as PollingCounter, IncrementingCounter, etc. Each counter has a name and a reference to containing EventSource
derived class.
A counter itself has one responsibility - SRP - when a metric is written it is enqueued to a fixed size buffer. When a buffer is full, it runs some basic aggregation for the sum, min and max values. Otherwise when an internal WritePayload() method is invoked, it calculates the rest of the statistics: mean, stddev, etc. and writes the event to the EventSource
with EventCounters name and LogAlways log level.
Who calls WritePayload() method?
When an EventCounter is created, it is added to a counter group. A counter group is a set of counters that belong to a given EventSource. When an EventSource
gets enabled, the first related countergroup start up a shared (static) thread for running a Timer
. This timer then calculates the next poll interval for all enabled counter groups and waits for the given time. Once the thread is signaled, it selects the corresponding counter group and invokes WritePayload() method for all counters in the group.
Note that for this reason a counter group always ticks as frequent as the most frequent subscribtion requires it.
Finally, when WritePayload() writes an event to the EventSource
a list of listeners subsribed will be invoked through a dispatcher. The dispatcher holds a reference to EventListener
and the listener's OnEventWritten() method is invoked. Here is a comment from the source detailing the caveats:
An EventListener represents a target for the events generated by EventSources (that is subclasses of EventSource), in the current appdomain. When a new EventListener is created it is logically attached to all eventSources in that appdomain. When the EventListener is Disposed, then it is disconnected from the event eventSources. Note that there is a internal list of STRONG references to EventListeners, which means that relying on the lack of references to EventListeners to clean up EventListeners will NOT work. You must call EventListener.Dispose explicitly when a dispatcher is no longer needed.
A user typically derives from EventListener
, to subscribe to in-process events. This derived class may override OnEventSourceCreated() and OnEventWritten() methods. EventListener
also publishes two events: public event EventHandler<EventWrittenEventArgs>? EventWritten;
and public event EventHandler<EventSourceCreatedEventArgs>? EventSourceCreated
. These events are fired by the corresponding virtual methods. To make sure these events are fired, the derived class must call the overriden base methods in its implementation.
When it comes to testing I will look into these use-cases:
Test a custom event source
Test a custom event listener subscribing to a built-in/custom event
A careful reader might already spot, that the challenge when testing events comes from the fact that counters are aggregated on a separate thread in an async manner. This means to avoid the tests waiting for an arbitary long time, we need a good mechanism to get notified once a counter is written. For unit test typically there is less focus on the correctness of the counter's logic (as that is already tested with the .net releases), instead to focus shifts that the correct counters are written, in the correct number of times.
Testing Custom EventSource
Let's create a custom EventSource
: SudokuResolverEvents
counts the number of sudokus resolved by an algorithm. This class is singleton, so only a single instance exists of it during the lifetime of the application.
[EventSource(Name = "SudokuResolverEvents")] public sealed class SudokuResolverEvents : EventSource { private IncrementingEventCounter _counter; public static readonly SudokuResolverEvents Instance = new SudokuResolverEvents(); private SudokuResolverEvents() { _counter = new IncrementingEventCounter("solved-sudoku", this); } public void Solved() { _counter.Increment(); } }
Besides this counter class there is a class using this counter. As the user class is part of the core business logic of resolving sudokus, it is the one incrementing the number of solved sudokus every time its core method completes. Here I am using a Delay instead of the actual work being done.
public class BusinessLogic { public async Task<int> Resolve() { await Task.Delay(120); // Solve a sudoku SudokuResolverEvents.Instance.Solved(); return 0; } }
To test such an EventSource class, I will use the xUnit test framework. For the tests 2 helper classes are added: a generic TestEventListener
and a static class for extension methods for SudokuResolverEvents
. I also disable running tests parallel to each other, because the counter itself is singleton, so multiple tests invoking the same counter would defeat its testability.
public static class SudokuResolverEventsExtensions { public static TestEventListener CaptureEvents(this SudokuResolverEvents source, string counterName) { return new TestEventListener(source, counterName); } }
The extension method creates a TestEventListener
passing the event source and the counter name to subscribe to.
TestEventListener
is a bit more involved:
public class TestEventListener : EventListener { private readonly string _counterName; private List<IDictionary<string, object>> _capturedEvents; private int? _awaitEventsCount; private TaskCompletionSource<IEnumerable<IDictionary<string, object>>> _awaitEvents; public TestEventListener(EventSource eventSource, string counterName) { _counterName = counterName; _capturedEvents = new List<IDictionary<string, object>>(); EnableEvents(eventSource, EventLevel.LogAlways, EventKeywords.All, new Dictionary<string, string> { { "EventCounterIntervalSec", (0.1).ToString() } }); } protected override void OnEventWritten(EventWrittenEventArgs eventData) { if (eventData.Payload == null || eventData.Payload.Count == 0) return; if (eventData.Payload[0] is IDictionary<string, object> eventPayload && eventPayload.TryGetValue("Name", out var nameData) && nameData is string name && name == _counterName) { _capturedEvents.Add(eventPayload); if (_awaitEventsCount.HasValue && _awaitEventsCount.Value <= _capturedEvents.Count) { _awaitEvents.TrySetResult(_capturedEvents); _awaitEventsCount = null; _capturedEvents = new List<IDictionary<string, object>>(); } } } public Task<IEnumerable<IDictionary<string, object>>> AwaitEventAsync(int count, CancellationToken token = default) { _awaitEvents = new TaskCompletionSource<IEnumerable<IDictionary<string, object>>>(); _awaitEventsCount = count; token.Register(() => _awaitEvents.TrySetCanceled(token)); return _awaitEvents.Task; } }
First to point out, it is not thread-safe, for basic tests this should be fine. In the constructor it enables events for the event source passed in. Because this event source must exist before instanciating TestEventListener
, there is no need for overriding OnEventSourceCreated() method for enabling event sources created later. EnableEvents() has an argument (0.1).ToString()
specifying how often events are fired by the WritePayload(). The 0.1 value is an arbiraty float
passed in as a string
. Interanlly TryParse() method is used to parse the value, which uses the current culture. Certain cultures swap , and . in decimal numbers, hence it is safer to use ToString() to format the numbers for the current culture.
OnEventWritten() method captures events from counters with the specific name passed in at construction of TestEventListener
. Once a number of events ceptured, it sets the collected events as a result of a TaskCompletionSource
.
AwaitEventAsync() method initializes the TaskCompletionSource
, and sets the number of events to be collected. With all the plumming done, let's see how tests can be written:
[Fact] public async Task Solved_Increments_Counter() { using var listenContext = SudokuResolverEvents.Instance.CaptureEvents("solved-sudoku"); SudokuResolverEvents.Instance.Solved(); var capturedEvents = await listenContext.AwaitEventAsync(2); Assert.Contains(capturedEvents, x => x.TryGetValue("Increment", out var inc) && inc is double increment && increment == 1); }
First, in a using block the CaptureEvents
extension method creates the listener. Secondly, the Solved()
, this is the method under test. The result of this is an event with Increment key-value pair set to 1. Next, two events are awaited: the first event happens when the listener subscribes to the event source. Any subsequent events are fired every 0.1 seconds. Finally, we assert that there is indeed one event captured from the event counter, where the increment is set to one.
Testing the BusinessLogic
class is really similar to this:
[Fact] public async Task Resolve_Increases_SolvedCounter() { var sut = new BusinessLogic(); using var listenContext = SudokuResolverEvents.Instance.CaptureEvents("solved-sudoku"); await sut.Resolve(); var capturedEvents = await listenContext.AwaitEventAsync(2); Assert.Contains(capturedEvents, x => x.TryGetValue("Increment", out var inc) && inc is double increment && increment == 1); }
This post has given an overview on the most important classes of EventSource
and EventListener
infrastructure in dotnet. It has focused on in-process eventing flow, and detailed how events are fired. Next, it has shown a custom event source and a class using such an event source. Finally, it has provided a way to test the custom event source, by adding a couple of helper methods for the testing project.
In the next post I will show how a custom EventListener
may be tested.