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