Skip to content

Commit a8c5b84

Browse files
authored
Make counters use dedicated thread instead of timer (dotnet/coreclr#25864)
* Make a dedicated polling thread for EventCounters * Cleanup * remove debug prints * nit * Fix comment * addressing issues from code review * Fix an assertion from getting fired when we have multiple listeners * Fix a test issue * code review feedback * Fix a potential deadlock * Fix another deadlock * Allow s_sleepDurationInMilliseconds to be reset to a larger value * More issues fix * Let thread wake up from sleep if needed * Some suggestions for the counters fix. The resulting behavior should be the same as what is there now (assuming I didn't mess anything up), these are all just code simplifications that hopefully make it easier to review and maintain going forward. In total I think this reduces the change size in CounterGroup.cs by ~30%. - With the addition of the AutoResetEvent the ManualResetEvent is no longer needed, removed it. - Removed the various if foo != null checks for the shared state, it is all initialized once when then thread is created and then assumed to be non-null elsewhere. - Removed a 2nd lock acquisition inside OnTimer - Replaced an if with Math.Min in PollForValues * fix test Commit migrated from dotnet/coreclr@b3de897
1 parent 88f7610 commit a8c5b84

File tree

3 files changed

+93
-81
lines changed

3 files changed

+93
-81
lines changed

src/coreclr/tests/src/tracing/eventcounter/eventcounter.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData)
7676
if (payload.Key.Equals("Mean"))
7777
{
7878
int mean = Int32.Parse(payload.Value.ToString());
79+
Console.WriteLine("Adding " + mean + " to known list of means");
7980
means.Add(mean);
8081
}
8182
else if (payload.Key.Equals("DisplayName"))
@@ -95,17 +96,12 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData)
9596
public bool validateMean()
9697
{
9798
// we expect to see 1 because we wrote only 1s
99+
// we *might* also see 0 because there is a period of time we didn't write stuff and got callback
98100
if (!means.Contains(1))
99101
{
102+
Console.WriteLine("Mean doesn't have a 1");
100103
return false;
101104
}
102-
103-
// we also expect to see 0 because there is a period of time we didn't write stuff and got callback
104-
if (!means.Contains(0))
105-
{
106-
return false;
107-
}
108-
109105
return true;
110106
}
111107
}
@@ -159,4 +155,4 @@ public static int Main(string[] args)
159155
}
160156
}
161157
}
162-
}
158+
}

src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.cs

Lines changed: 81 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ internal class CounterGroup
2121
{
2222
private readonly EventSource _eventSource;
2323
private readonly List<DiagnosticCounter> _counters;
24+
private static readonly object s_counterGroupLock = new object();
2425

2526
internal CounterGroup(EventSource eventSource)
2627
{
@@ -31,13 +32,13 @@ internal CounterGroup(EventSource eventSource)
3132

3233
internal void Add(DiagnosticCounter eventCounter)
3334
{
34-
lock (this) // Lock the CounterGroup
35+
lock (s_counterGroupLock) // Lock the CounterGroup
3536
_counters.Add(eventCounter);
3637
}
3738

3839
internal void Remove(DiagnosticCounter eventCounter)
3940
{
40-
lock (this) // Lock the CounterGroup
41+
lock (s_counterGroupLock) // Lock the CounterGroup
4142
_counters.Remove(eventCounter);
4243
}
4344

@@ -56,17 +57,17 @@ private void OnEventSourceCommand(object? sender, EventCommandEventArgs e)
5657

5758
if (e.Arguments.TryGetValue("EventCounterIntervalSec", out string? valueStr) && float.TryParse(valueStr, out float value))
5859
{
59-
lock (this) // Lock the CounterGroup
60+
lock (s_counterGroupLock) // Lock the CounterGroup
6061
{
6162
EnableTimer(value);
6263
}
6364
}
6465
}
6566
else if (e.Command == EventCommand.Disable)
6667
{
67-
lock (this)
68+
lock(s_counterGroupLock)
6869
{
69-
_pollingIntervalInMilliseconds = 0;
70+
DisableTimer();
7071
}
7172
}
7273
}
@@ -79,11 +80,10 @@ private void OnEventSourceCommand(object? sender, EventCommandEventArgs e)
7980
// this table provides the mapping from EventSource -> CounterGroup
8081
// which represents this 'attached' information.
8182
private static WeakReference<CounterGroup>[]? s_counterGroups;
82-
private static readonly object s_counterGroupsLock = new object();
8383

8484
private static void EnsureEventSourceIndexAvailable(int eventSourceIndex)
8585
{
86-
Debug.Assert(Monitor.IsEntered(s_counterGroupsLock));
86+
Debug.Assert(Monitor.IsEntered(s_counterGroupLock));
8787
if (CounterGroup.s_counterGroups == null)
8888
{
8989
CounterGroup.s_counterGroups = new WeakReference<CounterGroup>[eventSourceIndex + 1];
@@ -98,7 +98,7 @@ private static void EnsureEventSourceIndexAvailable(int eventSourceIndex)
9898

9999
internal static CounterGroup GetCounterGroup(EventSource eventSource)
100100
{
101-
lock (s_counterGroupsLock)
101+
lock (s_counterGroupLock)
102102
{
103103
int eventSourceIndex = EventListener.EventSourceIndex(eventSource);
104104
EnsureEventSourceIndexAvailable(eventSourceIndex);
@@ -120,32 +120,20 @@ internal static CounterGroup GetCounterGroup(EventSource eventSource)
120120

121121
private DateTime _timeStampSinceCollectionStarted;
122122
private int _pollingIntervalInMilliseconds;
123-
private Timer? _pollingTimer;
124-
125-
private void DisposeTimer()
126-
{
127-
Debug.Assert(Monitor.IsEntered(this));
128-
if (_pollingTimer != null)
129-
{
130-
_pollingTimer.Dispose();
131-
_pollingTimer = null;
132-
}
133-
}
123+
private DateTime _nextPollingTimeStamp;
134124

135125
private void EnableTimer(float pollingIntervalInSeconds)
136126
{
137-
Debug.Assert(Monitor.IsEntered(this));
127+
Debug.Assert(Monitor.IsEntered(s_counterGroupLock));
138128
if (pollingIntervalInSeconds <= 0)
139129
{
140-
DisposeTimer();
141130
_pollingIntervalInMilliseconds = 0;
142131
}
143132
else if (_pollingIntervalInMilliseconds == 0 || pollingIntervalInSeconds * 1000 < _pollingIntervalInMilliseconds)
144133
{
145-
Debug.WriteLine("Polling interval changed at " + DateTime.UtcNow.ToString("mm.ss.ffffff"));
146134
_pollingIntervalInMilliseconds = (int)(pollingIntervalInSeconds * 1000);
147-
DisposeTimer();
148135
ResetCounters(); // Reset statistics for counters before we start the thread.
136+
149137
_timeStampSinceCollectionStarted = DateTime.UtcNow;
150138
// Don't capture the current ExecutionContext and its AsyncLocals onto the timer causing them to live forever
151139
bool restoreFlow = false;
@@ -157,7 +145,25 @@ private void EnableTimer(float pollingIntervalInSeconds)
157145
restoreFlow = true;
158146
}
159147

160-
_pollingTimer = new Timer(s => ((CounterGroup)s!).OnTimer(null), this, _pollingIntervalInMilliseconds, _pollingIntervalInMilliseconds);
148+
_nextPollingTimeStamp = DateTime.UtcNow + new TimeSpan(0, 0, (int)pollingIntervalInSeconds);
149+
150+
// Create the polling thread and init all the shared state if needed
151+
if (s_pollingThread == null)
152+
{
153+
s_pollingThreadSleepEvent = new AutoResetEvent(false);
154+
s_counterGroupEnabledList = new List<CounterGroup>();
155+
s_pollingThread = new Thread(PollForValues) { IsBackground = true };
156+
s_pollingThread.Start();
157+
}
158+
159+
if (!s_counterGroupEnabledList!.Contains(this))
160+
{
161+
s_counterGroupEnabledList.Add(this);
162+
}
163+
164+
// notify the polling thread that the polling interval may have changed and the sleep should
165+
// be recomputed
166+
s_pollingThreadSleepEvent!.Set();
161167
}
162168
finally
163169
{
@@ -168,9 +174,15 @@ private void EnableTimer(float pollingIntervalInSeconds)
168174
}
169175
}
170176

177+
private void DisableTimer()
178+
{
179+
_pollingIntervalInMilliseconds = 0;
180+
s_counterGroupEnabledList?.Remove(this);
181+
}
182+
171183
private void ResetCounters()
172184
{
173-
lock (this) // Lock the CounterGroup
185+
lock (s_counterGroupLock) // Lock the CounterGroup
174186
{
175187
foreach (var counter in _counters)
176188
{
@@ -190,63 +202,65 @@ private void ResetCounters()
190202
}
191203
}
192204

193-
private void OnTimer(object? state)
205+
private void OnTimer()
194206
{
195-
Debug.WriteLine("Timer fired at " + DateTime.UtcNow.ToString("mm.ss.ffffff"));
196-
lock (this) // Lock the CounterGroup
207+
Debug.Assert(Monitor.IsEntered(s_counterGroupLock));
208+
if (_eventSource.IsEnabled())
197209
{
198-
if (_eventSource.IsEnabled())
199-
{
200-
DateTime now = DateTime.UtcNow;
201-
TimeSpan elapsed = now - _timeStampSinceCollectionStarted;
210+
DateTime now = DateTime.UtcNow;
211+
TimeSpan elapsed = now - _timeStampSinceCollectionStarted;
202212

203-
foreach (var counter in _counters)
204-
{
205-
counter.WritePayload((float)elapsed.TotalSeconds, _pollingIntervalInMilliseconds);
206-
}
207-
_timeStampSinceCollectionStarted = now;
208-
}
209-
else
213+
foreach (var counter in _counters)
210214
{
211-
DisposeTimer();
215+
counter.WritePayload((float)elapsed.TotalSeconds, _pollingIntervalInMilliseconds);
212216
}
217+
_timeStampSinceCollectionStarted = now;
218+
219+
do
220+
{
221+
_nextPollingTimeStamp += new TimeSpan(0, 0, 0, 0, _pollingIntervalInMilliseconds);
222+
} while (_nextPollingTimeStamp <= now);
213223
}
214224
}
215225

216-
#region PCL timer hack
217226

218-
#if ES_BUILD_PCL
219-
internal delegate void TimerCallback(object state);
220-
221-
internal sealed class Timer : CancellationTokenSource, IDisposable
222-
{
223-
private int _period;
224-
private TimerCallback _callback;
225-
private object _state;
226227

227-
internal Timer(TimerCallback callback, object state, int dueTime, int period)
228-
{
229-
_callback = callback;
230-
_state = state;
231-
_period = period;
232-
Schedule(dueTime);
233-
}
228+
private static Thread? s_pollingThread;
229+
// Used for sleeping for a certain amount of time while allowing the thread to be woken up
230+
private static AutoResetEvent? s_pollingThreadSleepEvent;
234231

235-
private void Schedule(int dueTime)
236-
{
237-
Task.Delay(dueTime, Token).ContinueWith(OnTimer, null, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
238-
}
232+
private static List<CounterGroup>? s_counterGroupEnabledList;
239233

240-
private void OnTimer(Task t, object s)
234+
private static void PollForValues()
235+
{
236+
AutoResetEvent? sleepEvent = null;
237+
while (true)
241238
{
242-
Schedule(_period);
243-
_callback(_state);
239+
int sleepDurationInMilliseconds = Int32.MaxValue;
240+
lock (s_counterGroupLock)
241+
{
242+
sleepEvent = s_pollingThreadSleepEvent;
243+
foreach (CounterGroup counterGroup in s_counterGroupEnabledList!)
244+
{
245+
DateTime now = DateTime.UtcNow;
246+
if (counterGroup._nextPollingTimeStamp < now + new TimeSpan(0, 0, 0, 0, 1))
247+
{
248+
counterGroup.OnTimer();
249+
}
250+
251+
int millisecondsTillNextPoll = (int)((counterGroup._nextPollingTimeStamp - now).TotalMilliseconds);
252+
millisecondsTillNextPoll = Math.Max(1, millisecondsTillNextPoll);
253+
sleepDurationInMilliseconds = Math.Min(sleepDurationInMilliseconds, millisecondsTillNextPoll);
254+
}
255+
}
256+
if (sleepDurationInMilliseconds == Int32.MaxValue)
257+
{
258+
sleepDurationInMilliseconds = -1; // WaitOne uses -1 to mean infinite
259+
}
260+
sleepEvent?.WaitOne(sleepDurationInMilliseconds);
244261
}
245-
246-
public new void Dispose() { base.Cancel(); }
247262
}
248-
#endif
249-
#endregion // PCL timer hack
263+
250264

251265
#endregion // Timer Processing
252266

src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventCounter.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,14 @@ internal override void WritePayload(float intervalSec, int pollingIntervalMillis
116116

117117
internal void ResetStatistics()
118118
{
119-
Debug.Assert(Monitor.IsEntered(this));
120-
_count = 0;
121-
_sum = 0;
122-
_sumSquared = 0;
123-
_min = double.PositiveInfinity;
124-
_max = double.NegativeInfinity;
119+
lock(this)
120+
{
121+
_count = 0;
122+
_sum = 0;
123+
_sumSquared = 0;
124+
_min = double.PositiveInfinity;
125+
_max = double.NegativeInfinity;
126+
}
125127
}
126128

127129
#endregion // Statistics Calculation

0 commit comments

Comments
 (0)