--- /dev/null
+// 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
+}