// 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.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
///
public class EventCounter
{
///
/// Initializes a new instance of the class.
///
/// 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;
EventCounterGroup.AddEventCounter(eventSource, this);
}
///
/// Writes the metric.
///
/// The value.
public void WriteMetric(float value)
{
Enqueue(value);
}
#region private implementation
private readonly string _name;
#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;
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 (_bufferedValues)
{
Flush();
}
i = 0;
}
if (result == UnusedBufferSlotValue)
{
// CompareExchange succeeded
_bufferedValuesIndex = i;
return;
}
}
}
private void Flush()
{
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)
{
_sum += value;
_sumSquared += value * value;
if (_count == 0 || value > _max)
{
_max = value;
}
if (_count == 0 || value < _min)
{
_min = value;
}
_count++;
}
internal EventCounterPayload GetEventCounterPayload()
{
lock (_bufferedValues)
{
Flush();
EventCounterPayload result = new EventCounterPayload();
result.Name = _name;
result.Count = _count;
result.Mean = _sum / _count;
result.StandardDerivation = (float)Math.Sqrt(_sumSquared / _count - _sum * _sum / _count / _count);
result.Min = _min;
result.Max = _max;
ResetStatistics();
return result;
}
}
private void ResetStatistics()
{
_count = 0;
_sum = 0;
_sumSquared = 0;
_min = 0;
_max = 0;
}
#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 StandardDerivation { 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("StandardDerivation", StandardDerivation);
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 : IDisposable
{
private readonly EventSource _eventSource;
private readonly int _eventSourceIndex;
private readonly List _eventCounters;
internal EventCounterGroup(EventSource eventSource, int eventSourceIndex)
{
_eventSource = eventSource;
_eventSourceIndex = eventSourceIndex;
_eventCounters = new List();
RegisterCommandCallback();
}
private void Add(EventCounter eventCounter)
{
_eventCounters.Add(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))
{
EnableTimer(value);
}
}
}
#endregion // EventSource Command Processing
#region Global EventCounterGroup Array management
private static EventCounterGroup[] s_eventCounterGroups;
internal static void AddEventCounter(EventSource eventSource, EventCounter eventCounter)
{
int eventSourceIndex = EventListener.EventSourceIndex(eventSource);
EventCounterGroup.EnsureEventSourceIndexAvailable(eventSourceIndex);
EventCounterGroup eventCounterGroup = GetEventCounterGroup(eventSource);
eventCounterGroup.Add(eventCounter);
}
private static void EnsureEventSourceIndexAvailable(int eventSourceIndex)
{
if (EventCounterGroup.s_eventCounterGroups == null)
{
EventCounterGroup.s_eventCounterGroups = new EventCounterGroup[eventSourceIndex + 1];
}
else if (eventSourceIndex >= EventCounterGroup.s_eventCounterGroups.Length)
{
EventCounterGroup[] newEventCounterGroups = new EventCounterGroup[eventSourceIndex + 1];
Array.Copy(EventCounterGroup.s_eventCounterGroups, 0, newEventCounterGroups, 0, EventCounterGroup.s_eventCounterGroups.Length);
EventCounterGroup.s_eventCounterGroups = newEventCounterGroups;
}
}
private static EventCounterGroup GetEventCounterGroup(EventSource eventSource)
{
int eventSourceIndex = EventListener.EventSourceIndex(eventSource);
EventCounterGroup result = EventCounterGroup.s_eventCounterGroups[eventSourceIndex];
if (result == null)
{
result = new EventCounterGroup(eventSource, eventSourceIndex);
EventCounterGroup.s_eventCounterGroups[eventSourceIndex] = result;
}
return result;
}
#endregion // Global EventCounterGroup Array management
#region Timer Processing
private DateTime _timeStampSinceCollectionStarted;
private int _pollingIntervalInMilliseconds;
private Timer _pollingTimer;
private void EnableTimer(float pollingIntervalInSeconds)
{
if (pollingIntervalInSeconds == 0)
{
if (_pollingTimer != null)
{
_pollingTimer.Dispose();
_pollingTimer = null;
}
_pollingIntervalInMilliseconds = 0;
}
else if (_pollingIntervalInMilliseconds == 0 || pollingIntervalInSeconds < _pollingIntervalInMilliseconds)
{
_pollingIntervalInMilliseconds = (int)(pollingIntervalInSeconds * 1000);
if (_pollingTimer != null)
{
_pollingTimer.Dispose();
_pollingTimer = null;
}
_timeStampSinceCollectionStarted = DateTime.Now;
_pollingTimer = new Timer(OnTimer, null, _pollingIntervalInMilliseconds, _pollingIntervalInMilliseconds);
}
}
private void OnTimer(object state)
{
if (_eventSource.IsEnabled())
{
DateTime now = DateTime.Now;
TimeSpan elapsed = now - _timeStampSinceCollectionStarted;
lock (_pollingTimer)
{
foreach (var eventCounter in _eventCounters)
{
EventCounterPayload payload = eventCounter.GetEventCounterPayload();
payload.IntervalSec = (float)elapsed.TotalSeconds;
_eventSource.Write("EventCounters", new EventSourceOptions() { Level = EventLevel.LogAlways }, new { Payload = payload });
}
_timeStampSinceCollectionStarted = now;
}
}
else
{
_pollingTimer.Dispose();
_pollingTimer = null;
EventCounterGroup.s_eventCounterGroups[_eventSourceIndex] = null;
}
}
#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
#region Implementation of the IDisposable interface
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
if (_pollingTimer != null)
{
_pollingTimer.Dispose();
_pollingTimer = null;
}
}
_disposed = true;
}
#endregion // Implementation of the IDisposable interface
}
#endregion // internal supporting classes
}