//****************************************************************************************************** // 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 ] /// /// Downsampling method enumeration. /// public enum DownsamplingMethod { /// /// Downsamples to the last measurement received. /// /// /// 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. /// LastReceived, /// /// Downsamples to the measurement closest to frame time. /// /// /// 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. /// Closest, /// /// Downsamples by applying a user-defined value filter over all received measurements to anti-alias the results. /// /// /// This option will produce the best result but has a processing penalty. /// Filtered, /// /// Downsamples to the measurement that has the best quality closest to frame time. /// /// /// This option chooses the measurement closest to the frame time with the best quality. /// BestQuality } #endregion /// /// Measurement concentrator base class. /// /// /// /// This class synchronizes (i.e., sorts by timestamp) real-time measurements. /// /// /// 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.) /// /// /// 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. /// /// public abstract class ConcentratorBase : IDisposable { #region [ Members ] // Nested Types /// /// Frame rate timer. /// /// /// One static instance of this internal class is created per encountered frame rate / processing interval. /// 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 ] /// /// Create a new class. /// /// Desired frame rate for . /// Desired processing interval, if applicable. /// /// When the is set to -1, the frame rate timer interval will be calculated as a distribution /// of whole milliseonds over the specified number of . Otherwise the specified /// will be used as the timer interval. /// 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(); } /// /// Releases the unmanaged resources before the object is reclaimed by . /// ~FrameRateTimer() { Dispose(false); } #endregion #region [ Properties ] /// /// Gets frames per second for this . /// public int FramesPerSecond { get { return m_framesPerSecond; } } /// /// Gets the processing interval defined for the . /// public int ProcessingInterval { get { return m_processingInterval; } } /// /// Gets reference count for this . /// public int ReferenceCount { get { return m_referenceCount; } } #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) { if (m_processingInterval == -1) m_timer.Tick -= SetTimerPeriod; m_timer.Dispose(); } m_timer = null; } } finally { m_disposed = true; // Prevent duplicate dispose. } } } /// /// Adds a reference to this . /// /// Tick function to add to event list. public void AddReference(EventHandler tickFunction) { m_timer.Tick += tickFunction; m_referenceCount++; } /// /// Removes a reference to this . /// /// Tick function to remove from event list. 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 /// /// This event is raised every 5 seconds allowing consumer to track current number of unpublished seconds of data in the queue. /// /// /// is the total number of unpublished seconds of data. /// public event EventHandler> UnpublishedSamples; /// /// This event is raised if there is an exception encountered while attempting to process a frame in the sample queue. /// /// /// /// is the encountered while parsing the data stream. /// /// /// Processing will not stop for any exceptions thrown by the user function, but any captured exceptions will be exposed through this event. /// /// public event EventHandler> ProcessException; /// /// This event is raised if there are any measurements being discarded during the sorting process. /// /// /// is the enumeration of values that are being discarded during the sorting process. /// public event EventHandler>> DiscardingMeasurements; /// /// This event is raised when is disposed. /// public event EventHandler Disposed; /// /// Raised, for the benefit of dependent classes, when lag time is updated. /// internal event Action LagTimeUpdated; /// /// Raised, for the benefit of dependent classes, when lead time is updated. /// internal event Action 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 ] /// /// Creates a new . /// /// /// Concentration will not begin until consumer "Starts" concentrator (i.e., calling method or setting /// = true). /// 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; } /// /// Creates a new from specified parameters. /// /// Number of frames to publish per second. /// Past time deviation tolerance, in seconds - this becomes the amount of time to wait before publishing begins. /// Future time deviation tolerance, in seconds - this becomes the tolerated +/- accuracy of the local clock to real-time. /// /// /// must be greater then 0. /// /// /// must be greater than zero, but can be specified in sub-second intervals (e.g., set to .25 for a quarter-second lag time). /// Note that this defines time sensitivity to past timestamps. /// /// /// must be greater than zero, but can be specified in sub-second intervals (e.g., set to .5 for a half-second lead time). /// Note that this defines time sensitivity to future timestamps. /// /// /// Concentration will not begin until consumer "Starts" concentrator (i.e., calling method or setting /// = true). /// /// /// Specified argument is outside of allowed value range (see remarks). protected ConcentratorBase(int framesPerSecond, double lagTime, double leadTime) : this() { this.FramesPerSecond = framesPerSecond; this.LagTime = lagTime; this.LeadTime = leadTime; } /// /// Releases the unmanaged resources before the object is reclaimed by . /// ~ConcentratorBase() { // We implement finalizer for this class to ensure sample queue shuts down in an orderly fashion. Dispose(false); } #endregion #region [ Properties ] /// /// Gets or sets the allowed past time deviation tolerance, in seconds (can be subsecond). /// /// /// Defines the time sensitivity to past measurement timestamps. /// The number of seconds allowed before assuming a measurement timestamp is too old. /// This becomes the amount of delay introduced by the concentrator to allow time for data to flow into the system. /// /// LagTime must be greater than zero, but it can be less than one. public double LagTime { get { return m_lagTime; } set { if (value <= 0) throw new ArgumentOutOfRangeException("value", "LagTime must be greater than zero, but it can be less than one"); m_lagTime = value; m_lagTicks = (long)(m_lagTime * Ticks.PerSecond); if (LagTimeUpdated != null) LagTimeUpdated(m_lagTime); } } /// /// Gets defined past time deviation tolerance, in ticks. /// public Ticks LagTicks { get { return m_lagTicks; } } /// /// Gets or sets the allowed future time deviation tolerance, in seconds (can be subsecond). /// /// /// Defines the time sensitivity to future measurement timestamps. /// The number of seconds allowed before assuming a measurement timestamp is too advanced. /// This becomes the tolerated +/- accuracy of the local clock to real-time. /// /// LeadTime must be greater than zero, but it can be less than one. public double LeadTime { get { return m_leadTime; } set { if (value <= 0) throw new ArgumentOutOfRangeException("value", "LeadTime must be greater than zero, but it can be less than one"); m_leadTime = value; if (LeadTimeUpdated != null) LeadTimeUpdated(m_leadTime); } } /// /// Gets or sets flag to start tracking the absolute latest received measurement values. /// /// /// Lastest received measurement value will be available via the property. /// Note that enabling this option will slightly increase the required sorting time. /// public bool TrackLatestMeasurements { get { return m_trackLatestMeasurements; } set { m_trackLatestMeasurements = value; } } /// /// Gets reference to the collection of absolute latest received measurement values. /// public ImmediateMeasurements LatestMeasurements { get { return m_latestMeasurements; } } /// /// Gets reference to the last published . /// public IFrame LastFrame { get { if (m_frameQueue != null) { TrackingFrame last = m_frameQueue.Last; if (last != null) return last.SourceFrame; } return null; } } /// /// Gets or sets flag that determines if precision timer should be used for frame publication. /// public bool UsePrecisionTimer { get { return m_usePrecisionTimer; } set { if (!value && m_processingInterval > 0) throw new InvalidOperationException("A precision timer must be used when a specific processing interval has been defined."); if (value && m_processingInterval == 0) throw new InvalidOperationException("A precision timer cannot be used when the processing interval is set to zero, i.e., process data as fast as possible."); if (m_usePrecisionTimer != value) { m_usePrecisionTimer = value; if (m_usePrecisionTimer) { // Subscribe to frame rate timer, creating it if it doesn't exist AttachToFrameRateTimer(m_framesPerSecond, m_processingInterval); } else { // Unsubscribe from last frame rate timer, if any DetachFromFrameRateTimer(m_framesPerSecond, m_processingInterval); // Make sure to release publication wait wandle if it's currently waiting... if (m_publicationWaitHandle != null) m_publicationWaitHandle.Set(); } } } } /// /// Gets or sets the number of frames per second. /// /// /// Valid frame rates for a are greater than 0 frames per second. /// public int FramesPerSecond { get { return m_framesPerSecond; } set { if (value < 1) throw new ArgumentOutOfRangeException("value", "Frames per second must be greater than 0"); if (m_framesPerSecond != value) { // Unsubscribe from last frame rate timer, if any DetachFromFrameRateTimer(m_framesPerSecond, m_processingInterval); m_framesPerSecond = value; m_ticksPerFrame = Ticks.PerSecond / (double)m_framesPerSecond; // We calculate the default maximum wait time for frame publication in whole milliseconds per frame plus 20%, // this comes out to be 40 milliseconds at 30 frames per second and 20 milliseconds at 60 frames per second m_maximumPublicationTimeout = (int)Math.Round((m_ticksPerFrame + m_ticksPerFrame * 0.2D) / Ticks.PerMillisecond); if (m_frameQueue != null) m_frameQueue.FramesPerSecond = m_framesPerSecond; if (m_usePrecisionTimer) { // Subscribe to frame rate timer, creating it if it doesn't exist AttachToFrameRateTimer(m_framesPerSecond, m_processingInterval); } else { // Make sure to release publication wait wandle if it's currently waiting... if (m_publicationWaitHandle != null) m_publicationWaitHandle.Set(); } } } } /// /// Gets or sets the desired processing interval, in milliseconds, for the adapter. /// /// /// /// This property is normally only used when you need the concentrator to send data at faster than real-time speeds, /// e.g., faster than the defined . A use case would be pushing historical data through /// the concentrator where you want to sort and publish data as quickly as possible. /// /// /// With the exception of the values of -1 and 0, this value specifies the desired processing interval for data, e.g., /// a timer interval, overwhich to process data. A value of -1 means to use the default processing interval, i.e., use /// the , while a value of 0 means to process data as fast as possible. /// /// /// From a real-time perspective the defines its general processing interval based on /// the defined property. The frames per second property, however, is more than a basic /// processing interval since it is used to define the intervals in one second that will become the time sorting /// destination "buckets" used by the concentrator irrespective of the data rate of the incoming data. As an example, /// if the frames per second of the concentrator is set to 30 and the source data rate is 60fps, then data will be /// downsampled to 30 frames of sorted incoming data but the assigned processing interval will be used to publish the /// frames at the specified rate. /// /// /// The implemented functionality of the process interval property will be to respond to values in the following way: /// /// /// Value /// Response /// /// /// < 0 /// /// In this case the default processing interval has been requested, as a result the /// will be set to false and the concentrator processing interval will be defined based on the currently defined /// property, e.g., if the frames per second is 30 the processing interval will be 33.33ms. /// /// /// /// 0 /// /// In this case the processing interval has been defined to process data as fast as possible, as a result the /// property will be set to true and property /// will be set to false. With a processing interval of zero data is expected to flow into the concentrator as quick as /// it can be provided. The property will still be used to sort data by time into appropriate /// frames, but the concentrator will use the reception time of the measurements against the defined lag-time to make sure /// needed data has arrived before publication and frames will be published at the same rate of data arrival. /// /// /// /// > 0 /// /// In this case a specific processing interval has been defined for processing data, as a result both the /// and properties will be set to true. With /// a specifically defined processing interval, data is expected to flow into the concentrator at a similar rate. The /// property will still be used to sort data by time into appropriate frames, but the concentrator /// will use the reception time of the measurements against the defined lag-time to make sure needed data has arrived before /// publication and frames will be published on the specified interval. If multiple frames are ready for publication when the /// processing interval executes, then all the ready frames will be published sequentially as quickly as possible. /// /// /// /// /// public virtual int ProcessingInterval { get { return m_processingInterval; } set { if (m_processingInterval != value) { DetachFromFrameRateTimer(m_framesPerSecond, m_processingInterval); m_processingInterval = value; if (m_processingInterval < -1) m_processingInterval = -1; ProcessByReceivedTimestamp = (m_processingInterval > -1); if (m_processingInterval == 0) { m_usePrecisionTimer = false; // Make sure to release publication wait wandle if it's currently waiting... if (m_publicationWaitHandle != null) m_publicationWaitHandle.Set(); } else if (m_processingInterval > 0) { m_usePrecisionTimer = true; // Subscribe to frame rate timer, creating it if it doesn't exist AttachToFrameRateTimer(m_framesPerSecond, m_processingInterval); } else { if (m_usePrecisionTimer) { // Subscribe to frame rate timer, creating it if it doesn't exist AttachToFrameRateTimer(m_framesPerSecond, m_processingInterval); } else { // Unsubscribe from last frame rate timer, if any DetachFromFrameRateTimer(m_framesPerSecond, m_processingInterval); // Make sure to release publication wait wandle if it's currently waiting... if (m_publicationWaitHandle != null) m_publicationWaitHandle.Set(); } } } } } /// /// Gets or sets the maximum frame publication timeout in milliseconds, set to (-1) to wait indefinitely. /// /// /// /// The concentrator automatically defines a precision timer to provide the heatbeat for frame publication, however if the system /// gets busy the heartbeat signals can be missed. This property defines a maximum wait timeout before reception of the heartbeat /// signal to make sure frame publications continue to occur in a timely fashion even when a system is under stress. /// /// /// This property is automatically defined as 2% more than the number of milliseconds per frame when the /// property is set. Users can override this default value to provide custom behavior for this timeout. /// /// public int MaximumPublicationTimeout { get { return m_maximumPublicationTimeout; } set { m_maximumPublicationTimeout = value; } } /// /// Gets or sets collection of wait handles used to synchronize concentration publication with external events. /// protected AutoResetEvent[] ExternalEventHandles { get { return m_externalEventHandles; } set { if (value != null && value.Length > 0) { m_externalEventHandles = value; // Assign default external event timeout if none is defined if (m_externalEventTimeout == 0) m_externalEventTimeout = (int)(m_ticksPerFrame / Ticks.PerMillisecond); } else { m_externalEventHandles = null; } } } /// /// Gets or sets maximum to time to wait, in milliseconds, for external events before publishing proceeds. /// protected int ExternalEventTimeout { get { return m_externalEventTimeout; } set { m_externalEventTimeout = value; } } /// /// Gets or sets the maximum time resolution, in ticks, to use when sorting measurements by timestamps into their proper destination frame. /// /// /// /// /// Desired maximum resolution /// Value to assign /// /// /// Seconds /// . /// /// /// Milliseconds /// . /// /// /// Microseconds /// . /// /// /// 100-Nanoseconds /// 0 /// /// /// Assigning values less than zero will be set to zero since minimum possible concentrator resolution is one tick (100-nanoseconds). Assigning /// values values greater than . will be set to . /// since maximum possible concentrator resolution is one second (i.e., 1 frame per second). /// public long TimeResolution { get { return m_timeResolution; } set { if (value < 0) m_timeResolution = 0; if (value > Ticks.PerSecond) m_timeResolution = Ticks.PerSecond; else m_timeResolution = value; // Calculate half the distance of the time resolution for use as an offset m_timeOffset = (m_timeResolution > 1 ? m_timeResolution / 2 : 1); // Assign desired time resolution to frame queue if (m_frameQueue != null) m_frameQueue.TimeResolution = m_timeResolution; } } /// /// Gets or sets the to be used by the concentrator. /// /// /// The downsampling method determines the algorithm to use if input is being received at a higher-resolution than the defined output. /// public DownsamplingMethod DownsamplingMethod { get { return m_downsamplingMethod; } set { m_downsamplingMethod = value; // Assign desired downsampling method to frame queue if (m_frameQueue != null) m_frameQueue.DownsamplingMethod = m_downsamplingMethod; } } /// /// Gets the number of ticks per frame. /// public double TicksPerFrame { get { return m_ticksPerFrame; } } /// /// Gets or sets the expected number of measurements to be assigned to a single frame. /// public int ExpectedMeasurements { get { return m_expectedMeasurements; } set { m_expectedMeasurements = value; } } /// /// Gets or sets flag that allows system to preemptively publish frames assuming all have arrived. /// /// /// In order for this property to used, the must be defined. /// public bool AllowPreemptivePublishing { get { return m_allowPreemptivePublishing; } set { m_allowPreemptivePublishing = value; } } /// /// Gets or sets flag that determines if timestamp reasonability checks should be performed on incoming /// measurements (i.e., measurement timestamps are compared to system clock for reasonability using /// tolerance). /// /// /// Setting this value to false will make the concentrator use the latest value received as "real-time" /// without validation; this is not recommended in production since time reported by source devices may /// be grossly incorrect. For non-production configurations, setting this value to false will allow /// concentration of historical data. /// public bool PerformTimestampReasonabilityCheck { get { return m_performTimestampReasonabilityCheck; } set { m_performTimestampReasonabilityCheck = value; } } /// /// Gets or sets flag that determines if concentrator should sort measurements by received time. /// /// /// /// Setting this value to true will make concentrator use the timestamp of measurement /// reception, which is typically the creation time, for sorting and /// publication. This is useful in scenarios where the concentrator will be receiving very large /// volumes of data but not necessarily in real-time, such as, reading values from a file where /// you want data to be sorted and processed as fast as possible. /// /// /// Setting this value to true will force to be true /// and to be false. /// /// public bool ProcessByReceivedTimestamp { get { return m_processByReceivedTimestamp; } set { if (!value && m_processingInterval > -1) throw new InvalidOperationException("Processing by received timestamp cannot be disabled when a processing interval is defined."); m_processByReceivedTimestamp = value; if (m_processByReceivedTimestamp) { m_useLocalClockAsRealTime = true; m_allowSortsByArrival = false; } } } /// /// Gets or sets flag that determines if system should track timestamp of publication for all frames and measurements. /// /// /// Setting this value to true will cause the concentrator to mark the timestamp of publication in each /// and its measurement's . /// Since this is extra processing time that may not be needed except in cases of calculating statistics for /// system performance, this must be explicitly enabled. /// public bool TrackPublishedTimestamp { get { return m_trackPublishedTimestamp; } set { m_trackPublishedTimestamp = value; } } /// /// Gets or sets the current enabled state of concentrator. /// /// Current enabled state of concentrator /// /// Concentrator must be started by calling method or setting /// = true) before concentration will begin. /// public virtual bool Enabled { get { return m_enabled; } set { if (value) Start(); else Stop(); } } /// /// Gets the UTC time the concentrator was started. /// public Ticks StartTime { get { return m_startTime; } } /// /// Gets the UTC time the concentrator was stopped. /// public Ticks StopTime { get { return m_stopTime; } } /// /// Gets the total amount of time, in seconds, that the concentrator has been active. /// public virtual Time RunTime { get { Ticks processingTime = 0; if (m_startTime > 0) { if (m_stopTime > 0) { processingTime = m_stopTime - m_startTime; } else { #if UseHighResolutionTime processingTime = PrecisionTimer.UtcNow.Ticks - m_startTime; #else processingTime = DateTime.UtcNow.Ticks - m_startTime; #endif } } if (processingTime < 0) processingTime = 0; return processingTime.ToSeconds(); } } /// /// Gets or sets flag that determines if bad timestamps (as determined by measurement's timestamp quality) /// should be ignored when sorting measurements. /// /// /// Setting this property to true forces system to use timestamps as-is without checking quality. /// If this property is true, it will supercede operation of . /// public bool IgnoreBadTimestamps { get { return m_ignoreBadTimestamps; } set { m_ignoreBadTimestamps = value; } } /// /// Gets or sets flag that determines whether or not to allow incoming measurements with bad timestamps /// to be sorted by arrival time. /// /// /// /// Value defaults to true, so any incoming measurement with a bad timestamp quality will be sorted /// according to its arrival time. Setting the property to false will cause all measurements with a /// bad timestamp quality to be discarded. This property will only be considered when /// is false. /// /// /// Value will be forced to false if is true. /// /// public bool AllowSortsByArrival { get { return m_allowSortsByArrival; } set { if (m_processByReceivedTimestamp) m_allowSortsByArrival = false; else m_allowSortsByArrival = value; } } /// /// Gets or sets flag that determines whether or not to use the local clock time as real-time. /// /// /// /// Use your local system clock as real-time only if the time is locally GPS-synchronized, /// or if the measurement values being sorted were not measured relative to a GPS-synchronized clock. /// /// /// If is true, will /// always be set to true, even if you try to set it to false. /// /// public bool UseLocalClockAsRealTime { get { return m_useLocalClockAsRealTime; } set { if (m_processByReceivedTimestamp) m_useLocalClockAsRealTime = true; else m_useLocalClockAsRealTime = value; } } /// /// Gets the the most accurate time value that is available. If = true, then /// this function will return . Otherwise, this function will return the timestamp of the /// most recent measurement, or if no measurement timestamps are within time deviation /// tolerances as specified by the value. /// /// /// Because the measurements being received by remote devices are often measured relative to GPS time, these timestamps /// are typically more accurate than the local clock. As a result, we can use the latest received timestamp as the best /// local time measurement we have (ignoring transmission delays); but, even these times can be incorrect so we still have /// to apply reasonability checks to these times. To do this, we use the local system time and the /// value to validate the latest measured timestamp. If the newest received measurement timestamp gets too old or creeps /// too far into the future (both validated + and - against defined lead time property value), we will fall back on local /// system time. Note that this creates a dependency on a fairly accurate local clock - the smaller the lead time deviation /// tolerance, the better the needed local clock accuracy. For example, a lead time deviation tolerance of a few seconds /// might only require keeping the local clock synchronized to an NTP time source; but, a sub-second tolerance would /// require that the local clock be very close to GPS time. /// public Ticks RealTime { get { if (m_useLocalClockAsRealTime) { // Assumes local system clock is the best value we have for real-time. #if UseHighResolutionTime return PrecisionTimer.UtcNow.Ticks; #else return DateTime.UtcNow.Ticks; #endif } else { if (m_performTimestampReasonabilityCheck) { // If the current value for real-time is outside of the time deviation tolerance of the local // clock, then we set latest measurement time (i.e., real-time) to be the current local clock // time. Since the lead time typically defines the tolerated accuracy of the local clock to // real-time we will use this value as the + and - timestamp tolerance to validate if the // measurement time is reasonable. #if UseHighResolutionTime long currentTimeTicks = PrecisionTimer.UtcNow.Ticks; #else long currentTimeTicks = DateTime.UtcNow.Ticks; #endif double distance = (currentTimeTicks - Thread.VolatileRead(ref m_realTimeTicks)) / (double)Ticks.PerSecond; // Set real-time ticks to current ticks if value is outside of tolerances if (distance > m_leadTime || distance < -m_leadTime) Thread.VolatileWrite(ref m_realTimeTicks, currentTimeTicks); } // Assume lastest measurement timestamp is the best value we have for real-time. return Thread.VolatileRead(ref m_realTimeTicks); } } } /// /// Gets the total number of measurements ever requested for sorting. /// public long ReceivedMeasurements { get { return Thread.VolatileRead(ref m_receivedMeasurements); } } /// /// Gets the total number of measurements successfully sorted. /// public long ProcessedMeasurements { get { return Thread.VolatileRead(ref m_processedMeasurements); } } /// /// Gets the total number of measurements that have been discarded because of old timestamps /// (i.e., measurements that were outside the time deviation tolerance from base time, past or future). /// public long DiscardedMeasurements { get { return Thread.VolatileRead(ref m_discardedMeasurements); } } /// /// Gets a reference the last that was discarded by the concentrator. /// public IMeasurement LastDiscardedMeasurement { get { return m_lastDiscardedMeasurement; } } /// /// Gets the calculated latency of the last that was discarded by the concentrator. /// public Ticks LastDiscardedMeasurementLatency { get { return Thread.VolatileRead(ref m_lastDiscardedMeasurementLatency); } } /// /// Gets the total number of published measurements. /// public long PublishedMeasurements { get { return Thread.VolatileRead(ref m_publishedMeasurements); } } /// /// Gets the total number of published frames. /// public long PublishedFrames { get { return Thread.VolatileRead(ref m_publishedFrames); } } /// /// Gets the total number of measurements that were sorted by arrival because the measurement reported a bad timestamp quality. /// public long MeasurementsSortedByArrival { get { return Thread.VolatileRead(ref m_measurementsSortedByArrival); } } /// /// Gets the total number of downsampled measurements processed by the concentrator. /// public long DownsampledMeasurements { get { return Thread.VolatileRead(ref m_downsampledMeasurements); } } /// /// Gets the total number of missed sorts by timeout processed by the concentrator. /// public long MissedSortsByTimeout { get { return Thread.VolatileRead(ref m_missedSortsByTimeout); } } /// /// Gets the total number of wait handle expirations encounted due to delayed precision timer releases. /// public long WaitHandleExpirations { get { return Thread.VolatileRead(ref m_waitHandleExpirations); } } /// /// Gets the total number of frames ahead of schedule processed by the concentrator. /// public long FramesAheadOfSchedule { get { return Thread.VolatileRead(ref m_framesAheadOfSchedule); } } /// /// Gets the total number of seconds frames have spent in the publication process since concentrator started. /// public Time TotalPublicationTime { get { return ((Ticks)Thread.VolatileRead(ref m_totalPublishTime)).ToSeconds(); } } /// /// Gets the average required frame publication time, in seconds. /// /// /// If user publication function, , consistently exceeds available publishing time /// (i.e., 1 / ), concentration will fall behind. /// public Time AveragePublicationTimePerFrame { get { return TotalPublicationTime / PublishedFrames; } } /// /// Gets detailed state of concentrator frame queue. /// public string QueueState { get { if (m_frameQueue != null) return m_frameQueue.ExamineQueueState(m_expectedMeasurements); return ""; } } /// /// Gets current detailed state and status of concentrator for display purposes. /// public virtual string Status { get { StringBuilder status = new StringBuilder(); IFrame lastFrame = LastFrame; IMeasurement lastDiscardedMeasurement = null; #if UseHighResolutionTime DateTime currentTime = PrecisionTimer.UtcNow; #else DateTime currentTime = DateTime.UtcNow; #endif status.AppendFormat(" Data concentration is: {0}", Enabled ? "Enabled" : "Disabled"); status.AppendLine(); status.AppendFormat(" Total process run time: {0}", RunTime.ToString()); status.AppendLine(); status.AppendFormat(" Measurement wait delay: {0} seconds (lag time)", m_lagTime); status.AppendLine(); status.AppendFormat(" Local clock tolerance: {0} seconds (lead time)", m_leadTime); status.AppendLine(); status.AppendFormat(" Maximum time resolution: {0} ticks", m_timeResolution); status.AppendLine(); status.AppendFormat(" Downsampling method: {0}", m_downsamplingMethod); status.AppendLine(); status.AppendFormat(" External event handles: {0}", m_externalEventHandles == null ? "None defined" : m_externalEventHandles.Length + " defined"); status.AppendLine(); if (m_externalEventHandles != null) { status.AppendFormat(" External event timeout: {0} milliseconds", m_externalEventTimeout); status.AppendLine(); } status.AppendFormat(" Local clock time (UTC): {0}", currentTime.ToString("dd-MMM-yyyy HH:mm:ss.fff")); status.AppendLine(); status.AppendFormat(" Using clock as real-time: {0}", m_useLocalClockAsRealTime); status.AppendLine(); if (!m_useLocalClockAsRealTime) { status.Append(" Local clock accuracy: "); #if UseHighResolutionTime status.Append(SecondsFromRealTime(PrecisionTimer.UtcNow.Ticks).ToString("0.0000")); #else status.Append(SecondsFromRealTime(DateTime.UtcNow.Ticks).ToString("0.0000")); #endif status.Append(" second deviation from latest time"); status.AppendLine(); } status.AppendFormat(" Ignore bad timestamps: {0}", m_ignoreBadTimestamps); status.AppendLine(); status.AppendFormat(" Allow sorts by arrival: {0}", !m_ignoreBadTimestamps && m_allowSortsByArrival); status.AppendLine(); status.AppendFormat(" Use preemptive publishing: {0}", m_allowPreemptivePublishing); status.AppendLine(); status.AppendFormat(" Time reasonability check: {0}", m_performTimestampReasonabilityCheck ? "Enabled" : "Disabled"); status.AppendLine(); status.AppendFormat(" Tracking publication time: {0}", m_trackPublishedTimestamp); status.AppendLine(); status.AppendFormat(" Process by received time: {0}", m_processByReceivedTimestamp); status.AppendLine(); status.AppendFormat(" Received measurements: {0}", ReceivedMeasurements); status.AppendLine(); status.AppendFormat(" Processed measurements: {0}", ProcessedMeasurements); status.AppendLine(); status.AppendFormat(" Discarded measurements: {0}", DiscardedMeasurements); status.AppendLine(); status.AppendFormat(" Downsampled measurements: {0}", DownsampledMeasurements); status.AppendLine(); status.AppendFormat(" Published measurements: {0}", PublishedMeasurements); status.AppendLine(); status.AppendFormat(" Expected measurements: {0} ({1} / frame)", PublishedFrames * m_expectedMeasurements, m_expectedMeasurements); status.AppendLine(); status.Append("Last discarded measurement: "); Interlocked.Exchange(ref lastDiscardedMeasurement, m_lastDiscardedMeasurement); if (lastDiscardedMeasurement == null) { status.Append(""); } else { status.Append(Measurement.ToString(lastDiscardedMeasurement)); status.Append(" - "); status.Append(((DateTime)lastDiscardedMeasurement.Timestamp).ToString("dd-MMM-yyyy HH:mm:ss.fff")); status.AppendLine(); status.AppendFormat(" Latency of last discarded: {0} seconds", LastDiscardedMeasurementLatency.ToSeconds().ToString("0.0000")); } status.AppendLine(); status.AppendFormat(" Average publication time: {0} milliseconds", (AveragePublicationTimePerFrame / SI.Milli).ToString("0.0000")); status.AppendLine(); status.AppendFormat(" Pre-lag-time publication: {0}", (FramesAheadOfSchedule / (double)PublishedFrames).ToString("##0.0000%")); status.AppendLine(); status.AppendFormat(" Downsampling application: {0}", (DownsampledMeasurements / (double)ProcessedMeasurements).ToString("##0.0000%")); status.AppendLine(); status.AppendFormat(" User function utilization: {0} of available time used", (1.0D - (m_ticksPerFrame - (double)AveragePublicationTimePerFrame.ToTicks()) / m_ticksPerFrame).ToString("##0.0000%")); status.AppendLine(); status.AppendFormat("Published measurement loss: {0}", (DiscardedMeasurements / (double)ProcessedMeasurements).ToString("##0.0000%")); status.AppendLine(); status.AppendFormat(" Total sorts by arrival: {0}", MeasurementsSortedByArrival); status.AppendLine(); status.AppendFormat(" Measurement time accuracy: {0}", (1.0D - MeasurementsSortedByArrival / (double)ReceivedMeasurements).ToString("##0.0000%")); status.AppendLine(); status.AppendFormat(" Missed sorts by timeout: {0}", MissedSortsByTimeout); status.AppendLine(); status.AppendFormat(" Loss due to timeouts: {0}", (MissedSortsByTimeout / (double)ProcessedMeasurements).ToString("##0.0000%")); status.AppendLine(); status.AppendFormat(" Using precision timer: {0}", m_usePrecisionTimer); status.AppendLine(); status.AppendFormat(" Wait handle timeout: {0} milliseconds", m_maximumPublicationTimeout); status.AppendLine(); status.AppendFormat(" Wait handle expirations: {0}", WaitHandleExpirations); status.AppendLine(); status.AppendFormat(" Total published frames: {0}", PublishedFrames); status.AppendLine(); status.AppendFormat(" Defined frame rate: {0} frames/sec, {1} ticks/frame", m_framesPerSecond, m_ticksPerFrame.ToString("0.00")); status.AppendLine(); status.AppendFormat(" Estimated mean frame rate: {0} frames/sec", (PublishedFrames / (RunTime - m_lagTime)).ToString("0.00")); status.AppendLine(); status.AppendFormat(" Processing interval: {0}", ProcessingInterval < 0 ? ((Ticks)m_ticksPerFrame).ToMilliseconds().ToString("0.00") + " milliseconds" : (ProcessingInterval == 0 ? "As fast as possible" : ProcessingInterval + " milliseconds")); status.AppendLine(); lock (s_frameRateTimers) { Tuple key = new Tuple(m_framesPerSecond, m_processingInterval); FrameRateTimer timer; if (s_frameRateTimers.TryGetValue(key, out timer)) { status.AppendFormat(" Timer reference count: {0} concentrator{1} for the {2}fps @ {3:0.00}ms timer", timer.ReferenceCount, timer.ReferenceCount > 1 ? "s" : "", m_framesPerSecond, ProcessingInterval < 0 ? ((Ticks)m_ticksPerFrame).ToMilliseconds() : (double)ProcessingInterval); status.AppendLine(); } status.AppendFormat(" Total frame rate timers: {0}", s_frameRateTimers.Count); status.AppendLine(); } status.AppendFormat(" Queued frame count: {0}", m_frameQueue.Count); status.AppendLine(); status.Append(" Last published frame: "); if (lastFrame == null) { status.Append(""); } else { status.Append(((DateTime)lastFrame.Timestamp).ToString("dd-MMM-yyyy HH:mm:ss.fff")); status.AppendLine(); status.Append(" Last sorted measurement: "); status.Append(Measurement.ToString(lastFrame.LastSortedMeasurement)); } status.AppendLine(); return status.ToString(); } } #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. protected virtual void Dispose(bool disposing) { if (!m_disposed) { try { if (disposing) { // Make sure concentrator is stopped Stop(); DetachFromFrameRateTimer(m_framesPerSecond, m_processingInterval); m_publicationThread = null; if (m_publicationWaitHandle != null) { AutoResetEvent publicationWaitHandle = m_publicationWaitHandle; m_publicationWaitHandle = null; publicationWaitHandle.Set(); publicationWaitHandle.Close(); } if (m_frameQueue != null) { m_frameQueue.Dispose(); } m_frameQueue = null; if (m_monitorTimer != null) { m_monitorTimer.Elapsed -= MonitorUnpublishedSamples; m_monitorTimer.Dispose(); } m_monitorTimer = null; if (m_latestMeasurements != null) { m_latestMeasurements.Dispose(); } m_latestMeasurements = null; m_lastDiscardedMeasurement = null; // Clear minimum timer resolution. PrecisionTimer.ClearMinimumTimerResolution(1); } } finally { m_disposed = true; // Prevent duplicate dispose. if (Disposed != null) Disposed(this, EventArgs.Empty); } } } /// /// Starts the concentrator, if it is not already running. /// /// /// Concentrator must be started by calling method or setting /// = true) before concentration will begin. /// public virtual void Start() { if (!m_enabled) { ResetStatistics(); m_stopTime = 0; #if UseHighResolutionTime m_startTime = PrecisionTimer.UtcNow.Ticks; #else m_startTime = DateTime.UtcNow.Ticks; #endif m_frameQueue.Clear(); m_monitorTimer.Start(); // Start real-time frame publication m_enabled = true; } } /// /// Stops the concentrator. /// public virtual void Stop() { if (m_enabled) { m_enabled = false; if (m_monitorTimer != null) m_monitorTimer.Stop(); if (m_frameQueue != null) m_frameQueue.Clear(); #if UseHighResolutionTime m_stopTime = PrecisionTimer.UtcNow.Ticks; #else m_stopTime = DateTime.UtcNow.Ticks; #endif } } /// /// Resets the statistics of the concentrator. /// public virtual void ResetStatistics() { m_receivedMeasurements = 0; m_processedMeasurements = 0; m_discardedMeasurements = 0; m_downsampledMeasurements = 0; m_publishedMeasurements = 0; m_measurementsSortedByArrival = 0; m_missedSortsByTimeout = 0; m_waitHandleExpirations = 0; m_framesAheadOfSchedule = 0; m_publishedFrames = 0; m_totalPublishTime = 0; m_lastDiscardedMeasurement = null; m_lastDiscardedMeasurementLatency = 0; } /// /// Returns the deviation, in seconds, that the given number of ticks is from real-time (i.e., ). /// /// Timestamp to calculate distance from real-time. /// A value indicating the deviation, in seconds, from real-time. public double SecondsFromRealTime(Ticks timestamp) { // Make sure real-time is initialized for initial distance calculation if (Thread.VolatileRead(ref m_realTimeTicks) == 0) { long currentTimeTicks; if (m_performTimestampReasonabilityCheck) #if UseHighResolutionTime currentTimeTicks = PrecisionTimer.UtcNow.Ticks; #else currentTimeTicks = DateTime.UtcNow.Ticks; #endif else currentTimeTicks = timestamp; Thread.VolatileWrite(ref m_realTimeTicks, currentTimeTicks); } return (RealTime - timestamp).ToSeconds(); } /// /// Returns the deviation, in milliseconds, that the given number of ticks is from real-time (i.e., ). /// /// Timestamp to calculate distance from real-time. /// A value indicating the deviation in milliseconds. public double MillisecondsFromRealTime(Ticks timestamp) { return SecondsFromRealTime(timestamp) / SI.Milli; } /// /// Sorts the placing the data point in its proper . /// /// to sort. public virtual void SortMeasurement(IMeasurement measurement) { SortMeasurements(new IMeasurement[] { measurement }); } /// /// Sorts each placing each data point in its proper . /// /// Collection of 's to sort. public virtual void SortMeasurements(IEnumerable measurements) { if (!m_enabled) return; // This function is called continually with new measurements to handle "time-alignment" (i.e., sorting) // of these new values. Many threads will be waiting for frames of time aligned data so make sure any // work to be done here is executed as efficiently as possible. // Note that breaking up this function into several parts might help with readability and make it // easier to maintain, but to reduce function calls (and hence save time) the decision was made to // put the code into one larger more complex function... TrackingFrame frame = null; List discardedMeasurements = null; IMeasurement derivedMeasurement; IFrame sourceFrame; Ticks timestamp = 0, lastTimestamp = 0; double distance; bool discardMeasurement; // Track the total number of measurements ever received for sorting. Interlocked.Add(ref m_receivedMeasurements, measurements.Count()); // Measurements usually come in groups. This function processes all available measurements in the // collection here directly as an optimization which avoids the overhead of a function call for // each measurement. foreach (IMeasurement measurement in measurements) { // Reset flag for next measurement. discardMeasurement = false; // Check for a bad measurement timestamp. if (!m_ignoreBadTimestamps && (measurement.StateFlags & MeasurementStateFlags.BadTime) > 0) { if (m_allowSortsByArrival) { // Device reports measurement timestamp as bad; this typically means that the GPS timestamp of the // source device is not accurate. If the concentrator is set to allow sorts by arrival then it is // assumed that our local real-time value is better than the device measurement, so we sort the // measurement by arrival time. timestamp = RealTime; Interlocked.Increment(ref m_measurementsSortedByArrival); } else { // If sorting by arrival time is not allowed, measurements with bad timestamps are discarded. discardMeasurement = true; } } else { // Timestamp quality is good, get ticks for this measurement. timestamp = measurement.Timestamp; } if (!discardMeasurement) { // // *** Sort the measurement into proper frame *** // // Get the destination frame for the measurement. Note that groups of parsed measurements will // typically be coming in from the same source and will have the same ticks. If we have already // found the destination frame for the same ticks, then there is no need to lookup frame again. if (frame == null || timestamp != lastTimestamp) { if (m_processByReceivedTimestamp) { // If sorting by received timestamp, simply get a frame for this measurement since // this is used in scenarios where very large volumes of data need concentration frame = m_frameQueue.GetFrame(timestamp); lastTimestamp = timestamp; } else { // Badly time-aligned measurements, or those coming in at a higher sample rate, may fall // outside available frame buckets. To check for this, the difference between the measurement // timestamp and real-time in seconds is calculated and validated between lag and lead times. distance = SecondsFromRealTime(timestamp); if (distance > m_lagTime || (m_performTimestampReasonabilityCheck && distance < -m_leadTime)) { // This data has come in late or has a future timestamp. For old timestamps, we're not // going to create a frame for data that will never be processed. For future dates we // must assume that the clock from source device must be advanced and out-of-sync with // real-time - either way this data will be discarded. frame = null; } else { // Get a frame for this measurement frame = m_frameQueue.GetFrame(timestamp); lastTimestamp = timestamp; } } } if (frame == null) { // Measurement is discarded if no bucket (i.e., destination frame) was found for it. discardMeasurement = true; lastTimestamp = 0; } else { // Derive new measurement value applying any needed downsampling derivedMeasurement = frame.DeriveMeasurementValue(measurement); if (derivedMeasurement == null) { // Count this as a discarded measurement if downsampling derivation was not applied. discardMeasurement = true; } else { sourceFrame = frame.SourceFrame; // Access published flag within critical section to ensure no updates will // be made to frame while it is being published frame.Lock.EnterReadLock(); try { if (!sourceFrame.Published) { // Assign derived measurement to its source frame using user customizable function. AssignMeasurementToFrame(sourceFrame, derivedMeasurement); sourceFrame.LastSortedMeasurement = derivedMeasurement; // Track the total number of measurements successfully requested for sorting. Interlocked.Increment(ref m_processedMeasurements); } else { // Track the total number of measurements that failed to sort because the // system ran out of time. Interlocked.Increment(ref m_missedSortsByTimeout); // Count this as a discarded measurement if it was never assigned to the frame. discardMeasurement = true; } } finally { frame.Lock.ExitReadLock(); } // If enabled, concentrator will track the absolute latest measurement values. if (m_trackLatestMeasurements) m_latestMeasurements.UpdateMeasurementValue(derivedMeasurement); } } } if (discardMeasurement) { // This measurement was marked to be discarded. measurement.StateFlags |= MeasurementStateFlags.DiscardedValue; Interlocked.Exchange(ref m_lastDiscardedMeasurement, measurement); Interlocked.Exchange(ref m_lastDiscardedMeasurementLatency, RealTime - m_lastDiscardedMeasurement.Timestamp); // Track total number of discarded measurements Interlocked.Increment(ref m_discardedMeasurements); // Make sure discarded measurement collection exists if (discardedMeasurements == null) discardedMeasurements = new List(); // Add discarded measurement to local collection discardedMeasurements.Add(measurement); } else { // // *** Manage "real-time" ticks *** // if (!m_useLocalClockAsRealTime) { // Algorithm: // If the measurement time is newer than the current real-time value and within the // specified time deviation tolerance of the local clock time, then the measurement // timestamp is set as real-time. if (timestamp > Thread.VolatileRead(ref m_realTimeTicks)) { if (m_performTimestampReasonabilityCheck) { // Apply a resonability check to this value using the local clock. Since the lead time // typically defines the tolerated accuracy of the local clock to real-time, this value // is used as the + and - timestamp tolerance to validate if the time is reasonable. #if UseHighResolutionTime long currentTimeTicks = PrecisionTimer.UtcNow.Ticks; #else long currentTimeTicks = DateTime.UtcNow.Ticks; #endif if (timestamp.TimeIsValid(currentTimeTicks, m_leadTime, m_leadTime)) { // The new time measurement looks good, so this function assumes the time is "real-time" Thread.VolatileWrite(ref m_realTimeTicks, timestamp); } else { // Measurement ticks were outside of time deviation tolerances so we'll also check to make // sure current real-time ticks are within these tolerances as well distance = (currentTimeTicks - Thread.VolatileRead(ref m_realTimeTicks)) / (double)Ticks.PerSecond; if (distance > m_leadTime || distance < -m_leadTime) { // New time measurement was invalid as was current real-time value so we have no choice but to // assume the current time as "real-time", so we set real-time ticks to current ticks Thread.VolatileWrite(ref m_realTimeTicks, currentTimeTicks); } } } else { // Reasonability checks are disabled, assume newest time is real-time... Thread.VolatileWrite(ref m_realTimeTicks, timestamp); } } } } } // Provide discarded measurements to consumers, if any if (discardedMeasurements != null) OnDiscardingMeasurements(discardedMeasurements); } /// /// Publish of time-aligned collection of values that arrived within the /// concentrator's defined . /// /// of measurements with the same timestamp that arrived within that are ready for processing. /// Index of within a second ranging from zero to - 1. /// /// If user implemented publication function consistently exceeds available publishing time (i.e., 1 / seconds), /// concentration will fall behind. A small amount of this time is required by the for processing overhead, so actual total time /// available for user function process will always be slightly less than 1 / seconds. /// protected abstract void PublishFrame(IFrame frame, int index); /// /// Creates a new for the given . /// /// Timestamp for new in . /// New at given . /// /// Derived classes can override this method to create a new custom . Default /// behavior creates a basic to hold synchronized measurements. /// protected internal virtual IFrame CreateNewFrame(Ticks timestamp) { return new Frame(timestamp, m_expectedMeasurements); } /// /// Assigns to its associated . /// /// /// Derived classes can choose to override this method to handle custom assignment of a to /// its . Default behavior simply assigns measurement to frame's keyed measurement dictionary: /// frame.Measurements[measurement.Key] = measurement; /// /// The that is used. /// The type of to use."/> protected virtual void AssignMeasurementToFrame(IFrame frame, IMeasurement measurement) { frame.Measurements[measurement.Key] = measurement; } /// /// Raises the event. /// /// Exception to send to event. /// /// Allows derived classes to raise a processing exception. /// protected virtual void OnProcessException(Exception ex) { if (ProcessException != null) ProcessException(this, new EventArgs(ex)); } /// /// Raises the event. /// /// Total number of unpublished seconds of data. protected virtual void OnUnpublishedSamples(int seconds) { if (UnpublishedSamples != null) UnpublishedSamples(this, new EventArgs(seconds)); } /// /// Raises the event. /// /// Enumeration of values being discarded. /// /// Allows derived classes to raise a discarding measurements event. /// protected virtual void OnDiscardingMeasurements(IEnumerable measurements) { if (DiscardingMeasurements != null) DiscardingMeasurements(this, new EventArgs>(measurements)); } // Tick handler for frame rate timer simply signals waiting thread to publish private void StartFramePublication(object sender, EventArgs e) { if (m_enabled) m_publicationWaitHandle.Set(); } // Frame publication handler private void PublishFrames() { TrackingFrame frame; IFrame sourceFrame; Ticks timestamp; long startTime, stopTime; int frameIndex; // Keep thread alive... while (m_publicationWaitHandle != null) { // Keep publishing frames so long as they are ready for publication. This handles case where // system may be falling behind because user function is taking too long - exit when no // other frames are available to process while (m_enabled) { try { // Get top frame frame = m_frameQueue.Head; if (frame == null) { // No frame ready to publish, exit break; } else { // Get ticks for this frame sourceFrame = frame.SourceFrame; timestamp = sourceFrame.Timestamp; if (m_processByReceivedTimestamp) { // When processing by received timestamp, we need to test received timestamp against lagtime // to make sure there has been time enough to publish frame: if (m_lagTicks - (RealTime - sourceFrame.ReceivedTimestamp) > 0) break; } else { // See if any lagtime needs to pass before we begin publishing, exiting if it's not time to publish if (m_lagTicks - (RealTime - timestamp) > 0) { // It's not the scheduled time to publish this frame, however, if preemptive publishing is enabled, // an expected number of measurements per-frame have been defined and the frame has received this // expected number of measurements, we can go ahead and publish the frame ahead of schedule. This // is useful if the lag time is high to ensure no data is missed but it's desirable to publish the // frame as soon as the expected data has arrived. if (m_expectedMeasurements < 1 || !m_allowPreemptivePublishing || sourceFrame.SortedMeasurements < m_expectedMeasurements) break; // All data has been received for this frame, so we'll go ahead and publish ahead-of-schedule Interlocked.Increment(ref m_framesAheadOfSchedule); } } // Handle any needed external synchronization, if defined if (m_externalEventHandles != null) WaitHandle.WaitAll(m_externalEventHandles, m_externalEventTimeout); // Mark start time for publication #if UseHighResolutionTime startTime = PrecisionTimer.UtcNow.Ticks; #else startTime = DateTime.UtcNow.Ticks; #endif // Calculate index of this frame within its second - note that we have to calculate this // value instead of using frameIndex since it is is possible for multiple frames to be // published within one frame period if the system is stressed frameIndex = (int)(((double)timestamp.DistanceBeyondSecond() + m_timeOffset) / m_ticksPerFrame); // Mark the frame as published to prevent any further sorting into this frame - setting this flag // is in a critcal section to ensure that sorting into this frame has ceased prior to publication frame.Lock.EnterWriteLock(); try { sourceFrame.Published = true; } finally { frame.Lock.ExitWriteLock(); } try { // Publish the current frame (i.e., call user implemented publication function) PublishFrame(sourceFrame, frameIndex); } finally { // Remove the frame from the queue whether it is successfully published or not m_frameQueue.Pop(); // Update publication statistics Interlocked.Increment(ref m_publishedFrames); Interlocked.Add(ref m_publishedMeasurements, sourceFrame.SortedMeasurements); Interlocked.Add(ref m_downsampledMeasurements, frame.DownsampledMeasurements); // Mark stop time for publication #if UseHighResolutionTime stopTime = PrecisionTimer.UtcNow.Ticks; #else stopTime = DateTime.UtcNow.Ticks; #endif if (m_trackPublishedTimestamp) sourceFrame.PublishedTimestamp = stopTime; // Track total publication time Interlocked.Add(ref m_totalPublishTime, stopTime - startTime); } } } catch (ThreadAbortException) { throw; } catch (Exception ex) { // Not stopping for exceptions - but we'll let user know there are issues... OnProcessException(ex); break; } } // Wait for next publication signal, timing out if signal takes too long if (m_usePrecisionTimer && m_publicationWaitHandle != null && !m_publicationWaitHandle.WaitOne(m_maximumPublicationTimeout)) m_waitHandleExpirations++; else Thread.Sleep(1); } } // Exposes the number of unpublished seconds of data in the queue (note that first second of data will always be "publishing"). private void MonitorUnpublishedSamples(object sender, System.Timers.ElapsedEventArgs e) { int secondsOfData = (m_frameQueue.Count / m_framesPerSecond) - 1; if (secondsOfData < 0) secondsOfData = 0; OnUnpublishedSamples(secondsOfData); } // Handle attach to frame rate timer private void AttachToFrameRateTimer(int framesPerSecond, int processingInterval) { Tuple key = new Tuple(framesPerSecond, processingInterval); lock (s_frameRateTimers) { if (!m_attachedToFrameRateTimer) { FrameRateTimer timer; // Get static frame rate timer for given frames per second creating it if needed if (!s_frameRateTimers.TryGetValue(key, out timer)) { // Create a new frame rate timer which includes a high-precision timer for frame processing timer = new FrameRateTimer(framesPerSecond, processingInterval); // Add timer state for given rate to static collection s_frameRateTimers.Add(key, timer); } // Increment reference count and attach instance method "StartFramePublication" to static timer event list timer.AddReference(StartFramePublication); m_attachedToFrameRateTimer = true; } } } // Handle detach from frame rate timer private void DetachFromFrameRateTimer(int framesPerSecond, int processingInterval) { Tuple key = new Tuple(framesPerSecond, processingInterval); lock (s_frameRateTimers) { if (m_attachedToFrameRateTimer) { FrameRateTimer timer; // Look up static frame rate timer for given frames per second if (s_frameRateTimers.TryGetValue(key, out timer)) { // Decrement reference count and detach instance method "StartFramePublication" from static timer event list timer.RemoveReference(StartFramePublication); m_attachedToFrameRateTimer = false; // If timer is no longer being referenced we stop it and remove it from static collection if (timer.ReferenceCount == 0) { timer.Dispose(); s_frameRateTimers.Remove(key); } } } } } #endregion #region [ Static ] // Static Fields private static Dictionary, FrameRateTimer> s_frameRateTimers; // Static Constructor static ConcentratorBase() { s_frameRateTimers = new Dictionary, FrameRateTimer>(); } #endregion } }