@@ -204,22 +204,47 @@ private void ResetCounters()
204
204
205
205
private void OnTimer ( )
206
206
{
207
- Debug . Assert ( Monitor . IsEntered ( s_counterGroupLock ) ) ;
208
207
if ( _eventSource . IsEnabled ( ) )
209
208
{
210
- DateTime now = DateTime . UtcNow ;
211
- TimeSpan elapsed = now - _timeStampSinceCollectionStarted ;
209
+ DateTime now ;
210
+ TimeSpan elapsed ;
211
+ int pollingIntervalInMilliseconds ;
212
+ DiagnosticCounter [ ] counters ;
213
+ lock ( s_counterGroupLock )
214
+ {
215
+ now = DateTime . UtcNow ;
216
+ elapsed = now - _timeStampSinceCollectionStarted ;
217
+ pollingIntervalInMilliseconds = _pollingIntervalInMilliseconds ;
218
+ counters = new DiagnosticCounter [ _counters . Count ] ;
219
+ _counters . CopyTo ( counters ) ;
220
+ }
221
+
222
+ // MUST keep out of the scope of s_counterGroupLock because this will cause WritePayload
223
+ // callback can be re-entrant to CounterGroup (i.e. it's possible it calls back into EnableTimer()
224
+ // above, since WritePayload callback can contain user code that can invoke EventSource constructor
225
+ // and lead to a deadlock. (See https://github.com/dotnet/runtime/issues/40190 for details)
212
226
213
227
foreach ( var counter in _counters )
214
228
{
215
- counter . WritePayload ( ( float ) elapsed . TotalSeconds , _pollingIntervalInMilliseconds ) ;
229
+ // NOTE: It is still possible for a race condition to occur here. An example is if the session
230
+ // that subscribed to these batch of counters was disabled and it was immediately enabled in
231
+ // a different session, some of the counter data that was supposed to be written to the old
232
+ // session can now "overflow" into the new session.
233
+ // This problem pre-existed to this change (when we used to hold lock in the call to WritePayload):
234
+ // the only difference being the old behavior caused the entire batch of counters to be either
235
+ // written to the old session or the new session. The behavior change is not being treated as a
236
+ // significant problem to address for now, but we can come back and address it if it turns out to
237
+ // be an actual issue.
238
+ counter . WritePayload ( ( float ) elapsed . TotalSeconds , pollingIntervalInMilliseconds ) ;
216
239
}
217
- _timeStampSinceCollectionStarted = now ;
218
-
219
- do
240
+ lock ( s_counterGroupLock )
220
241
{
221
- _nextPollingTimeStamp += new TimeSpan ( 0 , 0 , 0 , 0 , _pollingIntervalInMilliseconds ) ;
222
- } while ( _nextPollingTimeStamp <= now ) ;
242
+ _timeStampSinceCollectionStarted = now ;
243
+ do
244
+ {
245
+ _nextPollingTimeStamp += new TimeSpan ( 0 , 0 , 0 , 0 , _pollingIntervalInMilliseconds ) ;
246
+ } while ( _nextPollingTimeStamp <= now ) ;
247
+ }
223
248
}
224
249
}
225
250
@@ -234,8 +259,15 @@ private void OnTimer()
234
259
private static void PollForValues ( )
235
260
{
236
261
AutoResetEvent ? sleepEvent = null ;
262
+
263
+ // Cache of onTimer callbacks for each CounterGroup.
264
+ // We cache these outside of the scope of s_counterGroupLock because
265
+ // calling into the callbacks can cause a re-entrancy into CounterGroup.Enable()
266
+ // and result in a deadlock. (See https://github.com/dotnet/runtime/issues/40190 for details)
267
+ List < Action > onTimers = new List < Action > ( ) ;
237
268
while ( true )
238
269
{
270
+ onTimers . Clear ( ) ;
239
271
int sleepDurationInMilliseconds = Int32 . MaxValue ;
240
272
lock ( s_counterGroupLock )
241
273
{
@@ -245,14 +277,18 @@ private static void PollForValues()
245
277
DateTime now = DateTime . UtcNow ;
246
278
if ( counterGroup . _nextPollingTimeStamp < now + new TimeSpan ( 0 , 0 , 0 , 0 , 1 ) )
247
279
{
248
- counterGroup . OnTimer ( ) ;
280
+ onTimers . Add ( ( ) => counterGroup . OnTimer ( ) ) ;
249
281
}
250
282
251
283
int millisecondsTillNextPoll = ( int ) ( ( counterGroup . _nextPollingTimeStamp - now ) . TotalMilliseconds ) ;
252
284
millisecondsTillNextPoll = Math . Max ( 1 , millisecondsTillNextPoll ) ;
253
285
sleepDurationInMilliseconds = Math . Min ( sleepDurationInMilliseconds , millisecondsTillNextPoll ) ;
254
286
}
255
287
}
288
+ foreach ( Action onTimer in onTimers )
289
+ {
290
+ onTimer . Invoke ( ) ;
291
+ }
256
292
if ( sleepDurationInMilliseconds == Int32 . MaxValue )
257
293
{
258
294
sleepDurationInMilliseconds = - 1 ; // WaitOne uses -1 to mean infinite
0 commit comments