//****************************************************************************************************** // SharedTimerScheduler.cs - Gbtc // // Copyright © 2016, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 11/10/2016 - Stephen C. Wills // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Threading; using GSF.Diagnostics; namespace GSF.Threading { /// /// Represents a timer manager which is the scheduler of . /// /// /// A with the same scheduler will use the same ThreadPool thread to process /// all of the instances in series when they have a common interval. Call /// order, based on registration sequence, will be preserved. /// public sealed class SharedTimerScheduler : IDisposable { #region [ Members ] // Represents a timer scheduler with of a common fire rate private class SharedTimerInstance : IDisposable { #region [ Members ] // Fields private readonly ConcurrentQueue> m_additionalQueueItems; private readonly LinkedList> m_callbacks; private readonly Timer m_timer; private readonly int m_interval; private readonly SharedTimerScheduler m_parentTimer; private readonly object m_syncRunning; private readonly object m_syncStats; private bool m_disposed; // Counter of the number of times an interval has been skipped // because the callbacks did not complete before the timer fired // again since the last time it was reset private long m_skippedIntervals; // Total CPU time spent processing the timer events. // since the last time it was reset private double m_elapsedWorkerTime; // Total number of times the timer events have fired // since the last time it was reset private int m_elapsedIntervals; /// Total number of callbacks that have occurred private int m_sumOfCallbacks; /// Count of the number of timer callbacks that exists in this factory private int m_sharedTimersCount; #endregion #region [ Constructors ] public SharedTimerInstance(SharedTimerScheduler parentTimer, int interval) { if ((object)parentTimer == null) throw new ArgumentNullException(nameof(parentTimer)); if (interval <= 0) throw new ArgumentOutOfRangeException(nameof(interval)); m_additionalQueueItems = new ConcurrentQueue>(); m_syncRunning = new object(); m_syncStats = new object(); m_parentTimer = parentTimer; m_interval = interval; m_callbacks = new LinkedList>(); m_timer = new Timer(Callback, null, interval, interval); } #endregion #region [ Methods ] public WeakAction RegisterCallback(Action callback) { if (m_disposed) throw new ObjectDisposedException(GetType().FullName); WeakAction weakAction = new WeakAction(callback); m_additionalQueueItems.Enqueue(weakAction); return weakAction; } public WeakAction RegisterCallback(WeakAction weakAction) { if (m_disposed) throw new ObjectDisposedException(GetType().FullName); m_additionalQueueItems.Enqueue(weakAction); return weakAction; } private void Callback(object state) { if (m_disposed) return; ShortTime fireTime = ShortTime.Now; bool lockTaken = false; try { Monitor.TryEnter(m_syncRunning, 0, ref lockTaken); if (!lockTaken) { lock (m_syncStats) m_skippedIntervals++; return; } DateTime fireTimeDatetime = fireTime.UtcTime; int loopCount = 0; LinkedListNode> timerAction = m_callbacks.First; while (timerAction != null) { if (m_disposed) return; // Removing the linked list item will invalidate the "Next" property, so we store it LinkedListNode> nextNode = timerAction.Next; try { if (!timerAction.Value.TryInvoke(fireTimeDatetime)) m_callbacks.Remove(timerAction); } catch (Exception ex) { m_parentTimer.m_log.Publish(MessageLevel.Warning, MessageFlags.BugReport, "Shared Timer Factory Exception", "Code that uses SharedTimerFactory should handle its own exceptions.", null, ex); } loopCount++; timerAction = nextNode; } lock (m_syncStats) { m_sharedTimersCount = m_callbacks.Count; m_sumOfCallbacks += loopCount; m_elapsedIntervals++; m_elapsedWorkerTime += fireTime.ElapsedMilliseconds(); } WeakAction newCallbacks; while (m_additionalQueueItems.TryDequeue(out newCallbacks)) m_callbacks.AddLast(newCallbacks); if (m_callbacks.Count == 0) { lock (m_parentTimer.m_syncRoot) { while (m_additionalQueueItems.TryDequeue(out newCallbacks)) m_parentTimer.RegisterCallback(m_interval, newCallbacks); if (m_callbacks.Count == 0) { Dispose(); m_parentTimer.m_schedulesByInterval.Remove(m_interval); } } } } finally { if (lockTaken) Monitor.Exit(m_syncRunning); } } public void Dispose() { m_disposed = true; m_timer.Dispose(); } public void ResetStats() { lock (m_syncStats) { m_skippedIntervals = 0; m_elapsedIntervals = 0; m_elapsedWorkerTime = 0; m_elapsedIntervals = 0; m_sumOfCallbacks = 0; } } public string StatusMessage(ref bool warning, ref bool error) { lock (m_syncStats) { double averageCpuTime = 0; if (m_elapsedIntervals > 0) averageCpuTime = m_elapsedWorkerTime / m_elapsedIntervals; if (m_skippedIntervals > 0) warning = true; if (2 * averageCpuTime > m_interval) warning = true; if (averageCpuTime > m_interval * 2) error = true; return $"Interval: {m_interval} Skipped Intervals: {m_skippedIntervals} Elapsed Intervals: {m_elapsedIntervals} Average CPU Time: {averageCpuTime:N2}ms Sum of Callbacks: {m_sumOfCallbacks} Shared Timers: {m_sharedTimersCount}"; } } #endregion } // Fields private readonly Dictionary m_schedulesByInterval; private readonly object m_syncRoot; // Since it is not expected that many shared timers will exist, log publisher is a member instance. // This will provide the initialization stack so it will be easier to distinguish this instance // of StaticTimer from other instances. private readonly LogPublisher m_log; private readonly ScheduledTask m_reportStatus; private bool m_disposed; #endregion #region [ Constructors ] /// /// Creates a new instance of the class. /// public SharedTimerScheduler() { m_syncRoot = new object(); m_log = Logger.CreatePublisher(typeof(SharedTimerScheduler), MessageClass.Component); m_schedulesByInterval = new Dictionary(); m_reportStatus = new ScheduledTask(); m_reportStatus.Running += ReportStatus; m_reportStatus.Start(60 * 1000); } #endregion #region [ Properties ] /// /// Gets flag that determines if this instance has been disposed. /// public bool IsDisposed => m_disposed; #endregion #region [ Methods ] /// /// Creates a using the current . /// /// The interval of the timer, default is 100. /// A shared timer instance that fires at the given interval. public SharedTimer CreateTimer(int interval = 100) { return new SharedTimer(this, interval); } /// /// Registers the given callback with the timer running at the given interval. /// /// The interval at which to run the timer. /// The action to be performed when the timer is triggered. /// /// The weak reference callback that will be executed when this timer fires. To unregister /// the callback, call . /// [EditorBrowsable(EditorBrowsableState.Never)] internal WeakAction RegisterCallback(int interval, Action callback) { if ((object)callback == null) throw new ArgumentNullException(nameof(callback)); if (interval <= 0) throw new ArgumentOutOfRangeException(nameof(interval)); if (m_disposed) throw new ObjectDisposedException(GetType().FullName); lock (m_syncRoot) { if (m_disposed) throw new ObjectDisposedException(GetType().FullName); SharedTimerInstance instance; if (!m_schedulesByInterval.TryGetValue(interval, out instance)) { instance = new SharedTimerInstance(this, interval); m_schedulesByInterval.Add(interval, instance); } return instance.RegisterCallback(callback); } } private WeakAction RegisterCallback(int interval, WeakAction weakAction) { if ((object)weakAction == null) throw new ArgumentNullException(nameof(weakAction)); if (interval <= 0) throw new ArgumentOutOfRangeException(nameof(interval)); if (m_disposed) throw new ObjectDisposedException(GetType().FullName); lock (m_syncRoot) { if (m_disposed) throw new ObjectDisposedException(GetType().FullName); SharedTimerInstance instance; if (!m_schedulesByInterval.TryGetValue(interval, out instance)) { instance = new SharedTimerInstance(this, interval); m_schedulesByInterval.Add(interval, instance); } return instance.RegisterCallback(weakAction); } } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { // Causes the status to be generated one last time before clearing everything m_reportStatus.Dispose(); lock (m_syncRoot) { m_disposed = true; foreach (SharedTimerInstance instance in m_schedulesByInterval.Values) instance.Dispose(); m_schedulesByInterval.Clear(); } m_log.Publish(MessageLevel.Warning, "Timer Disposed"); } private void ReportStatus(object sender, EventArgs e) { m_reportStatus.Start(60 * 1000); StringBuilder status = new StringBuilder(); bool warning = false; bool error = false; lock (m_syncRoot) { status.AppendLine("Shared Timer Factory Status"); foreach (KeyValuePair item in m_schedulesByInterval) { status.AppendLine(item.Value.StatusMessage(ref warning, ref error)); item.Value.ResetStats(); } } m_log.Publish(error ? MessageLevel.Error : warning ? MessageLevel.Warning : MessageLevel.Info, MessageFlags.SystemHealth, "SharedTimerScheduler Status", status.ToString()); } #endregion } }