File: src\libraries\System.Private.CoreLib\src\System\Diagnostics\Tracing\CounterGroup.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Runtime.Versioning;
using System.Threading;
 
namespace System.Diagnostics.Tracing
{
#if !ES_BUILD_STANDALONE
    [UnsupportedOSPlatform("browser")]
#endif
    internal sealed class CounterGroup
    {
        private readonly EventSource _eventSource;
        private readonly List<DiagnosticCounter> _counters;
        private static readonly object s_counterGroupLock = new object();
 
        internal CounterGroup(EventSource eventSource)
        {
            _eventSource = eventSource;
            _counters = new List<DiagnosticCounter>();
            RegisterCommandCallback();
        }
 
        internal void Add(DiagnosticCounter eventCounter)
        {
            lock (s_counterGroupLock) // Lock the CounterGroup
                _counters.Add(eventCounter);
        }
 
        internal void Remove(DiagnosticCounter eventCounter)
        {
            lock (s_counterGroupLock) // Lock the CounterGroup
                _counters.Remove(eventCounter);
        }
 
#region EventSource Command Processing
 
        private void RegisterCommandCallback()
        {
            _eventSource.EventCommandExecuted += OnEventSourceCommand;
        }
 
        private void OnEventSourceCommand(object? sender, EventCommandEventArgs e)
        {
            // Should only be enable or disable
            Debug.Assert(e.Command == EventCommand.Enable || e.Command == EventCommand.Disable);
 
            lock (s_counterGroupLock)      // Lock the CounterGroup
            {
                if (e.Command == EventCommand.Enable)
                {
                    Debug.Assert(e.Arguments != null);
 
                    if (!e.Arguments.TryGetValue("EventCounterIntervalSec", out string? valueStr)
                        || !float.TryParse(valueStr, out float intervalValue))
                    {
                        // Command is Enable but no EventCounterIntervalSec arg so ignore
                        return;
                    }
 
                    // Sending an Enabled with EventCounterIntervalSec <=0 is a signal that we should immediately turn
                    // off counters
                    if (intervalValue <= 0)
                    {
                        DisableTimer();
                    }
                    else
                    {
                        EnableTimer(intervalValue);
                    }
                }
                else
                {
                    Debug.Assert(e.Command == EventCommand.Disable);
                    // Since we allow sessions to send multiple Enable commands to update the interval, we cannot
                    // rely on ref counting to determine when to enable and disable counters. You will get an arbitrary
                    // number of Enables and one Disable per session.
                    //
                    // Previously we would turn off counters when we received any Disable command, but that meant that any
                    // session could turn off counters for all other sessions. To get to a good place we now will only
                    // turn off counters once the EventSource that provides the counters is disabled. We can then end up
                    // keeping counters on too long in certain circumstances - if one session enables counters, then a second
                    // session enables the EventSource but not counters we will stay on until both sessions terminate, even
                    // if the first session terminates first.
                    if (!_eventSource.IsEnabled())
                    {
                        DisableTimer();
                    }
                }
 
                Debug.Assert((s_counterGroupEnabledList == null && !_eventSource.IsEnabled())
                                || (_eventSource.IsEnabled() && s_counterGroupEnabledList!.Contains(this))
                                || (_pollingIntervalInMilliseconds == 0 && !s_counterGroupEnabledList!.Contains(this))
                                || (!_eventSource.IsEnabled() && !s_counterGroupEnabledList!.Contains(this)));
            }
        }
 
#endregion // EventSource Command Processing
 
#region Global CounterGroup Array management
 
        // We need eventCounters to 'attach' themselves to a particular EventSource.
        // this table provides the mapping from EventSource -> CounterGroup
        // which represents this 'attached' information.
        private static WeakReference<CounterGroup>[]? s_counterGroups;
 
        private static void EnsureEventSourceIndexAvailable(int eventSourceIndex)
        {
            Debug.Assert(Monitor.IsEntered(s_counterGroupLock));
            if (s_counterGroups == null)
            {
                s_counterGroups = new WeakReference<CounterGroup>[eventSourceIndex + 1];
            }
            else if (eventSourceIndex >= s_counterGroups.Length)
            {
                WeakReference<CounterGroup>[] newCounterGroups = new WeakReference<CounterGroup>[eventSourceIndex + 1];
                Array.Copy(s_counterGroups, newCounterGroups, s_counterGroups.Length);
                s_counterGroups = newCounterGroups;
            }
        }
 
        internal static CounterGroup GetCounterGroup(EventSource eventSource)
        {
            lock (s_counterGroupLock)
            {
                int eventSourceIndex = EventListener.EventSourceIndex(eventSource);
                EnsureEventSourceIndexAvailable(eventSourceIndex);
                Debug.Assert(s_counterGroups != null);
                WeakReference<CounterGroup> weakRef = s_counterGroups[eventSourceIndex];
                if (weakRef == null || !weakRef.TryGetTarget(out CounterGroup? ret))
                {
                    ret = new CounterGroup(eventSource);
                    s_counterGroups[eventSourceIndex] = new WeakReference<CounterGroup>(ret);
                }
                return ret;
            }
        }
 
#endregion // Global CounterGroup Array management
 
#region Timer Processing
 
        private DateTime _timeStampSinceCollectionStarted;
        private int _pollingIntervalInMilliseconds;
        private DateTime _nextPollingTimeStamp;
 
        private void EnableTimer(float pollingIntervalInSeconds)
        {
            Debug.Assert(pollingIntervalInSeconds > 0);
            Debug.Assert(Monitor.IsEntered(s_counterGroupLock));
            if (_pollingIntervalInMilliseconds == 0 || pollingIntervalInSeconds * 1000 < _pollingIntervalInMilliseconds)
            {
                _pollingIntervalInMilliseconds = (int)(pollingIntervalInSeconds * 1000);
                // Schedule IncrementingPollingCounter reset and synchronously reset other counters
                HandleCountersReset();
 
                _timeStampSinceCollectionStarted = DateTime.UtcNow;
                _nextPollingTimeStamp = DateTime.UtcNow + new TimeSpan(0, 0, (int)pollingIntervalInSeconds);
 
                // Create the polling thread and init all the shared state if needed
                if (s_pollingThread == null)
                {
                    s_pollingThreadSleepEvent = new AutoResetEvent(false);
                    s_counterGroupEnabledList = new List<CounterGroup>();
                    s_pollingThread = new Thread(PollForValues)
                    {
                        IsBackground = true,
                        Name = ".NET Counter Poller"
                    };
                    s_pollingThread.Start();
                }
 
                if (!s_counterGroupEnabledList!.Contains(this))
                {
                    s_counterGroupEnabledList.Add(this);
                }
 
                // notify the polling thread that the polling interval may have changed and the sleep should
                // be recomputed
                s_pollingThreadSleepEvent!.Set();
            }
        }
 
        private void DisableTimer()
        {
            Debug.Assert(Monitor.IsEntered(s_counterGroupLock));
            _pollingIntervalInMilliseconds = 0;
            s_counterGroupEnabledList?.Remove(this);
 
            if (s_needsResetIncrementingPollingCounters.Count > 0)
            {
                foreach (DiagnosticCounter diagnosticCounter in _counters)
                {
                    if (diagnosticCounter is IncrementingPollingCounter pollingCounter)
                        s_needsResetIncrementingPollingCounters.Remove(pollingCounter);
                }
            }
        }
 
        private void HandleCountersReset()
        {
            Debug.Assert(Monitor.IsEntered(s_counterGroupLock));
            foreach (DiagnosticCounter counter in _counters)
            {
                if (counter is IncrementingEventCounter ieCounter)
                {
                    ieCounter.UpdateMetric();
                }
                else if (counter is IncrementingPollingCounter ipCounter)
                {
                    // IncrementingPollingCounters will be reset on timer thread
                    // We need this to avoid deadlocks caused by running IncrementingPollingCounter._totalValueProvider under EventListener.EventListenersLock
                    s_needsResetIncrementingPollingCounters.Add(ipCounter);
                }
                else if (counter is EventCounter eCounter)
                {
                    eCounter.ResetStatistics();
                }
            }
        }
 
        private void OnTimer()
        {
            if (_eventSource.IsEnabled())
            {
                DateTime now;
                TimeSpan elapsed;
                int pollingIntervalInMilliseconds;
                DiagnosticCounter[] counters;
                lock (s_counterGroupLock)
                {
                    now = DateTime.UtcNow;
                    elapsed = now - _timeStampSinceCollectionStarted;
                    pollingIntervalInMilliseconds = _pollingIntervalInMilliseconds;
                    counters = new DiagnosticCounter[_counters.Count];
                    _counters.CopyTo(counters);
                }
 
                // MUST keep out of the scope of s_counterGroupLock because this will cause WritePayload
                // callback can be re-entrant to CounterGroup (i.e. it's possible it calls back into EnableTimer()
                // above, since WritePayload callback can contain user code that can invoke EventSource constructor
                // and lead to a deadlock. (See https://github.com/dotnet/runtime/issues/40190 for details)
                foreach (DiagnosticCounter counter in counters)
                {
                    // NOTE: It is still possible for a race condition to occur here. An example is if the session
                    // that subscribed to these batch of counters was disabled and it was immediately enabled in
                    // a different session, some of the counter data that was supposed to be written to the old
                    // session can now "overflow" into the new session.
                    // This problem pre-existed to this change (when we used to hold lock in the call to WritePayload):
                    // the only difference being the old behavior caused the entire batch of counters to be either
                    // written to the old session or the new session. The behavior change is not being treated as a
                    // significant problem to address for now, but we can come back and address it if it turns out to
                    // be an actual issue.
                    counter.WritePayload((float)elapsed.TotalSeconds, pollingIntervalInMilliseconds);
                }
 
                lock (s_counterGroupLock)
                {
                    _timeStampSinceCollectionStarted = now;
                    TimeSpan delta = now - _nextPollingTimeStamp;
                    delta = _pollingIntervalInMilliseconds > delta.TotalMilliseconds ? TimeSpan.FromMilliseconds(_pollingIntervalInMilliseconds) : delta;
                    if (_pollingIntervalInMilliseconds > 0)
                        _nextPollingTimeStamp += TimeSpan.FromMilliseconds(_pollingIntervalInMilliseconds * Math.Ceiling(delta.TotalMilliseconds / _pollingIntervalInMilliseconds));
                }
            }
        }
 
        private static Thread? s_pollingThread;
        // Used for sleeping for a certain amount of time while allowing the thread to be woken up
        private static AutoResetEvent? s_pollingThreadSleepEvent;
 
        private static List<CounterGroup>? s_counterGroupEnabledList;
        private static List<IncrementingPollingCounter> s_needsResetIncrementingPollingCounters = [];
 
        private static void PollForValues()
        {
            AutoResetEvent? sleepEvent = null;
 
            // Cache of onTimer callbacks for each CounterGroup.
            // We cache these outside of the scope of s_counterGroupLock because
            // calling into the callbacks can cause a re-entrancy into CounterGroup.Enable()
            // and result in a deadlock. (See https://github.com/dotnet/runtime/issues/40190 for details)
            var onTimers = new List<CounterGroup>();
            List<IncrementingPollingCounter>? countersToReset = null;
            while (true)
            {
                int sleepDurationInMilliseconds = int.MaxValue;
                lock (s_counterGroupLock)
                {
                    sleepEvent = s_pollingThreadSleepEvent;
                    foreach (CounterGroup counterGroup in s_counterGroupEnabledList!)
                    {
                        DateTime now = DateTime.UtcNow;
                        if (counterGroup._nextPollingTimeStamp < now + new TimeSpan(0, 0, 0, 0, 1))
                        {
                            onTimers.Add(counterGroup);
                        }
 
                        int millisecondsTillNextPoll = (int)((counterGroup._nextPollingTimeStamp - now).TotalMilliseconds);
                        millisecondsTillNextPoll = Math.Max(1, millisecondsTillNextPoll);
                        sleepDurationInMilliseconds = Math.Min(sleepDurationInMilliseconds, millisecondsTillNextPoll);
                    }
 
                    if (s_needsResetIncrementingPollingCounters.Count > 0)
                    {
                        countersToReset = s_needsResetIncrementingPollingCounters;
                        s_needsResetIncrementingPollingCounters = [];
                    }
                }
 
                if (countersToReset != null)
                {
                    foreach (IncrementingPollingCounter counter in countersToReset)
                    {
                        counter.UpdateMetric();
                    }
 
                    countersToReset = null;
                }
 
                foreach (CounterGroup onTimer in onTimers)
                {
                    onTimer.OnTimer();
                }
                onTimers.Clear();
                if (sleepDurationInMilliseconds == int.MaxValue)
                {
                    sleepDurationInMilliseconds = -1; // WaitOne uses -1 to mean infinite
                }
                sleepEvent?.WaitOne(sleepDurationInMilliseconds);
            }
        }
 
#endregion // Timer Processing
 
    }
}