Add new counter APIs (dotnet/coreclr#23077)
authorSung Yoon Whang <suwhang@microsoft.com>
Wed, 13 Mar 2019 08:10:35 +0000 (01:10 -0700)
committerGitHub <noreply@github.com>
Wed, 13 Mar 2019 08:10:35 +0000 (01:10 -0700)
* Refactor EventCounter and add PollingCounter

* Add PollingCounter

* Add IncrementingEventCounter and IncrementingPollingCounter

* Add MetaData API to Counters

* Some more refactoring

* removing commented out code

* some more cleanup

* build fix and addressing some PR comments

* Addressing PR feedback

* use StringBuilder to generate metadata string

Commit migrated from https://github.com/dotnet/coreclr/commit/d247b06ed9c4dc3e6f625629da7db846be687f2d

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/BaseCounter.cs [new file with mode: 0644]
src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.cs [new file with mode: 0644]
src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterPayload.cs [new file with mode: 0644]
src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventCounter.cs
src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/IncrementingEventCounter.cs [new file with mode: 0644]
src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/IncrementingPollingCounter.cs [new file with mode: 0644]
src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/PollingCounter.cs [new file with mode: 0644]

index ce0f102..133f169 100644 (file)
   </ItemGroup>
   <ItemGroup>
     <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\ActivityTracker.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\BaseCounter.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\CounterGroup.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\CounterPayload.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\EventActivityOptions.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\EventCounter.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\EventDescriptor.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\EventSourceException.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\FrameworkEventSource.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\IEventProvider.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\IncrementingEventCounter.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\IncrementingPollingCounter.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\PollingCounter.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\StubEnvironment.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\Winmeta.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\TraceLogging\ArrayTypeInfo.cs" />
diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/BaseCounter.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/BaseCounter.cs
new file mode 100644 (file)
index 0000000..447852e
--- /dev/null
@@ -0,0 +1,110 @@
+// 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.Text;
+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>
+    /// BaseCounter is an abstract class that serves as the parent class for various Counter* classes, 
+    /// namely EventCounter, PollingCounter, IncrementingEventCounter, and IncrementingPollingCounter.
+    /// </summary>
+    public abstract class BaseCounter : IDisposable
+    {
+        /// <summary>
+        /// All Counters 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 BaseCounter(string name, EventSource eventSource)
+        {
+            if (name == null)
+            {
+                throw new ArgumentNullException(nameof(_name));
+            }
+
+            if (eventSource == null)
+            {
+                throw new ArgumentNullException(nameof(eventSource));
+            }
+
+            _group = CounterGroup.GetCounterGroup(eventSource);
+            _group.Add(this);
+            _eventSource = eventSource;
+            _name = name;
+            _metaData = new Dictionary<string, string>();
+        }
+
+        /// <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()
+        {
+            if (_group != null)
+            {
+                _group.Remove(this);
+                _group = null;
+            }
+        }
+
+        /// <summary>
+        /// Adds a key-value metadata to the EventCounter that will be included as a part of the payload
+        /// </summary>
+        internal void AddMetaData(string key, string value)
+        {
+            lock (MyLock)
+            {
+                _metaData.Add(key, value);
+            }
+        }
+
+        internal string DisplayName { get; set; }
+
+        #region private implementation
+
+        internal readonly string _name;
+
+        private CounterGroup _group;
+        private Dictionary<string, string> _metaData;
+        internal EventSource _eventSource;
+
+        internal abstract void WritePayload(float intervalSec);
+
+        // arbitrarily we use name as the lock object.  
+        internal object MyLock { get { return _name; } }
+
+        internal void ReportOutOfBandMessage(string message)
+        {
+            _eventSource.ReportOutOfBandMessage(message, true);
+        }
+
+        internal string GetMetaDataString()
+        {
+            StringBuilder sb = new StringBuilder("");
+            foreach(KeyValuePair<string, string> kvPair in _metaData)
+            {
+                sb.Append($"{kvPair.Key}:{kvPair.Value},");
+            }
+            return sb.Length == 0 ? "" : sb.ToString(0, sb.Length - 1); // Get rid of the last ","
+        }
+
+        #endregion // private implementation
+    }
+}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.cs
new file mode 100644 (file)
index 0000000..010cbd5
--- /dev/null
@@ -0,0 +1,230 @@
+// 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
+{
+    internal class CounterGroup
+    {
+        private readonly EventSource _eventSource;
+        private readonly List<BaseCounter> _counters;
+
+        internal CounterGroup(EventSource eventSource)
+        {
+            _eventSource = eventSource;
+            _counters = new List<BaseCounter>();
+            RegisterCommandCallback();
+        }
+
+        internal void Add(BaseCounter eventCounter)
+        {
+            lock (this) // Lock the CounterGroup
+                _counters.Add(eventCounter);
+        }
+
+        internal void Remove(BaseCounter eventCounter)
+        {
+            lock (this) // Lock the CounterGroup
+                _counters.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 means 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 CounterGroup
+                    {
+                        EnableTimer(value);
+                    }
+                }
+            }
+        }
+
+        #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 readonly object s_counterGroupsLock = new object();
+
+        private static void EnsureEventSourceIndexAvailable(int eventSourceIndex)
+        {
+            Debug.Assert(Monitor.IsEntered(s_counterGroupsLock));
+            if (CounterGroup.s_counterGroups == null)
+            {
+                CounterGroup.s_counterGroups = new WeakReference<CounterGroup>[eventSourceIndex + 1];
+            }
+            else if (eventSourceIndex >= CounterGroup.s_counterGroups.Length)
+            {
+                WeakReference<CounterGroup>[] newCounterGroups = new WeakReference<CounterGroup>[eventSourceIndex + 1];
+                Array.Copy(CounterGroup.s_counterGroups, 0, newCounterGroups, 0, CounterGroup.s_counterGroups.Length);
+                CounterGroup.s_counterGroups = newCounterGroups;
+            }
+        }
+
+        internal static CounterGroup GetCounterGroup(EventSource eventSource)
+        {
+            lock (s_counterGroupsLock)
+            {
+                int eventSourceIndex = EventListener.EventSourceIndex(eventSource);
+                EnsureEventSourceIndexAvailable(eventSourceIndex);
+                WeakReference<CounterGroup> weakRef = CounterGroup.s_counterGroups[eventSourceIndex];
+                CounterGroup ret = null;
+                if (weakRef == null || !weakRef.TryGetTarget(out ret))
+                {
+                    ret = new CounterGroup(eventSource);
+                    CounterGroup.s_counterGroups[eventSourceIndex] = new WeakReference<CounterGroup>(ret);
+                }
+                return ret;
+            }
+        }
+
+        #endregion // Global CounterGroup 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 => ((CounterGroup)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 CounterGroup
+            {
+                if (_eventSource.IsEnabled())
+                {
+                    DateTime now = DateTime.UtcNow;
+                    TimeSpan elapsed = now - _timeStampSinceCollectionStarted;
+
+                    foreach (var counter in _counters)
+                    {
+                        counter.WritePayload((float)elapsed.TotalSeconds);
+                    }
+                    _timeStampSinceCollectionStarted = now;
+                }
+                else
+                {
+                    DisposeTimer();
+                }
+            }
+        }
+
+        #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
+
+    }
+}
\ No newline at end of file
diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterPayload.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterPayload.cs
new file mode 100644 (file)
index 0000000..e7dd90d
--- /dev/null
@@ -0,0 +1,164 @@
+// 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
+{
+    // TODO: This should be removed as we make the new payloads public
+    [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
+    }
+
+    [EventData]
+    internal class CounterPayload : IEnumerable<KeyValuePair<string, object>>
+    {
+        public string Name { get; set; }
+
+        public string DisplayName { 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; }
+
+        public string MetaData { get; 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>("DisplayName", DisplayName);
+                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);
+                yield return new KeyValuePair<string, object>("IntervalSec", IntervalSec);
+                yield return new KeyValuePair<string, object>("Series", $"Interval={IntervalSec}");
+                yield return new KeyValuePair<string, object>("CounterType", "Mean");
+                yield return new KeyValuePair<string, object>("MetaData", MetaData);
+            }
+        }
+
+        #endregion // Implementation of the IEnumerable interface
+    }
+
+    [EventData]
+    internal class IncrementingCounterPayload : IEnumerable<KeyValuePair<string, object>>
+    {
+        public string Name { get; set; }
+
+        public string DisplayName { get; set; }
+
+        public TimeSpan DisplayRateTimeScale { get; set; }
+
+        public float Increment { get; set; }
+
+        public float IntervalSec { get; internal set; }
+
+        public string MetaData { get; 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>("DisplayName", DisplayName);
+                yield return new KeyValuePair<string, object>("DisplayRateTimeScale", DisplayRateTimeScale.ToString("c"));
+                yield return new KeyValuePair<string, object>("Increment", Increment);
+                yield return new KeyValuePair<string, object>("IntervalSec", IntervalSec);
+                yield return new KeyValuePair<string, object>("Series", $"Interval={IntervalSec}");
+                yield return new KeyValuePair<string, object>("CounterType", "Sum");
+                yield return new KeyValuePair<string, object>("MetaData", MetaData);
+            }
+        }
+
+        #endregion // Implementation of the IEnumerable interface
+    }
+}
\ No newline at end of file
index d0e9638..85fc40a 100644 (file)
@@ -26,7 +26,7 @@ namespace System.Diagnostics.Tracing
     /// 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 partial class EventCounter : IDisposable
+    public partial class EventCounter : BaseCounter
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="EventCounter"/> class.
@@ -35,24 +35,12 @@ namespace System.Diagnostics.Tracing
         /// </summary>
         /// <param name="name">The name.</param>
         /// <param name="eventSource">The event source.</param>
-        public EventCounter(string name, EventSource eventSource)
+        public EventCounter(string name, EventSource eventSource) : base(name, 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;
+
+            InitializeBuffer();
         }
 
         /// <summary>
@@ -65,94 +53,7 @@ namespace System.Diagnostics.Tracing
             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
+        public override string ToString() => $"EventCounter '{_name}' Count {_count} Mean {(((double)_sum) / _count).ToString("n3")}";
 
         #region Statistics Calculation
 
@@ -163,7 +64,7 @@ namespace System.Diagnostics.Tracing
         private float _min;
         private float _max;
 
-        private void OnMetricWritten(float value)
+        internal void OnMetricWritten(float value)
         {
             Debug.Assert(Monitor.IsEntered(MyLock));
             _sum += value;
@@ -177,31 +78,31 @@ namespace System.Diagnostics.Tracing
             _count++;
         }
 
-        internal EventCounterPayload GetEventCounterPayload()
+        internal override void WritePayload(float intervalSec)
         {
-            lock (MyLock)     // Lock the counter
+            lock (MyLock)
             {
                 Flush();
-                EventCounterPayload result = new EventCounterPayload();
-                result.Name = _name;
-                result.Count = _count;
+                EventCounterPayload payload = new EventCounterPayload();
+                payload.Name = _name;
+                payload.Count = _count;
+                payload.IntervalSec = intervalSec;
                 if (0 < _count)
                 {
-                    result.Mean = _sum / _count;
-                    result.StandardDeviation = (float)Math.Sqrt(_sumSquared / _count - _sum * _sum / _count / _count);
+                    payload.Mean = _sum / _count;
+                    payload.StandardDeviation = (float)Math.Sqrt(_sumSquared / _count - _sum * _sum / _count / _count);
                 }
                 else
                 {
-                    result.Mean = 0;
-                    result.StandardDeviation = 0;
+                    payload.Mean = 0;
+                    payload.StandardDeviation = 0;
                 }
-                result.Min = _min;
-                result.Max = _max;
+                payload.Min = _min;
+                payload.Max = _max;
                 ResetStatistics();
-                return result;
+                _eventSource.Write("EventCounters", new EventSourceOptions() { Level = EventLevel.LogAlways }, new EventCounterPayloadType(payload));
             }
         }
-
         private void ResetStatistics()
         {
             Debug.Assert(Monitor.IsEntered(MyLock));
@@ -214,278 +115,73 @@ namespace System.Diagnostics.Tracing
 
         #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();
+        // 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;
 
-        private static void EnsureEventSourceIndexAvailable(int eventSourceIndex)
+        private void InitializeBuffer()
         {
-            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)
+            _bufferedValues = new float[BufferedSize];
+            for (int i = 0; i < _bufferedValues.Length; i++)
             {
-                WeakReference<EventCounterGroup>[] newEventCounterGroups = new WeakReference<EventCounterGroup>[eventSourceIndex + 1];
-                Array.Copy(EventCounterGroup.s_eventCounterGroups, 0, newEventCounterGroups, 0, EventCounterGroup.s_eventCounterGroups.Length);
-                EventCounterGroup.s_eventCounterGroups = newEventCounterGroups;
+                _bufferedValues[i] = UnusedBufferSlotValue;
             }
         }
 
-        internal static EventCounterGroup GetEventCounterGroup(EventSource eventSource)
+        protected void Enqueue(float value)
         {
-            lock (s_eventCounterGroupsLock)
+            // 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)
             {
-                int eventSourceIndex = EventListener.EventSourceIndex(eventSource);
-                EnsureEventSourceIndexAvailable(eventSourceIndex);
-                WeakReference<EventCounterGroup> weakRef = EventCounterGroup.s_eventCounterGroups[eventSourceIndex];
-                EventCounterGroup ret = null;
-                if (weakRef == null || !weakRef.TryGetTarget(out ret))
+                float result = Interlocked.CompareExchange(ref _bufferedValues[i], value, UnusedBufferSlotValue);
+                i++;
+                if (_bufferedValues.Length <= i)
                 {
-                    ret = new EventCounterGroup(eventSource);
-                    EventCounterGroup.s_eventCounterGroups[eventSourceIndex] = new WeakReference<EventCounterGroup>(ret);
+                    // 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;
                 }
-                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
+                if (result == UnusedBufferSlotValue)
                 {
-                    // Restore the current ExecutionContext
-                    if (restoreFlow)
-                        ExecutionContext.RestoreFlow();
+                    // CompareExchange succeeded 
+                    _bufferedValuesIndex = i;
+                    return;
                 }
             }
-            // Always fire the timer event (so you see everything up to this time).  
-            OnTimer(null);
         }
 
-        private void OnTimer(object state)
+        protected void Flush()
         {
-            Debug.WriteLine("Timer fired at " + DateTime.UtcNow.ToString("mm.ss.ffffff"));
-            lock (this) // Lock the EventCounterGroup
+            Debug.Assert(Monitor.IsEntered(MyLock));
+            for (int i = 0; i < _bufferedValues.Length; i++)
             {
-                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
+                var value = Interlocked.Exchange(ref _bufferedValues[i], UnusedBufferSlotValue);
+                if (value != UnusedBufferSlotValue)
                 {
-                    DisposeTimer();
+                    OnMetricWritten(value);
                 }
             }
-        }
-
-        /// <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(); }
+            _bufferedValuesIndex = 0;
         }
-#endif
-        #endregion // PCL timer hack
+    }
 
-        #endregion // Timer Processing
 
+    /// <summary>
+    /// This is the payload that is sent in the with EventSource.Write
+    /// </summary>
+    [EventData]
+    class EventCounterPayloadType
+    {
+        public EventCounterPayloadType(EventCounterPayload payload) { Payload = payload; }
+        public EventCounterPayload Payload { get; set; }
     }
 
-    #endregion // internal supporting classes
 }
diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/IncrementingEventCounter.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/IncrementingEventCounter.cs
new file mode 100644 (file)
index 0000000..05f6d43
--- /dev/null
@@ -0,0 +1,86 @@
+// 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>
+    /// IncrementingEventCounter is a variant of EventCounter for variables that are ever-increasing. 
+    /// Ex) # of exceptions in the runtime.
+    /// It does not calculate statistics like mean, standard deviation, etc. because it only accumulates
+    /// the counter value.
+    /// </summary>
+    internal partial class IncrementingEventCounter : BaseCounter
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="IncrementingEventCounter"/> class.
+        /// IncrementingEventCounter 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 IncrementingEventCounter(string name, EventSource eventSource) : base(name, eventSource)
+        {
+        }
+
+        /// <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="increment">The value to increment by.</param>
+        public void Increment(float increment = 1)
+        {
+            lock(MyLock)
+            {
+                _increment += increment;
+            }
+        }
+
+        internal TimeSpan DisplayRateTimeScale { get; set; }
+        private float _increment;
+        private float _prevIncrement;
+
+        public override string ToString() => $"IncrementingEventCounter '{_name}' Increment {_increment}";
+
+        internal override void WritePayload(float intervalSec)
+        {
+            lock (MyLock)     // Lock the counter
+            {
+                IncrementingCounterPayload payload = new IncrementingCounterPayload();
+                payload.Name = _name;
+                payload.IntervalSec = intervalSec;
+                payload.DisplayName = DisplayName;
+                payload.DisplayRateTimeScale = DisplayRateTimeScale;
+                payload.MetaData = GetMetaDataString();
+                payload.Increment = _increment - _prevIncrement;
+                _prevIncrement = _increment;
+                _eventSource.Write("EventCounters", new EventSourceOptions() { Level = EventLevel.LogAlways }, new IncrementingEventCounterPayloadType(payload));
+            }
+        }
+    }
+
+
+    /// <summary>
+    /// This is the payload that is sent in the with EventSource.Write
+    /// </summary>
+    [EventData]
+    class IncrementingEventCounterPayloadType
+    {
+        public IncrementingEventCounterPayloadType(IncrementingCounterPayload payload) { Payload = payload; }
+        public IncrementingCounterPayload Payload { get; set; }
+    }
+
+}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/IncrementingPollingCounter.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/IncrementingPollingCounter.cs
new file mode 100644 (file)
index 0000000..d9f640c
--- /dev/null
@@ -0,0 +1,96 @@
+// 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>
+    /// IncrementingPollingCounter is a variant of EventCounter for variables that are ever-increasing. 
+    /// Ex) # of exceptions in the runtime.
+    /// It does not calculate statistics like mean, standard deviation, etc. because it only accumulates
+    /// the counter value.
+    /// Unlike IncrementingEventCounter, this takes in a polling callback that it can call to update 
+    /// its own metric periodically.
+    /// </summary>
+    internal partial class IncrementingPollingCounter : BaseCounter
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="IncrementingPollingCounter"/> class.
+        /// IncrementingPollingCounter 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 IncrementingPollingCounter(string name, EventSource eventSource, Func<float> getCountFunction) : base(name, eventSource)
+        {
+            _getCountFunction = getCountFunction;
+        }
+
+        public override string ToString() => $"IncrementingPollingCounter '{_name}' Increment {_increment}";
+
+        internal TimeSpan DisplayRateTimeScale { get; set; }
+        private float _increment;
+        private float _prevIncrement;
+        private Func<float> _getCountFunction;
+
+        /// <summary>
+        /// Calls "_getCountFunction" to enqueue the counter value to the queue. 
+        /// </summary>
+        private void UpdateMetric()
+        {
+            try
+            {
+                lock(MyLock)
+                {
+                    _increment += _getCountFunction();
+                }
+            }
+            catch (Exception ex)
+            {
+                ReportOutOfBandMessage($"ERROR: Exception during EventCounter {_name} getMetricFunction callback: " + ex.Message);
+            }
+        }
+
+        internal override void WritePayload(float intervalSec)
+        {
+            UpdateMetric();
+            lock (MyLock)     // Lock the counter
+            {
+                IncrementingCounterPayload payload = new IncrementingCounterPayload();
+                payload.Name = _name;
+                payload.DisplayName = DisplayName;
+                payload.DisplayRateTimeScale = DisplayRateTimeScale;
+                payload.IntervalSec = intervalSec;
+                payload.MetaData = GetMetaDataString();
+                payload.Increment = _increment - _prevIncrement;
+                _prevIncrement = _increment;
+                _eventSource.Write("EventCounters", new EventSourceOptions() { Level = EventLevel.LogAlways }, new IncrementingPollingCounterPayloadType(payload));
+            }
+        }
+    }
+
+
+    /// <summary>
+    /// This is the payload that is sent in the with EventSource.Write
+    /// </summary>
+    [EventData]
+    class IncrementingPollingCounterPayloadType
+    {
+        public IncrementingPollingCounterPayloadType(IncrementingCounterPayload payload) { Payload = payload; }
+        public IncrementingCounterPayload Payload { get; set; }
+    }
+
+}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/PollingCounter.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/PollingCounter.cs
new file mode 100644 (file)
index 0000000..364e50d
--- /dev/null
@@ -0,0 +1,82 @@
+// 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>
+    /// PollingCounter is a variant of EventCounter - it collects and calculates similar statistics 
+    /// as EventCounter. PollingCounter differs from EventCounter in that it takes in a callback
+    /// function to collect metrics on its own rather than the user having to call WriteMetric() 
+    /// every time.
+    /// </summary>
+    internal partial class PollingCounter : BaseCounter
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PollingCounter"/> class.
+        /// PollingCounter 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 PollingCounter(string name, EventSource eventSource, Func<float> getMetricFunction) : base(name, eventSource)
+        {
+            _getMetricFunction = getMetricFunction;
+        }
+
+        public override string ToString() => $"PollingCounter '{_name}' Count {1} Mean {_lastVal.ToString("n3")}";
+
+        private Func<float> _getMetricFunction;
+        private float _lastVal;
+
+        internal override void WritePayload(float intervalSec)
+        {
+            lock (MyLock)
+            {
+                float value = 0;
+                try 
+                {
+                    value = _getMetricFunction();
+                }
+                catch (Exception ex)
+                {
+                    ReportOutOfBandMessage($"ERROR: Exception during EventCounter {_name} getMetricFunction callback: " + ex.Message);
+                }
+
+                CounterPayload payload = new CounterPayload();
+                payload.Name = _name;
+                payload.Count = 1; // NOTE: These dumb-looking statistics is intentional
+                payload.IntervalSec = intervalSec;
+                payload.Mean = value;
+                payload.Max = value;
+                payload.Min = value;
+                payload.StandardDeviation = 0;
+                _lastVal = value;
+                _eventSource.Write("EventCounters", new EventSourceOptions() { Level = EventLevel.LogAlways }, new PollingPayloadType(payload));
+            }
+        }
+    }
+
+    /// <summary>
+    /// This is the payload that is sent in the with EventSource.Write
+    /// </summary>
+    [EventData]
+    class PollingPayloadType
+    {
+        public PollingPayloadType(CounterPayload payload) { Payload = payload; }
+        public CounterPayload Payload { get; set; }
+    }
+}