From 91952993aa11628c95101de1cee4e16ccad21a8e Mon Sep 17 00:00:00 2001 From: Sung Yoon Whang Date: Fri, 8 Feb 2019 19:58:24 -0800 Subject: [PATCH] move EventCounter to shared CoreLib (dotnet/corefxdotnet/coreclr#35183) Signed-off-by: dotnet-bot Commit migrated from https://github.com/dotnet/coreclr/commit/fc8470a32c9eed9dbfd7ec146fffe8f009f9219d --- .../src/System/Diagnostics/Tracing/EventCounter.cs | 491 +++++++++++++++++++++ 1 file changed, 491 insertions(+) create mode 100644 src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventCounter.cs 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 index 0000000..5d568a5 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventCounter.cs @@ -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 +{ + /// + /// 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. + /// + public class EventCounter : IDisposable + { + /// + /// Initializes a new instance of the class. + /// EVentCounters live as long as the EventSource that they are attached to unless they are + /// explicitly Disposed. + /// + /// The name. + /// The event source. + 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; + } + + /// + /// 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. + /// + /// The value. + public void WriteMetric(float value) + { + Enqueue(value); + } + + /// + /// 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. + /// + 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> + { + 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> GetEnumerator() + { + return ForEnumeration.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ForEnumeration.GetEnumerator(); + } + + private IEnumerable> ForEnumeration + { + get + { + yield return new KeyValuePair("Name", Name); + yield return new KeyValuePair("Mean", Mean); + yield return new KeyValuePair("StandardDeviation", StandardDeviation); + yield return new KeyValuePair("Count", Count); + yield return new KeyValuePair("Min", Min); + yield return new KeyValuePair("Max", Max); + } + } + + #endregion // Implementation of the IEnumerable interface + } + + internal class EventCounterGroup + { + private readonly EventSource _eventSource; + private readonly List _eventCounters; + + internal EventCounterGroup(EventSource eventSource) + { + _eventSource = eventSource; + _eventCounters = new List(); + 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[] 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[eventSourceIndex + 1]; + } + else if (eventSourceIndex >= EventCounterGroup.s_eventCounterGroups.Length) + { + WeakReference[] newEventCounterGroups = new WeakReference[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 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(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(); + } + } + } + + /// + /// This is the payload that is sent in the with EventSource.Write + /// + [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 +} -- 2.7.4