// 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. #if ES_BUILD_STANDALONE using System; using System.Diagnostics; #endif using System.Threading; #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 partial class EventCounter : DiagnosticCounter { /// /// 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) : base(name, eventSource) { _min = double.PositiveInfinity; _max = double.NegativeInfinity; InitializeBuffer(); Publish(); } /// /// 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((double)value); } public void WriteMetric(double value) { Enqueue(value); } public override string ToString() { int count = Volatile.Read(ref _count); return count == 0 ? $"EventCounter '{Name}' Count 0" : $"EventCounter '{Name}' Count {count} Mean {(_sum / count).ToString("n3")}"; } #region Statistics Calculation // Statistics private int _count; private double _sum; private double _sumSquared; private double _min; private double _max; internal void OnMetricWritten(double value) { Debug.Assert(Monitor.IsEntered(this)); _sum += value; _sumSquared += value * value; if (value > _max) _max = value; if (value < _min) _min = value; _count++; } internal override void WritePayload(float intervalSec, int pollingIntervalMillisec) { lock (this) { Flush(); CounterPayload payload = new CounterPayload(); payload.Count = _count; payload.IntervalSec = intervalSec; if (0 < _count) { payload.Mean = _sum / _count; payload.StandardDeviation = Math.Sqrt(_sumSquared / _count - _sum * _sum / _count / _count); } else { payload.Mean = 0; payload.StandardDeviation = 0; } payload.Min = _min; payload.Max = _max; payload.Series = $"Interval={pollingIntervalMillisec}"; // TODO: This may need to change when we support multi-session payload.CounterType = "Mean"; payload.Metadata = GetMetadataString(); payload.DisplayName = DisplayName ?? ""; payload.DisplayUnits = DisplayUnits ?? ""; payload.Name = Name; ResetStatistics(); EventSource.Write("EventCounters", new EventSourceOptions() { Level = EventLevel.LogAlways }, new CounterPayloadType(payload)); } } internal void ResetStatistics() { lock (this) { _count = 0; _sum = 0; _sumSquared = 0; _min = double.PositiveInfinity; _max = double.NegativeInfinity; } } #endregion // Statistics Calculation // Values buffering private const int BufferedSize = 10; private const double UnusedBufferSlotValue = double.NegativeInfinity; private volatile double[] _bufferedValues = null!; private volatile int _bufferedValuesIndex; private void InitializeBuffer() { _bufferedValues = new double[BufferedSize]; for (int i = 0; i < _bufferedValues.Length; i++) { _bufferedValues[i] = UnusedBufferSlotValue; } } private void Enqueue(double 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) { double 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 (this) // Lock the counter Flush(); i = 0; } if (result == UnusedBufferSlotValue) { // CompareExchange succeeded _bufferedValuesIndex = i; return; } } } protected void Flush() { Debug.Assert(Monitor.IsEntered(this)); for (int i = 0; i < _bufferedValues.Length; i++) { double value = Interlocked.Exchange(ref _bufferedValues[i], UnusedBufferSlotValue); if (value != UnusedBufferSlotValue) { OnMetricWritten(value); } } _bufferedValuesIndex = 0; } } /// /// This is the payload that is sent in the with EventSource.Write /// [EventData] internal class CounterPayloadType { public CounterPayloadType(CounterPayload payload) { Payload = payload; } public CounterPayload Payload { get; set; } } }