1111
1212namespace System . Diagnostics . Metrics
1313{
14- [ UnsupportedOSPlatform ( "browser" ) ]
1514 [ SecuritySafeCritical ]
1615 internal sealed class AggregationManager
1716 {
@@ -29,6 +28,9 @@ internal sealed class AggregationManager
2928 private readonly ConcurrentDictionary < Instrument , InstrumentState > _instrumentStates = new ( ) ;
3029 private readonly CancellationTokenSource _cts = new ( ) ;
3130 private Thread ? _collectThread ;
31+ #if OS_ISBROWSER_SUPPORT
32+ private Timer ? _pollingTimer ;
33+ #endif
3234 private readonly MeterListener _listener ;
3335 private int _currentTimeSeries ;
3436 private int _currentHistograms ;
@@ -43,6 +45,9 @@ internal sealed class AggregationManager
4345 private readonly Action _timeSeriesLimitReached ;
4446 private readonly Action _histogramLimitReached ;
4547 private readonly Action < Exception > _observableInstrumentCallbackError ;
48+ private DateTime _startTime ;
49+ private DateTime _intervalStartTime ;
50+ private DateTime _nextIntervalStartTime ;
4651
4752 public AggregationManager (
4853 int maxTimeSeries ,
@@ -160,15 +165,28 @@ public void Start()
160165 Debug . Assert ( _collectThread == null && ! _cts . IsCancellationRequested ) ;
161166 Debug . Assert ( CollectionPeriod . TotalSeconds >= MinCollectionTimeSecs ) ;
162167
163- // This explicitly uses a Thread and not a Task so that metrics still work
164- // even when an app is experiencing thread-pool starvation. Although we
165- // can't make in-proc metrics robust to everything, this is a common enough
166- // problem in .NET apps that it feels worthwhile to take the precaution.
167- _collectThread = new Thread ( ( ) => CollectWorker ( _cts . Token ) ) ;
168- _collectThread . IsBackground = true ;
169- _collectThread . Name = "MetricsEventSource CollectWorker" ;
170- _collectThread . Start ( ) ;
168+ _intervalStartTime = _nextIntervalStartTime = _startTime = DateTime . UtcNow ;
169+ #if OS_ISBROWSER_SUPPORT
170+ if ( OperatingSystem . IsBrowser ( ) )
171+ {
172+ TimeSpan delayTime = CalculateDelayTime ( CollectionPeriod . TotalSeconds ) ;
173+ _pollingTimer = new Timer ( CollectOnTimer , null , ( int ) delayTime . TotalMilliseconds , 0 ) ;
174+ }
175+ else
176+ #endif
177+ {
178+ // This explicitly uses a Thread and not a Task so that metrics still work
179+ // even when an app is experiencing thread-pool starvation. Although we
180+ // can't make in-proc metrics robust to everything, this is a common enough
181+ // problem in .NET apps that it feels worthwhile to take the precaution.
182+ _collectThread = new Thread ( CollectWorker ) ;
183+ _collectThread . IsBackground = true ;
184+ _collectThread . Name = "MetricsEventSource CollectWorker" ;
185+ #pragma warning disable CA1416 // 'Thread.Start' is unsupported on: 'browser', there the actual implementation is in AggregationManager.Wasm.cs
186+ _collectThread . Start ( ) ;
187+ #pragma warning restore CA1416
171188
189+ }
172190 _listener . Start ( ) ;
173191 _initialInstrumentEnumerationComplete ( ) ;
174192 }
@@ -187,57 +205,64 @@ public void Update()
187205 _initialInstrumentEnumerationComplete ( ) ;
188206 }
189207
190- private void CollectWorker ( CancellationToken cancelToken )
208+ private TimeSpan CalculateDelayTime ( double collectionIntervalSecs )
209+ {
210+ _intervalStartTime = _nextIntervalStartTime ;
211+
212+ // intervals end at startTime + X*collectionIntervalSecs. Under normal
213+ // circumstance X increases by 1 each interval, but if the time it
214+ // takes to do collection is very large then we might need to skip
215+ // ahead multiple intervals to catch back up.
216+ //
217+ DateTime now = DateTime . UtcNow ;
218+ double secsSinceStart = ( now - _startTime ) . TotalSeconds ;
219+ double alignUpSecsSinceStart = Math . Ceiling ( secsSinceStart / collectionIntervalSecs ) *
220+ collectionIntervalSecs ;
221+ _nextIntervalStartTime = _startTime . AddSeconds ( alignUpSecsSinceStart ) ;
222+
223+ // The delay timer precision isn't exact. We might have a situation
224+ // where in the previous loop iterations intervalStartTime=20.00,
225+ // nextIntervalStartTime=21.00, the timer was supposed to delay for 1s but
226+ // it exited early so we looped around and DateTime.Now=20.99.
227+ // Aligning up from DateTime.Now would give us 21.00 again so we also need to skip
228+ // forward one time interval
229+ DateTime minNextInterval = _intervalStartTime . AddSeconds ( collectionIntervalSecs ) ;
230+ if ( _nextIntervalStartTime <= minNextInterval )
231+ {
232+ _nextIntervalStartTime = minNextInterval ;
233+ }
234+ return _nextIntervalStartTime - now ;
235+ }
236+
237+ private void CollectWorker ( )
191238 {
192239 try
193240 {
194241 double collectionIntervalSecs = - 1 ;
242+ CancellationToken cancelToken ;
195243 lock ( this )
196244 {
197245 collectionIntervalSecs = CollectionPeriod . TotalSeconds ;
246+ cancelToken = _cts . Token ;
198247 }
199248 Debug . Assert ( collectionIntervalSecs >= MinCollectionTimeSecs ) ;
200249
201250 DateTime startTime = DateTime . UtcNow ;
202251 DateTime intervalStartTime = startTime ;
203- while ( ! cancelToken . IsCancellationRequested )
252+ while ( ! _cts . Token . IsCancellationRequested )
204253 {
205- // intervals end at startTime + X*collectionIntervalSecs. Under normal
206- // circumstance X increases by 1 each interval, but if the time it
207- // takes to do collection is very large then we might need to skip
208- // ahead multiple intervals to catch back up.
209- //
210- DateTime now = DateTime . UtcNow ;
211- double secsSinceStart = ( now - startTime ) . TotalSeconds ;
212- double alignUpSecsSinceStart = Math . Ceiling ( secsSinceStart / collectionIntervalSecs ) *
213- collectionIntervalSecs ;
214- DateTime nextIntervalStartTime = startTime . AddSeconds ( alignUpSecsSinceStart ) ;
215-
216- // The delay timer precision isn't exact. We might have a situation
217- // where in the previous loop iterations intervalStartTime=20.00,
218- // nextIntervalStartTime=21.00, the timer was supposed to delay for 1s but
219- // it exited early so we looped around and DateTime.Now=20.99.
220- // Aligning up from DateTime.Now would give us 21.00 again so we also need to skip
221- // forward one time interval
222- DateTime minNextInterval = intervalStartTime . AddSeconds ( collectionIntervalSecs ) ;
223- if ( nextIntervalStartTime <= minNextInterval )
224- {
225- nextIntervalStartTime = minNextInterval ;
226- }
227-
228254 // pause until the interval is complete
229- TimeSpan delayTime = nextIntervalStartTime - now ;
255+ TimeSpan delayTime = CalculateDelayTime ( collectionIntervalSecs ) ;
230256 if ( cancelToken . WaitHandle . WaitOne ( delayTime ) )
231257 {
232258 // don't do collection if timer may not have run to completion
233259 break ;
234260 }
235261
236262 // collect statistics for the completed interval
237- _beginCollection ( intervalStartTime , nextIntervalStartTime ) ;
263+ _beginCollection ( _intervalStartTime , _nextIntervalStartTime ) ;
238264 Collect ( ) ;
239- _endCollection ( intervalStartTime , nextIntervalStartTime ) ;
240- intervalStartTime = nextIntervalStartTime ;
265+ _endCollection ( _intervalStartTime , _nextIntervalStartTime ) ;
241266 }
242267 }
243268 catch ( Exception e )
@@ -246,12 +271,49 @@ private void CollectWorker(CancellationToken cancelToken)
246271 }
247272 }
248273
274+ #if OS_ISBROWSER_SUPPORT
275+ private void CollectOnTimer ( object ? _ )
276+ {
277+ try
278+ {
279+ // this is single-threaded so we don't need to lock
280+ CancellationToken cancelToken = _cts . Token ;
281+ double collectionIntervalSecs = CollectionPeriod . TotalSeconds ;
282+
283+ if ( cancelToken . IsCancellationRequested )
284+ {
285+ return ;
286+ }
287+
288+ // collect statistics for the completed interval
289+ _beginCollection ( _intervalStartTime , _nextIntervalStartTime ) ;
290+ Collect ( ) ;
291+ _endCollection ( _intervalStartTime , _nextIntervalStartTime ) ;
292+
293+ TimeSpan delayTime = CalculateDelayTime ( collectionIntervalSecs ) ;
294+ // schedule the next collection
295+ _pollingTimer ! . Change ( ( int ) delayTime . TotalMilliseconds , 0 ) ;
296+ }
297+ catch ( Exception e )
298+ {
299+ _collectionError ( e ) ;
300+ }
301+ }
302+ #endif
303+
249304 public void Dispose ( )
250305 {
251306 _cts . Cancel ( ) ;
252- if ( _collectThread != null )
307+ #if OS_ISBROWSER_SUPPORT
308+ if ( OperatingSystem . IsBrowser ( ) )
309+ {
310+ _pollingTimer ? . Dispose ( ) ;
311+ _pollingTimer = null ;
312+ }
313+ else
314+ #endif
253315 {
254- _collectThread . Join ( ) ;
316+ _collectThread ? . Join ( ) ;
255317 _collectThread = null ;
256318 }
257319 _listener . Dispose ( ) ;
0 commit comments