//****************************************************************************************************** // PrecisionInputTimer.cs - Gbtc // // Copyright © 2010, 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 Eclipse Public License -v 1.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/eclipse-1.0.php // // 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: // ---------------------------------------------------------------------------------------------------- // 07/25/2012 - J. Ritchie Carroll // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Threading; namespace GSF.TimeSeries { /// /// Precision input timer. /// /// /// This class is used to create highly accurate simulated data inputs aligned to the local clock.
/// One static instance of this internal class is created per encountered frame rate. ///
public sealed class PrecisionInputTimer : IDisposable { #region [ Members ] // Fields private PrecisionTimer m_timer; private bool m_useWaitHandleA; private SpinLock m_timerTickLock; private ManualResetEventSlim m_frameWaitHandleA; private ManualResetEventSlim m_frameWaitHandleB; private readonly int m_framesPerSecond; private readonly int m_frameWindowSize; private readonly int[] m_frameMilliseconds; private int m_lastFrameIndex; private long m_lastFrameTime; private long m_missedPublicationWindows; private long m_lastMissedWindowTime; private long m_resynchronizations; private int m_referenceCount; private Action m_exceptionHandler; private bool m_disposed; #endregion #region [ Constructors ] /// /// Create a new class. /// /// Desired frame rate for . internal PrecisionInputTimer(int framesPerSecond) { // Create synchronization objects m_timerTickLock = new SpinLock(); m_frameWaitHandleA = new ManualResetEventSlim(false); m_frameWaitHandleB = new ManualResetEventSlim(false); m_useWaitHandleA = true; m_framesPerSecond = framesPerSecond; // Create a new precision timer for this timer state m_timer = new PrecisionTimer(); m_timer.Resolution = 1; m_timer.Period = 1; m_timer.AutoReset = true; // Attach handler for timer ticks m_timer.Tick += m_timer_Tick; m_frameWindowSize = (int)Math.Round(1000.0D / framesPerSecond) * 2; m_frameMilliseconds = new int[framesPerSecond]; for (int frameIndex = 0; frameIndex < framesPerSecond; frameIndex++) { m_frameMilliseconds[frameIndex] = (int)(1.0D / framesPerSecond * (frameIndex * 1000.0D)); } // Start high resolution timer on a separate thread so the start // time can synchronized to the top of the millisecond ThreadPool.QueueUserWorkItem(SynchronizeInputTimer); } /// /// Releases the unmanaged resources before the object is reclaimed by . /// ~PrecisionInputTimer() { Dispose(false); } #endregion #region [ Properties ] /// /// Gets frames per second for this . /// public int FramesPerSecond { get { return m_framesPerSecond; } } /// /// Gets array of frame millisecond times for this . /// public int[] FrameMilliseconds { get { return m_frameMilliseconds; } } /// /// Gets reference count for this . /// public int ReferenceCount { get { return m_referenceCount; } } /// /// Gets number of resynchronizations that have occurred for this . /// public long Resynchronizations { get { return m_resynchronizations; } } /// /// Gets time of last frame, in ticks. /// public long LastFrameTime { get { return m_lastFrameTime; } } /// /// Gets a reference to the frame wait handle. /// public ManualResetEventSlim FrameWaitHandle { get { if (m_useWaitHandleA) return m_frameWaitHandleA; return m_frameWaitHandleB; } } /// /// Gets or sets function used to handle exceptions. /// public Action ExceptionHandler { get { return m_exceptionHandler; } set { m_exceptionHandler = value; } } #endregion #region [ Methods ] /// /// Releases all the resources used by the object. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. private void Dispose(bool disposing) { if (!m_disposed) { try { if (disposing) { if (m_timer != null) { m_timer.Tick -= m_timer_Tick; m_timer.Dispose(); } m_timer = null; if (m_frameWaitHandleA != null) { m_frameWaitHandleA.Set(); m_frameWaitHandleA.Dispose(); } m_frameWaitHandleA = null; if (m_frameWaitHandleB != null) { m_frameWaitHandleB.Set(); m_frameWaitHandleB.Dispose(); } m_frameWaitHandleB = null; } } finally { m_disposed = true; // Prevent duplicate dispose. } } } /// /// Adds a reference to this . /// public void AddReference() { m_referenceCount++; } /// /// Removes a reference to this . /// public void RemoveReference() { m_referenceCount--; } // This timer function is called every millisecond so that frames can be published at the exact desired time private void m_timer_Tick(object sender, EventArgs e) { // Slower systems or systems under stress may have trouble keeping up with a 1-ms timer, so // we only process this code if it's not already processing... bool locked = false; try { m_timerTickLock.TryEnter(2, ref locked); if (locked) { DateTime now = PrecisionTimer.UtcNow; int frameMilliseconds, milliseconds = now.Millisecond; long ticks = now.Ticks; bool releaseTimer = false, resync = false; // Make sure current time is reasonably close to current frame index if (Math.Abs(milliseconds - m_frameMilliseconds[m_lastFrameIndex]) > m_frameWindowSize) m_lastFrameIndex = 0; // See if it is time to publish for (int frameIndex = m_lastFrameIndex; frameIndex < m_frameMilliseconds.Length; frameIndex++) { frameMilliseconds = m_frameMilliseconds[frameIndex]; if (frameMilliseconds == milliseconds) { // See if system skipped a publication window if (m_lastFrameIndex != frameIndex) { // We monitor for missed windows in quick succession (within 1.5 seconds) if (ticks - m_lastMissedWindowTime > 15000000L) { // Threshold has passed since last missed window, so we reset counters m_lastMissedWindowTime = ticks; m_missedPublicationWindows = 0; } m_missedPublicationWindows++; // If the system is starting to skip publications it could need resynchronization, // so in this case we restart the high-resolution timer to get the timer started // closer to the top of the millisecond resync = (m_missedPublicationWindows > 4); } // Prepare index for next check, time moving forward m_lastFrameIndex = frameIndex + 1; if (m_lastFrameIndex >= m_frameMilliseconds.Length) m_lastFrameIndex = 0; if (resync) { if (m_timer != null) { m_timer.Stop(); ThreadPool.QueueUserWorkItem(SynchronizeInputTimer); m_resynchronizations++; } } releaseTimer = true; break; } else if (frameMilliseconds > milliseconds) { // Time has yet to pass, wait till the next tick break; } } if (releaseTimer) { // Baseline timestamp to the top of the millisecond for frame publication m_lastFrameTime = ticks - ticks % Ticks.PerMillisecond; // Pulse all waiting threads toggling between ready handles if (m_useWaitHandleA) { m_frameWaitHandleB.Reset(); m_useWaitHandleA = false; m_frameWaitHandleA.Set(); } else { m_frameWaitHandleA.Reset(); m_useWaitHandleA = true; m_frameWaitHandleB.Set(); } } } } catch (Exception ex) { ExceptionHandler(new InvalidOperationException("Exception thrown by precision input timer: " + ex.Message, ex)); } finally { if (locked) m_timerTickLock.Exit(); } } private void SynchronizeInputTimer(object state) { // Start timer at as close to the top of the millisecond as possible bool repeat = true; long last = 0, next; while (repeat) { next = PrecisionTimer.UtcNow.Ticks % Ticks.PerMillisecond % 1000; repeat = (next > last); last = next; } m_lastMissedWindowTime = 0; m_missedPublicationWindows = 0; if (m_timer != null) m_timer.Start(); } #endregion #region [ Static ] // Static Fields private static readonly Dictionary s_inputTimers = new Dictionary(); // Static Methods public static PrecisionInputTimer Attach(int framesPerSecond) { return Attach(framesPerSecond, null); } /// /// Attach to a for the specified . /// /// Desired frames per second for the input timer. /// Optional delegate to handle exception reporting from the timer. /// /// A that can be used for the specified . public static PrecisionInputTimer Attach(int framesPerSecond, Action exceptionHandler) { PrecisionInputTimer timer; lock (s_inputTimers) { // Get static input timer for given frames per second creating it if needed if (!s_inputTimers.TryGetValue(framesPerSecond, out timer)) { try { // Create a new precision input timer timer = new PrecisionInputTimer(framesPerSecond); timer.ExceptionHandler = exceptionHandler; // Add timer state for given rate to static collection s_inputTimers.Add(framesPerSecond, timer); } catch (Exception ex) { // Process exception for logging if ((object)exceptionHandler != null) exceptionHandler(new InvalidOperationException("Failed to create precision input timer due to exception: " + ex.Message, ex)); else throw; } } // Increment reference count for input timer at given frame rate timer.AddReference(); } return timer; } /// /// Detatch from the . /// /// Timer instance to detach from. /// /// Timer reference will be set to null after detatch. /// public static void Detach(ref PrecisionInputTimer timer) { if ((object)timer != null) { lock (s_inputTimers) { // Verify static frame rate timer for given frames per second exists if (s_inputTimers.ContainsKey(timer.FramesPerSecond)) { // Decrement reference count timer.RemoveReference(); // If timer is no longer being referenced we stop it and remove it from static collection if (timer.ReferenceCount == 0) { timer.Dispose(); s_inputTimers.Remove(timer.FramesPerSecond); } } } } timer = null; } #endregion } }