//******************************************************************************************************// ConcentratorBase.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:// ----------------------------------------------------------------------------------------------------// 09/02/2010 - J. Ritchie Carroll// Generated original version of source code.// 02/08/2011 - J. Ritchie Carroll// Added QueueState property to expose real-time queue state analysis.// 04/14/2011 - J. Ritchie Carroll// Added ProcessByReceivedTimestamp property to sort and publish measurements by received time.// 05/06/2011 - J. Ritchie Carroll / Jian (Ryan) Zuo// Updated to reduce lock contention and added volatile reads on stats for better accuracy.////****************************************************************************************************** using GSF.Units;using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading; namespace GSF.TimeSeries{ #region [ Enumerations ] /// <summary> /// Downsampling method enumeration. /// </summary> public enum DownsamplingMethod { /// <summary> /// Downsamples to the last measurement received. /// </summary> /// <remarks> /// Use this option if no downsampling is needed or the selected value is not critical. This is the fastest option if the incoming and outgoing frame rates match. /// </remarks> LastReceived, /// <summary> /// Downsamples to the measurement closest to frame time. /// </summary> /// <remarks> /// This is the typical operation used when performing simple downsampling. This is the fastest option if the incoming frame rate is faster than the outgoing frame rate. /// </remarks> Closest, /// <summary> /// Downsamples by applying a user-defined value filter over all received measurements to anti-alias the results. /// </summary> /// <remarks> /// This option will produce the best result but has a processing penalty. /// </remarks> Filtered, /// <summary> /// Downsamples to the measurement that has the best quality closest to frame time. /// </summary> /// <remarks> /// This option chooses the measurement closest to the frame time with the best quality. /// </remarks> BestQuality } #endregion /// <summary> /// Measurement concentrator base class. /// </summary> /// <remarks> /// <para> /// This class synchronizes (i.e., sorts by timestamp) real-time measurements. /// </para> /// <para> /// Note that your lag time should be defined as it relates to the rate at which data data is coming /// into the concentrator. Make sure you allow enough time for transmission of data over the network /// allowing any needed time for possible network congestion. Lead time should be defined as your /// confidence in the accuracy of your local clock (e.g., if you set lead time to 2, this means you /// trust that your local clock is within plus or minus 2 seconds of real-time.) /// </para> /// <para> /// This concentrator is designed to sort measurements being transmitted in real-time for data being /// sent at rates of at least 1 sample per second. Slower rates (e.g., once every few seconds) are not /// supported since sorting data at these speeds would be trivial. There is no defined maximum number /// of supported samples per second - but keep in mind that CPU utilization will increase as the /// measurement volume and frame rate increase. /// </para> /// </remarks> public abstract class ConcentratorBase : IDisposable { #region [ Members ] // Nested Types /// <summary> /// Frame rate timer. /// </summary> /// <remarks> /// One static instance of this internal class is created per encountered frame rate / processing interval. /// </remarks> private sealed class FrameRateTimer : IDisposable { #region [ Members ] // Fields private PrecisionTimer m_timer; private int m_framesPerSecond; private int m_processingInterval; private int[] m_framePeriods; private int m_frameIndex; private int m_lastFramePeriod; private int m_referenceCount; private bool m_disposed; #endregion #region [ Constructors ] /// <summary> /// Create a new <see cref="FrameRateTimer"/> class. /// </summary> /// <param name="framesPerSecond">Desired frame rate for <see cref="PrecisionTimer"/>.</param> /// <param name="processingInterval">Desired processing interval, if applicable.</param> /// <remarks> /// When the <paramref name="processingInterval"/> is set to -1, the frame rate timer interval will be calculated as a distribution /// of whole milliseonds over the specified number of <paramref name="framesPerSecond"/>. Otherwise the specified /// <paramref name="processingInterval"/> will be used as the timer interval. /// </remarks> public FrameRateTimer(int framesPerSecond, int processingInterval) { if (processingInterval == 0) throw new InvalidOperationException("A frame rate timer should not be created when using a processing interval of zero, i.e., processing data as fast as possible."); m_framesPerSecond = framesPerSecond; m_processingInterval = processingInterval; // Create a new precision timer for this timer state m_timer = new PrecisionTimer(); m_timer.AutoReset = true; if (processingInterval > 0) { // Establish fixed timer period m_timer.Period = processingInterval; } else { // Attach handler for timer period assignments m_timer.Tick += SetTimerPeriod; // Calculate distributed wait time periods over specified number of frames per second m_framePeriods = new int[framesPerSecond]; for (int frameIndex = 0; frameIndex < framesPerSecond; frameIndex++) { m_framePeriods[frameIndex] = CalcWaitTimeForFrameIndex(frameIndex); } // Establish initial timer period m_lastFramePeriod = m_framePeriods[0]; m_timer.Period = m_lastFramePeriod; } // Start timer m_timer.Start(); } /// <summary> /// Releases the unmanaged resources before the <see cref="FrameRateTimer"/> object is reclaimed by <see cref="GC"/>. /// </summary> ~FrameRateTimer() { Dispose(false); } #endregion #region [ Properties ] /// <summary> /// Gets frames per second for this <see cref="FrameRateTimer"/>. /// </summary> public int FramesPerSecond { get { return m_framesPerSecond; } } /// <summary> /// Gets the processing interval defined for the <see cref="FrameRateTimer"/>. /// </summary> public int ProcessingInterval { get { return m_processingInterval; } } /// <summary> /// Gets reference count for this <see cref="FrameRateTimer"/>. /// </summary> public int ReferenceCount { get { return m_referenceCount; } } #endregion #region [ Methods ] /// <summary> /// Releases all the resources used by the <see cref="FrameRateTimer"/> object. /// </summary> public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// <summary> /// Releases the unmanaged resources used by the <see cref="FrameRateTimer"/> 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) { if (m_processingInterval == -1) m_timer.Tick -= SetTimerPeriod; m_timer.Dispose(); } m_timer = null; } } finally { m_disposed = true; // Prevent duplicate dispose. } } } /// <summary> /// Adds a reference to this <see cref="FrameRateTimer"/>. /// </summary> /// <param name="tickFunction">Tick function to add to event list.</param> public void AddReference(EventHandler tickFunction) { m_timer.Tick += tickFunction; m_referenceCount++; } /// <summary> /// Removes a reference to this <see cref="FrameRateTimer"/>. /// </summary> /// <param name="tickFunction">Tick function to remove from event list.</param> public void RemoveReference(EventHandler tickFunction) { m_timer.Tick -= tickFunction; m_referenceCount--; } // Handler to assign next timer period private void SetTimerPeriod(object sender, EventArgs e) { int period; // First things first, prepare timer period for next call... m_frameIndex++; if (m_frameIndex >= m_framesPerSecond) m_frameIndex = 0; // Get the frame period for this frame index period = m_framePeriods[m_frameIndex]; // We only update timer period if it has changed since last call. Note that this is necessary since // timer periods are defined as integers but actual period is typically uneven (e.g., 33.333 ms) if (m_lastFramePeriod != period) m_timer.Period = period; m_lastFramePeriod = period; } // Wait times are not necessarily perfectly even (e.g., at 30 samples per second wait time per frame is 33.333... milliseconds) // so we use this function to evenly distribute integer based millisecond wait times across a second. private int CalcWaitTimeForFrameIndex(int frameIndex) { // Jian Zuo... int millisecondsWaitTime; int frameRate; int deficit; frameRate = (int)(Math.Round(1000.0D / m_framesPerSecond)); deficit = 1000 - frameRate * m_framesPerSecond; if (deficit == 0) { millisecondsWaitTime = frameRate; } else { if (frameIndex == 0) { millisecondsWaitTime = frameRate; } else if (frameIndex == m_framesPerSecond - 1) { millisecondsWaitTime = frameRate + (deficit > 0 ? 1 : -1); } else { double interval = m_framesPerSecond / Math.Abs((double)deficit); double pre_dis = mod_dis(frameIndex - 1, interval); double cur_dis = mod_dis(frameIndex, interval); double next_dis = mod_dis(frameIndex + 1, interval); millisecondsWaitTime = frameRate + ((cur_dis <= pre_dis && cur_dis < next_dis) ? (deficit > 0 ? 1 : -1) : 0); } } return millisecondsWaitTime; } private double mod_dis(int framesIndex, double interval) { double dis2 = (framesIndex + 1) % interval; double dis1 = interval - dis2; return (dis1 < dis2 ? dis1 : dis2); } #endregion } // Events /// <summary> /// This event is raised every 5 seconds allowing consumer to track current number of unpublished seconds of data in the queue. /// </summary> /// <remarks> /// <see cref="EventArgs{T}.Argument"/> is the total number of unpublished seconds of data. /// </remarks> public event EventHandler<EventArgs<int>> UnpublishedSamples; /// <summary> /// This event is raised if there is an exception encountered while attempting to process a frame in the sample queue. /// </summary> /// <remarks> /// <para> /// <see cref="EventArgs{T}.Argument"/> is the <see cref="Exception"/> encountered while parsing the data stream. /// </para> /// <para> /// Processing will not stop for any exceptions thrown by the user function, but any captured exceptions will be exposed through this event. /// </para> /// </remarks> public event EventHandler<EventArgs<Exception>> ProcessException; /// <summary> /// This event is raised if there are any measurements being discarded during the sorting process. /// </summary> /// <remarks> /// <see cref="EventArgs{T}.Argument"/> is the enumeration of <see cref="IMeasurement"/> values that are being discarded during the sorting process. /// </remarks> public event EventHandler<EventArgs<IEnumerable<IMeasurement>>> DiscardingMeasurements; /// <summary> /// This event is raised when <see cref="ConcentratorBase"/> is disposed. /// </summary> public event EventHandler Disposed; /// <summary> /// Raised, for the benefit of dependent classes, when lag time is updated. /// </summary> internal event Action<double> LagTimeUpdated; /// <summary> /// Raised, for the benefit of dependent classes, when lead time is updated. /// </summary> internal event Action<double> LeadTimeUpdated; // Fields private FrameQueue m_frameQueue; // Queue of frames to be published private Thread m_publicationThread; // Thread that handles frame publication private AutoResetEvent m_publicationWaitHandle; // Interframe publication wait handle private AutoResetEvent[] m_externalEventHandles; // External event wait handles private int m_externalEventTimeout; // External event timeout private bool m_usePrecisionTimer; // Flag that enables use of precision timer (over just simple thread sleep) private bool m_attachedToFrameRateTimer; // Flag that tracks if instance is attached to a frame rate timer private System.Timers.Timer m_monitorTimer; // Sample monitor - tracks total number of unpublished frames private int m_framesPerSecond; // Frames per second private double m_ticksPerFrame; // Ticks per frame private double m_lagTime; // Allowed past time deviation tolerance, in seconds private double m_leadTime; // Allowed future time deviation tolerance, in seconds private long m_timeResolution; // Maximum sorting resolution in ticks private int m_processingInterval; // Defines a specific processing interval for data, if desired private DownsamplingMethod m_downsamplingMethod; // Downsampling method to use if input is at a higher-resolution than output private double m_timeOffset; // Half the distance of the time resolution used for index calculation private int m_maximumPublicationTimeout; // Maximum publication wait timeout private Ticks m_lagTicks; // Current lag time calculated in ticks private volatile bool m_enabled; // Enabled state of concentrator private long m_startTime; // Start time of concentrator private long m_stopTime; // Stop time of concentrator private long m_realTimeTicks; // Timestamp of real-time or the most recently received measurement private bool m_ignoreBadTimestamps; // Determines whether or not to ignore bad timestamps when sorting measurements private bool m_allowSortsByArrival; // Determines whether or not to sort incoming measurements with a bad timestamp by arrival private bool m_useLocalClockAsRealTime; // Determines whether or not to use local system clock as "real-time" private bool m_allowPreemptivePublishing; // Determines whether or not to preemptively publish frame if expected measurements arrive private bool m_performTimestampReasonabilityCheck; // Determines whether or not to execute timestamp reasonability checks (i.e., lead time validation) private bool m_processByReceivedTimestamp; // Determines whether or not to sort and publish measurements by their ReceivedTimestamp private bool m_trackPublishedTimestamp; // Determines whether or not to track timestamp of publication for all processed frames and measurements private int m_expectedMeasurements; // Expected number of measurements to be sorted into a frame private long m_receivedMeasurements; // Total number of measurements ever received for sorting private long m_processedMeasurements; // Total number of measurements ever successfully sorted private long m_discardedMeasurements; // Total number of discarded measurements private long m_measurementsSortedByArrival; // Total number of measurements that were sorted by arrival private long m_publishedMeasurements; // Total number of published measurements private long m_downsampledMeasurements; // Total number of downsampled measurements private long m_missedSortsByTimeout; // Total number of unsorted measurements due to timeout waiting for lock private long m_waitHandleExpirations; // Total number of wait handle expirations encounted due to delayed precision timer releases private long m_framesAheadOfSchedule; // Total number of frames published ahead of schedule private long m_publishedFrames; // Total number of published frames private long m_totalPublishTime; // Total cumulative frame user function publication time (in ticks) - used to calculate average private bool m_trackLatestMeasurements; // Determines whether or not to track latest measurements private ImmediateMeasurements m_latestMeasurements; // Absolute latest received measurement values private IMeasurement m_lastDiscardedMeasurement; // Last measurement that was discarded by the concentrator private long m_lastDiscardedMeasurementLatency; // Latency of last measurement that was discarded by the concentrator private bool m_disposed; // Disposed flag detects redundant calls to dispose method #endregion #region [ Constructors ] /// <summary> /// Creates a new <see cref="ConcentratorBase"/>. /// </summary> /// <remarks> /// Concentration will not begin until consumer "Starts" concentrator (i.e., calling <see cref="ConcentratorBase.Start"/> method or setting /// <c><see cref="ConcentratorBase.Enabled"/> = true</c>). /// </remarks> protected ConcentratorBase() { m_usePrecisionTimer = true; m_allowSortsByArrival = true; m_allowPreemptivePublishing = true; m_performTimestampReasonabilityCheck = true; m_processingInterval = -1; m_downsamplingMethod = DownsamplingMethod.LastReceived; m_latestMeasurements = new ImmediateMeasurements(this); m_maximumPublicationTimeout = Timeout.Infinite; // Create a new queue for managing real-time frames m_frameQueue = new FrameQueue(this.CreateNewFrame); // Set minimum timer resolution to one millisecond to improve timer accuracy PrecisionTimer.SetMinimumTimerResolution(1); // Create publication wait handle m_publicationWaitHandle = new AutoResetEvent(false); // Create publication thread - this may be one of the few times when you can // accurately argue that you need high priority thread scheduling... m_publicationThread = new Thread(PublishFrames); m_publicationThread.Priority = ThreadPriority.Highest; m_publicationThread.Start(); // This timer monitors the total number of unpublished samples every 5 seconds. This is a useful statistic // to monitor: if total number of unpublished samples exceed lag time, measurement concentration could // be falling behind. m_monitorTimer = new System.Timers.Timer(); m_monitorTimer.Interval = 5000; m_monitorTimer.AutoReset = true; m_monitorTimer.Elapsed += MonitorUnpublishedSamples; } /// <summary> /// Creates a new <see cref="ConcentratorBase"/> from specified parameters. /// </summary>Lines 1–500 of 2406. Load more
//****************************************************************************************************** // ConcentratorBase.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: // ---------------------------------------------------------------------------------------------------- // 09/02/2010 - J. Ritchie Carroll // Generated original version of source code. // 02/08/2011 - J. Ritchie Carroll // Added QueueState property to expose real-time queue state analysis. // 04/14/2011 - J. Ritchie Carroll // Added ProcessByReceivedTimestamp property to sort and publish measurements by received time. // 05/06/2011 - J. Ritchie Carroll / Jian (Ryan) Zuo // Updated to reduce lock contention and added volatile reads on stats for better accuracy. // //****************************************************************************************************** using GSF.Units; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace GSF.TimeSeries { #region [ Enumerations ] /// <summary> /// Downsampling method enumeration. /// </summary> public enum DownsamplingMethod { /// <summary> /// Downsamples to the last measurement received. /// </summary> /// <remarks> /// Use this option if no downsampling is needed or the selected value is not critical. This is the fastest option if the incoming and outgoing frame rates match. /// </remarks> LastReceived, /// <summary> /// Downsamples to the measurement closest to frame time. /// </summary> /// <remarks> /// This is the typical operation used when performing simple downsampling. This is the fastest option if the incoming frame rate is faster than the outgoing frame rate. /// </remarks> Closest, /// <summary> /// Downsamples by applying a user-defined value filter over all received measurements to anti-alias the results. /// </summary> /// <remarks> /// This option will produce the best result but has a processing penalty. /// </remarks> Filtered, /// <summary> /// Downsamples to the measurement that has the best quality closest to frame time. /// </summary> /// <remarks> /// This option chooses the measurement closest to the frame time with the best quality. /// </remarks> BestQuality } #endregion /// <summary> /// Measurement concentrator base class. /// </summary> /// <remarks> /// <para> /// This class synchronizes (i.e., sorts by timestamp) real-time measurements. /// </para> /// <para> /// Note that your lag time should be defined as it relates to the rate at which data data is coming /// into the concentrator. Make sure you allow enough time for transmission of data over the network /// allowing any needed time for possible network congestion. Lead time should be defined as your /// confidence in the accuracy of your local clock (e.g., if you set lead time to 2, this means you /// trust that your local clock is within plus or minus 2 seconds of real-time.) /// </para> /// <para> /// This concentrator is designed to sort measurements being transmitted in real-time for data being /// sent at rates of at least 1 sample per second. Slower rates (e.g., once every few seconds) are not /// supported since sorting data at these speeds would be trivial. There is no defined maximum number /// of supported samples per second - but keep in mind that CPU utilization will increase as the /// measurement volume and frame rate increase. /// </para> /// </remarks> public abstract class ConcentratorBase : IDisposable { #region [ Members ] // Nested Types /// <summary> /// Frame rate timer. /// </summary> /// <remarks> /// One static instance of this internal class is created per encountered frame rate / processing interval. /// </remarks> private sealed class FrameRateTimer : IDisposable { #region [ Members ] // Fields private PrecisionTimer m_timer; private int m_framesPerSecond; private int m_processingInterval; private int[] m_framePeriods; private int m_frameIndex; private int m_lastFramePeriod; private int m_referenceCount; private bool m_disposed; #endregion #region [ Constructors ] /// <summary> /// Create a new <see cref="FrameRateTimer"/> class. /// </summary> /// <param name="framesPerSecond">Desired frame rate for <see cref="PrecisionTimer"/>.</param> /// <param name="processingInterval">Desired processing interval, if applicable.</param> /// <remarks> /// When the <paramref name="processingInterval"/> is set to -1, the frame rate timer interval will be calculated as a distribution /// of whole milliseonds over the specified number of <paramref name="framesPerSecond"/>. Otherwise the specified /// <paramref name="processingInterval"/> will be used as the timer interval. /// </remarks> public FrameRateTimer(int framesPerSecond, int processingInterval) { if (processingInterval == 0) throw new InvalidOperationException("A frame rate timer should not be created when using a processing interval of zero, i.e., processing data as fast as possible."); m_framesPerSecond = framesPerSecond; m_processingInterval = processingInterval; // Create a new precision timer for this timer state m_timer = new PrecisionTimer(); m_timer.AutoReset = true; if (processingInterval > 0) { // Establish fixed timer period m_timer.Period = processingInterval; } else { // Attach handler for timer period assignments m_timer.Tick += SetTimerPeriod; // Calculate distributed wait time periods over specified number of frames per second m_framePeriods = new int[framesPerSecond]; for (int frameIndex = 0; frameIndex < framesPerSecond; frameIndex++) { m_framePeriods[frameIndex] = CalcWaitTimeForFrameIndex(frameIndex); } // Establish initial timer period m_lastFramePeriod = m_framePeriods[0]; m_timer.Period = m_lastFramePeriod; } // Start timer m_timer.Start(); } /// <summary> /// Releases the unmanaged resources before the <see cref="FrameRateTimer"/> object is reclaimed by <see cref="GC"/>. /// </summary> ~FrameRateTimer() { Dispose(false); } #endregion #region [ Properties ] /// <summary> /// Gets frames per second for this <see cref="FrameRateTimer"/>. /// </summary> public int FramesPerSecond { get { return m_framesPerSecond; } } /// <summary> /// Gets the processing interval defined for the <see cref="FrameRateTimer"/>. /// </summary> public int ProcessingInterval { get { return m_processingInterval; } } /// <summary> /// Gets reference count for this <see cref="FrameRateTimer"/>. /// </summary> public int ReferenceCount { get { return m_referenceCount; } } #endregion #region [ Methods ] /// <summary> /// Releases all the resources used by the <see cref="FrameRateTimer"/> object. /// </summary> public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// <summary> /// Releases the unmanaged resources used by the <see cref="FrameRateTimer"/> 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) { if (m_processingInterval == -1) m_timer.Tick -= SetTimerPeriod; m_timer.Dispose(); } m_timer = null; } } finally { m_disposed = true; // Prevent duplicate dispose. } } } /// <summary> /// Adds a reference to this <see cref="FrameRateTimer"/>. /// </summary> /// <param name="tickFunction">Tick function to add to event list.</param> public void AddReference(EventHandler tickFunction) { m_timer.Tick += tickFunction; m_referenceCount++; } /// <summary> /// Removes a reference to this <see cref="FrameRateTimer"/>. /// </summary> /// <param name="tickFunction">Tick function to remove from event list.</param> public void RemoveReference(EventHandler tickFunction) { m_timer.Tick -= tickFunction; m_referenceCount--; } // Handler to assign next timer period private void SetTimerPeriod(object sender, EventArgs e) { int period; // First things first, prepare timer period for next call... m_frameIndex++; if (m_frameIndex >= m_framesPerSecond) m_frameIndex = 0; // Get the frame period for this frame index period = m_framePeriods[m_frameIndex]; // We only update timer period if it has changed since last call. Note that this is necessary since // timer periods are defined as integers but actual period is typically uneven (e.g., 33.333 ms) if (m_lastFramePeriod != period) m_timer.Period = period; m_lastFramePeriod = period; } // Wait times are not necessarily perfectly even (e.g., at 30 samples per second wait time per frame is 33.333... milliseconds) // so we use this function to evenly distribute integer based millisecond wait times across a second. private int CalcWaitTimeForFrameIndex(int frameIndex) { // Jian Zuo... int millisecondsWaitTime; int frameRate; int deficit; frameRate = (int)(Math.Round(1000.0D / m_framesPerSecond)); deficit = 1000 - frameRate * m_framesPerSecond; if (deficit == 0) { millisecondsWaitTime = frameRate; } else { if (frameIndex == 0) { millisecondsWaitTime = frameRate; } else if (frameIndex == m_framesPerSecond - 1) { millisecondsWaitTime = frameRate + (deficit > 0 ? 1 : -1); } else { double interval = m_framesPerSecond / Math.Abs((double)deficit); double pre_dis = mod_dis(frameIndex - 1, interval); double cur_dis = mod_dis(frameIndex, interval); double next_dis = mod_dis(frameIndex + 1, interval); millisecondsWaitTime = frameRate + ((cur_dis <= pre_dis && cur_dis < next_dis) ? (deficit > 0 ? 1 : -1) : 0); } } return millisecondsWaitTime; } private double mod_dis(int framesIndex, double interval) { double dis2 = (framesIndex + 1) % interval; double dis1 = interval - dis2; return (dis1 < dis2 ? dis1 : dis2); } #endregion } // Events /// <summary> /// This event is raised every 5 seconds allowing consumer to track current number of unpublished seconds of data in the queue. /// </summary> /// <remarks> /// <see cref="EventArgs{T}.Argument"/> is the total number of unpublished seconds of data. /// </remarks> public event EventHandler<EventArgs<int>> UnpublishedSamples; /// <summary> /// This event is raised if there is an exception encountered while attempting to process a frame in the sample queue. /// </summary> /// <remarks> /// <para> /// <see cref="EventArgs{T}.Argument"/> is the <see cref="Exception"/> encountered while parsing the data stream. /// </para> /// <para> /// Processing will not stop for any exceptions thrown by the user function, but any captured exceptions will be exposed through this event. /// </para> /// </remarks> public event EventHandler<EventArgs<Exception>> ProcessException; /// <summary> /// This event is raised if there are any measurements being discarded during the sorting process. /// </summary> /// <remarks> /// <see cref="EventArgs{T}.Argument"/> is the enumeration of <see cref="IMeasurement"/> values that are being discarded during the sorting process. /// </remarks> public event EventHandler<EventArgs<IEnumerable<IMeasurement>>> DiscardingMeasurements; /// <summary> /// This event is raised when <see cref="ConcentratorBase"/> is disposed. /// </summary> public event EventHandler Disposed; /// <summary> /// Raised, for the benefit of dependent classes, when lag time is updated. /// </summary> internal event Action<double> LagTimeUpdated; /// <summary> /// Raised, for the benefit of dependent classes, when lead time is updated. /// </summary> internal event Action<double> LeadTimeUpdated; // Fields private FrameQueue m_frameQueue; // Queue of frames to be published private Thread m_publicationThread; // Thread that handles frame publication private AutoResetEvent m_publicationWaitHandle; // Interframe publication wait handle private AutoResetEvent[] m_externalEventHandles; // External event wait handles private int m_externalEventTimeout; // External event timeout private bool m_usePrecisionTimer; // Flag that enables use of precision timer (over just simple thread sleep) private bool m_attachedToFrameRateTimer; // Flag that tracks if instance is attached to a frame rate timer private System.Timers.Timer m_monitorTimer; // Sample monitor - tracks total number of unpublished frames private int m_framesPerSecond; // Frames per second private double m_ticksPerFrame; // Ticks per frame private double m_lagTime; // Allowed past time deviation tolerance, in seconds private double m_leadTime; // Allowed future time deviation tolerance, in seconds private long m_timeResolution; // Maximum sorting resolution in ticks private int m_processingInterval; // Defines a specific processing interval for data, if desired private DownsamplingMethod m_downsamplingMethod; // Downsampling method to use if input is at a higher-resolution than output private double m_timeOffset; // Half the distance of the time resolution used for index calculation private int m_maximumPublicationTimeout; // Maximum publication wait timeout private Ticks m_lagTicks; // Current lag time calculated in ticks private volatile bool m_enabled; // Enabled state of concentrator private long m_startTime; // Start time of concentrator private long m_stopTime; // Stop time of concentrator private long m_realTimeTicks; // Timestamp of real-time or the most recently received measurement private bool m_ignoreBadTimestamps; // Determines whether or not to ignore bad timestamps when sorting measurements private bool m_allowSortsByArrival; // Determines whether or not to sort incoming measurements with a bad timestamp by arrival private bool m_useLocalClockAsRealTime; // Determines whether or not to use local system clock as "real-time" private bool m_allowPreemptivePublishing; // Determines whether or not to preemptively publish frame if expected measurements arrive private bool m_performTimestampReasonabilityCheck; // Determines whether or not to execute timestamp reasonability checks (i.e., lead time validation) private bool m_processByReceivedTimestamp; // Determines whether or not to sort and publish measurements by their ReceivedTimestamp private bool m_trackPublishedTimestamp; // Determines whether or not to track timestamp of publication for all processed frames and measurements private int m_expectedMeasurements; // Expected number of measurements to be sorted into a frame private long m_receivedMeasurements; // Total number of measurements ever received for sorting private long m_processedMeasurements; // Total number of measurements ever successfully sorted private long m_discardedMeasurements; // Total number of discarded measurements private long m_measurementsSortedByArrival; // Total number of measurements that were sorted by arrival private long m_publishedMeasurements; // Total number of published measurements private long m_downsampledMeasurements; // Total number of downsampled measurements private long m_missedSortsByTimeout; // Total number of unsorted measurements due to timeout waiting for lock private long m_waitHandleExpirations; // Total number of wait handle expirations encounted due to delayed precision timer releases private long m_framesAheadOfSchedule; // Total number of frames published ahead of schedule private long m_publishedFrames; // Total number of published frames private long m_totalPublishTime; // Total cumulative frame user function publication time (in ticks) - used to calculate average private bool m_trackLatestMeasurements; // Determines whether or not to track latest measurements private ImmediateMeasurements m_latestMeasurements; // Absolute latest received measurement values private IMeasurement m_lastDiscardedMeasurement; // Last measurement that was discarded by the concentrator private long m_lastDiscardedMeasurementLatency; // Latency of last measurement that was discarded by the concentrator private bool m_disposed; // Disposed flag detects redundant calls to dispose method #endregion #region [ Constructors ] /// <summary> /// Creates a new <see cref="ConcentratorBase"/>. /// </summary> /// <remarks> /// Concentration will not begin until consumer "Starts" concentrator (i.e., calling <see cref="ConcentratorBase.Start"/> method or setting /// <c><see cref="ConcentratorBase.Enabled"/> = true</c>). /// </remarks> protected ConcentratorBase() { m_usePrecisionTimer = true; m_allowSortsByArrival = true; m_allowPreemptivePublishing = true; m_performTimestampReasonabilityCheck = true; m_processingInterval = -1; m_downsamplingMethod = DownsamplingMethod.LastReceived; m_latestMeasurements = new ImmediateMeasurements(this); m_maximumPublicationTimeout = Timeout.Infinite; // Create a new queue for managing real-time frames m_frameQueue = new FrameQueue(this.CreateNewFrame); // Set minimum timer resolution to one millisecond to improve timer accuracy PrecisionTimer.SetMinimumTimerResolution(1); // Create publication wait handle m_publicationWaitHandle = new AutoResetEvent(false); // Create publication thread - this may be one of the few times when you can // accurately argue that you need high priority thread scheduling... m_publicationThread = new Thread(PublishFrames); m_publicationThread.Priority = ThreadPriority.Highest; m_publicationThread.Start(); // This timer monitors the total number of unpublished samples every 5 seconds. This is a useful statistic // to monitor: if total number of unpublished samples exceed lag time, measurement concentration could // be falling behind. m_monitorTimer = new System.Timers.Timer(); m_monitorTimer.Interval = 5000; m_monitorTimer.AutoReset = true; m_monitorTimer.Elapsed += MonitorUnpublishedSamples; } /// <summary> /// Creates a new <see cref="ConcentratorBase"/> from specified parameters. /// </summary>