move EventCounter to shared CoreLib (dotnet/corefxdotnet/coreclr#35183)
authorSung Yoon Whang <suwhang@microsoft.com>
Sat, 9 Feb 2019 03:58:24 +0000 (19:58 -0800)
committerJan Kotas <jkotas@microsoft.com>
Sat, 9 Feb 2019 07:14:48 +0000 (23:14 -0800)
Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
Commit migrated from https://github.com/dotnet/coreclr/commit/fc8470a32c9eed9dbfd7ec146fffe8f009f9219d

src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventCounter.cs [new file with mode: 0644]

diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventCounter.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventCounter.cs
new file mode 100644 (file)
index 0000000..5d568a5
--- /dev/null
@@ -0,0 +1,491 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Diagnostics;
+using System.Collections;
+using System.Collections.Generic;
+using System.Threading;
+#if ES_BUILD_PCL
+    using System.Threading.Tasks;
+#endif
+
+#if ES_BUILD_STANDALONE
+namespace Microsoft.Diagnostics.Tracing
+#else
+namespace System.Diagnostics.Tracing
+#endif
+{
+    /// <summary>
+    /// Provides the ability to collect statistics through EventSource
+    /// 
+    /// See https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.Tracing/documentation/EventCounterTutorial.md
+    /// for a tutorial guide.  
+    /// 
+    /// See https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestEventCounter.cs
+    /// which shows tests, which are also useful in seeing actual use.  
+    /// </summary>
+    public class EventCounter : IDisposable
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="EventCounter"/> class.
+        /// EVentCounters live as long as the EventSource that they are attached to unless they are
+        /// explicitly Disposed.   
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <param name="eventSource">The event source.</param>
+        public EventCounter(string name, EventSource eventSource)
+        {
+            if (name == null)
+            {
+                throw new ArgumentNullException(nameof(name));
+            }
+
+            if (eventSource == null)
+            {
+                throw new ArgumentNullException(nameof(eventSource));
+            }
+
+            InitializeBuffer();
+            _name = name;
+            _group = EventCounterGroup.GetEventCounterGroup(eventSource);
+            _group.Add(this);
+            _min = float.PositiveInfinity;
+            _max = float.NegativeInfinity;
+        }
+
+        /// <summary>
+        /// Writes 'value' to the stream of values tracked by the counter.  This updates the sum and other statistics that will 
+        /// be logged on the next timer interval.  
+        /// </summary>
+        /// <param name="value">The value.</param>
+        public void WriteMetric(float value)
+        {
+            Enqueue(value);
+        }
+
+        /// <summary>
+        /// Removes the counter from set that the EventSource will report on.  After being disposed, this
+        /// counter will do nothing and its resource will be reclaimed if all references to it are removed.
+        /// If an EventCounter is not explicitly disposed it will be cleaned up automatically when the
+        /// EventSource it is attached to dies.  
+        /// </summary>
+        public void Dispose()
+        {
+            var group = _group;
+            if (group != null)
+            {
+                group.Remove(this);
+                _group = null;
+            }
+        }
+
+        public override string ToString()
+        {
+            return "EventCounter '" + _name + "' Count " + _count + " Mean " + (((double)_sum) / _count).ToString("n3");
+        }
+        #region private implementation
+
+        private readonly string _name;
+        private EventCounterGroup _group;
+
+        #region Buffer Management
+
+        // Values buffering
+        private const int BufferedSize = 10;
+        private const float UnusedBufferSlotValue = float.NegativeInfinity;
+        private const int UnsetIndex = -1;
+        private volatile float[] _bufferedValues;
+        private volatile int _bufferedValuesIndex;
+
+        // arbitrarily we use _bufferfValues as the lock object.  
+        private object MyLock { get { return _bufferedValues; } }
+
+        private void InitializeBuffer()
+        {
+            _bufferedValues = new float[BufferedSize];
+            for (int i = 0; i < _bufferedValues.Length; i++)
+            {
+                _bufferedValues[i] = UnusedBufferSlotValue;
+            }
+        }
+
+        private void Enqueue(float value)
+        {
+            // It is possible that two threads read the same bufferedValuesIndex, but only one will be able to write the slot, so that is okay.
+            int i = _bufferedValuesIndex;
+            while (true)
+            {
+                float result = Interlocked.CompareExchange(ref _bufferedValues[i], value, UnusedBufferSlotValue);
+                i++;
+                if (_bufferedValues.Length <= i)
+                {
+                    // It is possible that two threads both think the buffer is full, but only one get to actually flush it, the other
+                    // will eventually enter this code path and potentially calling Flushing on a buffer that is not full, and that's okay too.
+                    lock (MyLock) // Lock the counter
+                        Flush();
+                    i = 0;
+                }
+
+                if (result == UnusedBufferSlotValue)
+                {
+                    // CompareExchange succeeded 
+                    _bufferedValuesIndex = i;
+                    return;
+                }
+            }
+        }
+
+        private void Flush()
+        {
+            Debug.Assert(Monitor.IsEntered(MyLock));
+            for (int i = 0; i < _bufferedValues.Length; i++)
+            {
+                var value = Interlocked.Exchange(ref _bufferedValues[i], UnusedBufferSlotValue);
+                if (value != UnusedBufferSlotValue)
+                {
+                    OnMetricWritten(value);
+                }
+            }
+
+            _bufferedValuesIndex = 0;
+        }
+
+        #endregion // Buffer Management
+
+        #region Statistics Calculation
+
+        // Statistics
+        private int _count;
+        private float _sum;
+        private float _sumSquared;
+        private float _min;
+        private float _max;
+
+        private void OnMetricWritten(float value)
+        {
+            Debug.Assert(Monitor.IsEntered(MyLock));
+            _sum += value;
+            _sumSquared += value * value;
+            if (value > _max)
+                _max = value;
+
+            if (value < _min)
+                _min = value;
+
+            _count++;
+        }
+
+        internal EventCounterPayload GetEventCounterPayload()
+        {
+            lock (MyLock)     // Lock the counter
+            {
+                Flush();
+                EventCounterPayload result = new EventCounterPayload();
+                result.Name = _name;
+                result.Count = _count;
+                if (0 < _count)
+                {
+                    result.Mean = _sum / _count;
+                    result.StandardDeviation = (float)Math.Sqrt(_sumSquared / _count - _sum * _sum / _count / _count);
+                }
+                else
+                {
+                    result.Mean = 0;
+                    result.StandardDeviation = 0;
+                }
+                result.Min = _min;
+                result.Max = _max;
+                ResetStatistics();
+                return result;
+            }
+        }
+
+        private void ResetStatistics()
+        {
+            Debug.Assert(Monitor.IsEntered(MyLock));
+            _count = 0;
+            _sum = 0;
+            _sumSquared = 0;
+            _min = float.PositiveInfinity;
+            _max = float.NegativeInfinity;
+        }
+
+        #endregion // Statistics Calculation
+
+        #endregion // private implementation
+    }
+
+    #region internal supporting classes
+
+    [EventData]
+    internal class EventCounterPayload : IEnumerable<KeyValuePair<string, object>>
+    {
+        public string Name { get; set; }
+
+        public float Mean { get; set; }
+
+        public float StandardDeviation { get; set; }
+
+        public int Count { get; set; }
+
+        public float Min { get; set; }
+
+        public float Max { get; set; }
+
+        public float IntervalSec { get; internal set; }
+
+        #region Implementation of the IEnumerable interface
+
+        public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
+        {
+            return ForEnumeration.GetEnumerator();
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return ForEnumeration.GetEnumerator();
+        }
+
+        private IEnumerable<KeyValuePair<string, object>> ForEnumeration
+        {
+            get
+            {
+                yield return new KeyValuePair<string, object>("Name", Name);
+                yield return new KeyValuePair<string, object>("Mean", Mean);
+                yield return new KeyValuePair<string, object>("StandardDeviation", StandardDeviation);
+                yield return new KeyValuePair<string, object>("Count", Count);
+                yield return new KeyValuePair<string, object>("Min", Min);
+                yield return new KeyValuePair<string, object>("Max", Max);
+            }
+        }
+
+        #endregion // Implementation of the IEnumerable interface
+    }
+
+    internal class EventCounterGroup
+    {
+        private readonly EventSource _eventSource;
+        private readonly List<EventCounter> _eventCounters;
+
+        internal EventCounterGroup(EventSource eventSource)
+        {
+            _eventSource = eventSource;
+            _eventCounters = new List<EventCounter>();
+            RegisterCommandCallback();
+        }
+
+        internal void Add(EventCounter eventCounter)
+        {
+            lock (this) // Lock the EventCounterGroup
+                _eventCounters.Add(eventCounter);
+        }
+
+        internal void Remove(EventCounter eventCounter)
+        {
+            lock (this) // Lock the EventCounterGroup
+                _eventCounters.Remove(eventCounter);
+        }
+
+        #region EventSource Command Processing
+
+        private void RegisterCommandCallback()
+        {
+            _eventSource.EventCommandExecuted += OnEventSourceCommand;
+        }
+
+        private void OnEventSourceCommand(object sender, EventCommandEventArgs e)
+        {
+            if (e.Command == EventCommand.Enable || e.Command == EventCommand.Update)
+            {
+                string valueStr;
+                float value;
+                if (e.Arguments.TryGetValue("EventCounterIntervalSec", out valueStr) && float.TryParse(valueStr, out value))
+                {
+                    // Recursion through EventSource callbacks possible.  When we enable the timer
+                    // we synchonously issue a EventSource.Write event, which in turn can call back
+                    // to user code (in an EventListener) while holding this lock.   This is dangerous
+                    // because it mean this code might inadvertantly participate in a lock loop. 
+                    // The scenario seems very unlikely so we ignore that problem for now.  
+                    lock (this)      // Lock the EventCounterGroup
+                    {
+                        EnableTimer(value);
+                    }
+                }
+            }
+        }
+
+        #endregion // EventSource Command Processing
+
+        #region Global EventCounterGroup Array management
+
+        // We need eventCounters to 'attach' themselves to a particular EventSource.   
+        // this table provides the mapping from EventSource -> EventCounterGroup 
+        // which represents this 'attached' information.   
+        private static WeakReference<EventCounterGroup>[] s_eventCounterGroups;
+        private static readonly object s_eventCounterGroupsLock = new object();
+
+        private static void EnsureEventSourceIndexAvailable(int eventSourceIndex)
+        {
+            Debug.Assert(Monitor.IsEntered(s_eventCounterGroupsLock));
+            if (EventCounterGroup.s_eventCounterGroups == null)
+            {
+                EventCounterGroup.s_eventCounterGroups = new WeakReference<EventCounterGroup>[eventSourceIndex + 1];
+            }
+            else if (eventSourceIndex >= EventCounterGroup.s_eventCounterGroups.Length)
+            {
+                WeakReference<EventCounterGroup>[] newEventCounterGroups = new WeakReference<EventCounterGroup>[eventSourceIndex + 1];
+                Array.Copy(EventCounterGroup.s_eventCounterGroups, 0, newEventCounterGroups, 0, EventCounterGroup.s_eventCounterGroups.Length);
+                EventCounterGroup.s_eventCounterGroups = newEventCounterGroups;
+            }
+        }
+
+        internal static EventCounterGroup GetEventCounterGroup(EventSource eventSource)
+        {
+            lock (s_eventCounterGroupsLock)
+            {
+                int eventSourceIndex = EventListener.EventSourceIndex(eventSource);
+                EnsureEventSourceIndexAvailable(eventSourceIndex);
+                WeakReference<EventCounterGroup> weakRef = EventCounterGroup.s_eventCounterGroups[eventSourceIndex];
+                EventCounterGroup ret = null;
+                if (weakRef == null || !weakRef.TryGetTarget(out ret))
+                {
+                    ret = new EventCounterGroup(eventSource);
+                    EventCounterGroup.s_eventCounterGroups[eventSourceIndex] = new WeakReference<EventCounterGroup>(ret);
+                }
+                return ret;
+            }
+        }
+
+        #endregion // Global EventCounterGroup Array management
+
+        #region Timer Processing
+
+        private DateTime _timeStampSinceCollectionStarted;
+        private int _pollingIntervalInMilliseconds;
+        private Timer _pollingTimer;
+
+        private void DisposeTimer()
+        {
+            Debug.Assert(Monitor.IsEntered(this));
+            if (_pollingTimer != null)
+            {
+                _pollingTimer.Dispose();
+                _pollingTimer = null;
+            }
+        }
+
+        private void EnableTimer(float pollingIntervalInSeconds)
+        {
+            Debug.Assert(Monitor.IsEntered(this));
+            if (pollingIntervalInSeconds <= 0)
+            {
+                DisposeTimer();
+                _pollingIntervalInMilliseconds = 0;
+            }
+            else if (_pollingIntervalInMilliseconds == 0 || pollingIntervalInSeconds * 1000 < _pollingIntervalInMilliseconds)
+            {
+                Debug.WriteLine("Polling interval changed at " + DateTime.UtcNow.ToString("mm.ss.ffffff"));
+                _pollingIntervalInMilliseconds = (int)(pollingIntervalInSeconds * 1000);
+                DisposeTimer();
+                _timeStampSinceCollectionStarted = DateTime.UtcNow;
+                // Don't capture the current ExecutionContext and its AsyncLocals onto the timer causing them to live forever
+                bool restoreFlow = false;
+                try
+                {
+                    if (!ExecutionContext.IsFlowSuppressed())
+                    {
+                        ExecutionContext.SuppressFlow();
+                        restoreFlow = true;
+                    }
+
+                    _pollingTimer = new Timer(s => ((EventCounterGroup)s).OnTimer(null), this, _pollingIntervalInMilliseconds, _pollingIntervalInMilliseconds);
+                }
+                finally
+                {
+                    // Restore the current ExecutionContext
+                    if (restoreFlow)
+                        ExecutionContext.RestoreFlow();
+                }
+            }
+            // Always fire the timer event (so you see everything up to this time).  
+            OnTimer(null);
+        }
+
+        private void OnTimer(object state)
+        {
+            Debug.WriteLine("Timer fired at " + DateTime.UtcNow.ToString("mm.ss.ffffff"));
+            lock (this) // Lock the EventCounterGroup
+            {
+                if (_eventSource.IsEnabled())
+                {
+                    DateTime now = DateTime.UtcNow;
+                    TimeSpan elapsed = now - _timeStampSinceCollectionStarted;
+
+                    foreach (var eventCounter in _eventCounters)
+                    {
+                        EventCounterPayload payload = eventCounter.GetEventCounterPayload();
+                        payload.IntervalSec = (float)elapsed.TotalSeconds;
+                        _eventSource.Write("EventCounters", new EventSourceOptions() { Level = EventLevel.LogAlways }, new PayloadType(payload));
+                    }
+                    _timeStampSinceCollectionStarted = now;
+                }
+                else
+                {
+                    DisposeTimer();
+                }
+            }
+        }
+
+        /// <summary>
+        /// This is the payload that is sent in the with EventSource.Write
+        /// </summary>
+        [EventData]
+        class PayloadType
+        {
+            public PayloadType(EventCounterPayload payload) { Payload = payload; }
+            public EventCounterPayload Payload { get; set; }
+        }
+
+        #region PCL timer hack
+
+#if ES_BUILD_PCL
+    internal delegate void TimerCallback(object state);
+
+        internal sealed class Timer : CancellationTokenSource, IDisposable
+        {
+            private int _period;
+            private TimerCallback _callback;
+            private object _state;
+
+            internal Timer(TimerCallback callback, object state, int dueTime, int period)
+            {
+                _callback = callback;
+                _state = state;
+                _period = period;
+                Schedule(dueTime);
+            }
+
+            private void Schedule(int dueTime)
+            {
+                Task.Delay(dueTime, Token).ContinueWith(OnTimer, null, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
+            }
+
+            private void OnTimer(Task t, object s)
+            {
+                Schedule(_period);
+                _callback(_state);
+            }
+
+            public new void Dispose() { base.Cancel(); }
+        }
+#endif
+        #endregion // PCL timer hack
+
+        #endregion // Timer Processing
+
+    }
+
+    #endregion // internal supporting classes
+}