Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions Source/Libraries/GSF.PhasorProtocols/ReplayTimer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,17 @@ public sealed class ReplayTimer
// time remains, then yield/spin to the exact deadline
private const double SleepGuardBand = 2.0D;

// Maximum deficit, in milliseconds, that the deadline may fall behind real
// time. This bounds the catch-up burst produced after a long stall (e.g., a
// pause/resume or a slow buffer read) while still allowing the normal buffer-
// read gaps of file playback to be fully recovered so the defined frame rate
// is maintained. Raise toward infinity for unbounded catch-up; lower for
// tighter burst control at the risk of dropping below the defined rate.
private const double MaxCatchUpLag = 1000.0D;

private readonly long m_periodTicks; // Query Performance Counter (QPC) ticks per frame
private readonly long m_guardBandTicks; // QPC ticks for the guard-band
private readonly long m_maxLagTicks; // QPC ticks for the maximum catch-up deficit
private long m_nextTick; // Next scheduled QPC tick (not equal to DateTime ticks)

/// <summary>
Expand All @@ -63,6 +72,7 @@ public ReplayTimer(int definedFrameRate)

m_periodTicks = (long)Math.Round(Stopwatch.Frequency / (double)definedFrameRate);
m_guardBandTicks = (long)Math.Round(Stopwatch.Frequency * SleepGuardBand / 1000.0D);
m_maxLagTicks = (long)Math.Round(Stopwatch.Frequency * MaxCatchUpLag / 1000.0D);
m_nextTick = Stopwatch.GetTimestamp();

DefinedFrameRate = definedFrameRate;
Expand All @@ -77,30 +87,39 @@ public ReplayTimer(int definedFrameRate)
/// Blocks until the next scheduled frame rate interval.
/// </summary>
/// <remarks>
/// The deadline advances by exactly one period from the current deadline (absolute
/// cadence) to keep inter-frame intervals consistent. If the deadline has already
/// fallen behind by more than one full period, it resets to "now + period" to
/// prevent a burst of catch-up frames.
/// The deadline advances by exactly one period from the previous deadline (absolute
/// cadence) so the long-term average rate stays locked to the defined frame rate even
/// when frames arrive in bursts separated by buffer-read gaps. When the deadline is
/// already at or behind the current time, the method returns without waiting so playback
/// can catch back up; the accrued deficit is clamped (see <see cref="MaxCatchUpLag"/>) so
/// a long stall cannot produce an unbounded burst of catch-up frames.
/// </remarks>
public void WaitNext()
{
// Advance deadline absolutely from previous deadline to maintain
// a steady cadence regardless of per-frame processing jitter
// Absolute cadence: advance the deadline by exactly one period from the
// previous deadline so the long-term average rate stays locked to the
// defined frame rate. File frames arrive in bursts separated by buffer-read
// gaps; during a gap the deadline accrues a deficit that is recovered on the
// following frames (which return without waiting) until the cadence catches
// back up to real time — this is what lets high replay rates keep pace.
long now = Stopwatch.GetTimestamp();
long nextTick = m_nextTick + m_periodTicks;

// If the advanced deadline is already in the past, we've fallen
// behind by more than a full period — reset to relative mode to
// avoid a burst of catch-up frames
if (Stopwatch.GetTimestamp() >= nextTick)
nextTick = Stopwatch.GetTimestamp() + m_periodTicks;
long ticksRemaining = nextTick - now;

m_nextTick = nextTick;
// At or past the deadline (processing slower than the interval, or catching
// up after a gap): return immediately without waiting. Clamp the accrued
// deficit so a long stall cannot produce an unbounded burst of catch-up frames.
if (ticksRemaining <= 0L)
{
if (now - nextTick > m_maxLagTicks)
nextTick = now - m_maxLagTicks;

// Fast exit if already past deadline (e.g., processing took longer than interval)
if (Stopwatch.GetTimestamp() >= nextTick)
m_nextTick = nextTick;
return;
}

long ticksRemaining = nextTick - Stopwatch.GetTimestamp();
m_nextTick = nextTick;

// For longer waits, sleep in one bulk call leaving only a small
// guard-band for spin/yield — this dramatically reduces the number
Expand All @@ -117,7 +136,7 @@ public void WaitNext()
// Yield / spin for the remaining guard-band to hit precise deadline
while (true)
{
long now = Stopwatch.GetTimestamp();
now = Stopwatch.GetTimestamp();

if (now >= nextTick)
return;
Expand Down
26 changes: 13 additions & 13 deletions Source/Libraries/GSF.PhasorProtocols/SelCWS/Common.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,31 +49,31 @@ public enum FrameType : byte
/// Phase channels for SEL CWS PoW analogs.
/// </summary>
public enum PhaseChannel
{
{
/// <summary>
/// Phase A current (IA).
/// Phase A voltage (VA).
/// </summary>
IA = 0,
VA = 0,
/// <summary>
/// Phase B current (IB).
/// Phase B voltage (VB).
/// </summary>
IB = 1,
VB = 1,
/// <summary>
/// Phase C current (IC).
/// Phase C voltage (VC).
/// </summary>
IC = 2,
VC = 2,
/// <summary>
/// Phase A voltage (VA).
/// Phase A current (IA).
/// </summary>
VA = 3,
IA = 3,
/// <summary>
/// Phase B voltage (VB).
/// Phase B current (IB).
/// </summary>
VB = 4,
IB = 4,
/// <summary>
/// Phase C voltage (VC).
/// Phase C current (IC).
/// </summary>
VC = 5
IC = 5
}

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ internal ConfigurationCell(ConfigurationFrame parent, LineFrequency nominalFrequ

for (int i = 0; i < Common.MaximumPhasorValues; i++)
{
PhasorDefinitions.Add(new PhasorDefinition(this, analogNames[i], i < 3 ? PhasorType.Current : PhasorType.Voltage));
PhasorDefinitions.Add(new PhasorDefinition(this, analogNames[i], i < 3 ? PhasorType.Voltage : PhasorType.Current));
}
}

Expand Down
8 changes: 4 additions & 4 deletions Source/Libraries/GSF.PhasorProtocols/SelCWS/FrameParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ protected override int ParseFrame(byte[] buffer, int offset, int length)

if (buffer[offset] != (byte)FrameType.DataFrame || m_initialDataFrame is null)
return parsedLength;

// Make sure enough frame buffer image is available for data frame to be parsed
if (length < parsedLength)
return 0;
Expand Down Expand Up @@ -414,12 +414,12 @@ private void ApplyEstimatedPhases(DataFrame dataFrame)
return;

// Expected order defined by SEL CWS protocol:
double ia = cell.AnalogValues[(int)PhaseChannel.IA].Value;
double ib = cell.AnalogValues[(int)PhaseChannel.IB].Value;
double ic = cell.AnalogValues[(int)PhaseChannel.IC].Value;
double va = cell.AnalogValues[(int)PhaseChannel.VA].Value;
double vb = cell.AnalogValues[(int)PhaseChannel.VB].Value;
double vc = cell.AnalogValues[(int)PhaseChannel.VC].Value;
double ia = cell.AnalogValues[(int)PhaseChannel.IA].Value;
double ib = cell.AnalogValues[(int)PhaseChannel.IB].Value;
double ic = cell.AnalogValues[(int)PhaseChannel.IC].Value;

// Ensure phase estimator is created
m_phaseEstimator ??= new RollingPhaseEstimator(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 02/08/2007 - J. Ritchie Carroll & Jian Ryan Zuo
// 11/04/2025 - Ritchie Carroll
// Generated original version of source code.
// 09/15/2009 - Stephen C. Wills
// Added new header and license agreement.
// 12/17/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************

Expand Down
Loading