479 lines · 17.6 KB · C#
RawDownload
//******************************************************************************************************
//  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
{
    /// <summary>
    /// Precision input timer.
    /// </summary>
    /// <remarks>
    /// This class is used to create highly accurate simulated data inputs aligned to the local clock.<br/>
    /// One static instance of this internal class is created per encountered frame rate.
    /// </remarks>
    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<Exception> m_exceptionHandler;
        private bool m_disposed;

        #endregion

        #region [ Constructors ]

        /// <summary>
        /// Create a new <see cref="PrecisionInputTimer"/> class.
        /// </summary>
        /// <param name="framesPerSecond">Desired frame rate for <see cref="PrecisionTimer"/>.</param>
        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);
        }

        /// <summary>
        /// Releases the unmanaged resources before the <see cref="PrecisionInputTimer"/> object is reclaimed by <see cref="GC"/>.
        /// </summary>
        ~PrecisionInputTimer()
        {
            Dispose(false);
        }

        #endregion

        #region [ Properties ]

        /// <summary>
        /// Gets frames per second for this <see cref="PrecisionInputTimer"/>.
        /// </summary>
        public int FramesPerSecond
        {
            get
            {
                return m_framesPerSecond;
            }
        }

        /// <summary>
        /// Gets array of frame millisecond times for this <see cref="PrecisionInputTimer"/>.
        /// </summary>
        public int[] FrameMilliseconds
        {
            get
            {
                return m_frameMilliseconds;
            }
        }

        /// <summary>
        /// Gets reference count for this <see cref="PrecisionInputTimer"/>.
        /// </summary>
        public int ReferenceCount
        {
            get
            {
                return m_referenceCount;
            }
        }

        /// <summary>
        /// Gets number of resynchronizations that have occurred for this <see cref="PrecisionInputTimer"/>.
        /// </summary>
        public long Resynchronizations
        {
            get
            {
                return m_resynchronizations;
            }
        }

        /// <summary>
        /// Gets time of last frame, in ticks.
        /// </summary>
        public long LastFrameTime
        {
            get
            {
                return m_lastFrameTime;
            }
        }

        /// <summary>
        /// Gets a reference to the frame wait handle.
        /// </summary>
        public ManualResetEventSlim FrameWaitHandle
        {
            get
            {
                if (m_useWaitHandleA)
                    return m_frameWaitHandleA;

                return m_frameWaitHandleB;
            }
        }

        /// <summary>
        /// Gets or sets function used to handle exceptions.
        /// </summary>
        public Action<Exception> ExceptionHandler
        {
            get
            {
                return m_exceptionHandler;
            }
            set
            {
                m_exceptionHandler = value;
            }
        }

        #endregion

        #region [ Methods ]

        /// <summary>
        /// Releases all the resources used by the <see cref="PrecisionInputTimer"/> object.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases the unmanaged resources used by the <see cref="PrecisionInputTimer"/> object and optionally releases the managed resources.
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
        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.
                }
            }
        }

        /// <summary>
        /// Adds a reference to this <see cref="PrecisionInputTimer"/>.
        /// </summary>
        public void AddReference()
        {
            m_referenceCount++;
        }

        /// <summary>
        /// Removes a reference to this <see cref="PrecisionInputTimer"/>.
        /// </summary>
        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<int, PrecisionInputTimer> s_inputTimers = new Dictionary<int, PrecisionInputTimer>();

        // Static Methods
  
        public static PrecisionInputTimer Attach(int framesPerSecond)
        {
            return Attach(framesPerSecond, null);
        }
        
        /// <summary>
        /// Attach to a <see cref="PrecisionInputTimer"/> for the specified <paramref name="framesPerSecond"/>.
        /// </summary>
        /// <param name="framesPerSecond">Desired frames per second for the input timer.</param>
        /// <param name="exceptionHandler">Optional delegate to handle exception reporting from the timer.</param>        
        /// /// <returns>A <see cref="PrecisionInputTimer"/> that can be used for the specified <paramref name="framesPerSecond"/>.</returns>
        public static PrecisionInputTimer Attach(int framesPerSecond, Action<Exception> 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;
        }

        /// <summary>
        /// Detatch from the <see cref="PrecisionInputTimer"/>.
        /// </summary>
        /// <param name="timer">Timer instance to detach from.</param>
        /// <remarks>
        /// Timer reference will be set to <c>null</c> after detatch.
        /// </remarks>
        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
    }
}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t