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
26 changes: 10 additions & 16 deletions src/AR.Iec61850.Transports.Npcap/SvTransmitIntervalEstimator.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
namespace AR.Iec61850.Transports.Npcap;

/// <summary>
/// Learns a stable SV transmit interval without allowing one scheduler-late observation
/// Learns a stable SV transmit interval without allowing scheduler-late observations
/// to become the permanent wire rate. The estimator activates only after several
/// mutually consistent intervals have been observed.
/// mutually consistent unpaced intervals have been observed, then remains immutable
/// for the lifetime of that stream session.
/// </summary>
internal sealed class SvTransmitIntervalEstimator
{
Expand Down Expand Up @@ -35,23 +36,16 @@ public SvTransmitIntervalEstimator(

public void Observe(long intervalTicks)
{
if (intervalTicks < _minimumIntervalTicks || intervalTicks > _maximumIntervalTicks)
{
if (NominalIntervalTicks == 0)
ResetCandidate();
// Once pacing is active, every subsequent interval is influenced by the pacer
// itself and by injection/scheduler lateness. Feeding those observations back
// into the nominal would create a one-way ratchet toward a slower wire rate.
// A deliberate sample-rate change therefore starts a new transport/session.
if (NominalIntervalTicks > 0)
return;
}

if (NominalIntervalTicks > 0)
if (intervalTicks < _minimumIntervalTicks || intervalTicks > _maximumIntervalTicks)
{
var minimumAccepted = NominalIntervalTicks * 3 / 4;
var maximumAccepted = NominalIntervalTicks * 3 / 2;
if (intervalTicks >= minimumAccepted && intervalTicks <= maximumAccepted)
{
NominalIntervalTicks = (long)Math.Round(
(NominalIntervalTicks * 0.9) + (intervalTicks * 0.1));
}

ResetCandidate();
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,45 @@ public void InconsistentIntervals_DoNotActivatePacing()
[Fact]
public void ActiveNominalRate_IgnoresLongSchedulerStall()
{
var estimator = new SvTransmitIntervalEstimator(20, 5_000, requiredConsistentIntervals: 4);
foreach (var interval in new long[] { 250, 249, 251, 250 })
estimator.Observe(interval);

var estimator = CreateActiveEstimator();
var nominalBeforeStall = estimator.NominalIntervalTicks;

estimator.Observe(4_000);

Assert.Equal(nominalBeforeStall, estimator.NominalIntervalTicks);
}

[Fact]
public void ActiveNominalRate_TracksOnlyNearbySteadyIntervals()
public void ActiveNominalRate_DoesNotRatchetOnModerateLateness()
{
var estimator = new SvTransmitIntervalEstimator(20, 5_000, requiredConsistentIntervals: 4);
foreach (var interval in new long[] { 250, 250, 250, 250 })
estimator.Observe(interval);
var estimator = CreateActiveEstimator();
var nominal = estimator.NominalIntervalTicks;

foreach (var delayedInterval in new long[] { 300, 310, 290, 320, 300, 305, 295, 315 })
estimator.Observe(delayedInterval);

Assert.Equal(nominal, estimator.NominalIntervalTicks);
}

[Fact]
public void ActiveNominalRate_DoesNotFeedPacedIntervalsBackIntoEstimator()
{
var estimator = CreateActiveEstimator();
var nominal = estimator.NominalIntervalTicks;

estimator.Observe(248);
estimator.Observe(252);
estimator.Observe(260);

Assert.InRange(estimator.NominalIntervalTicks, 250, 252);
Assert.Equal(nominal, estimator.NominalIntervalTicks);
}

private static SvTransmitIntervalEstimator CreateActiveEstimator()
{
var estimator = new SvTransmitIntervalEstimator(20, 5_000, requiredConsistentIntervals: 4);
foreach (var interval in new long[] { 250, 249, 251, 250 })
estimator.Observe(interval);
Assert.InRange(estimator.NominalIntervalTicks, 249, 251);
return estimator;
}
}
Loading