fix: SnapshotInterpolation.Insert() now has a bufferLimit to avoid ever growing snapshot buffers on extremely low-fps clients

This commit is contained in:
mischa 2023-06-27 19:10:30 +08:00
parent 60936f0646
commit 37bbd7eb3d
8 changed files with 81 additions and 47 deletions

View File

@ -153,13 +153,17 @@ protected void AddSnapshot(SortedList<double, TransformSnapshot> snapshots, doub
if (!scale.HasValue) scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : target.localScale;
// insert transform snapshot
SnapshotInterpolation.InsertIfNotExists(snapshots, new TransformSnapshot(
SnapshotInterpolation.InsertIfNotExists(
snapshots,
NetworkClient.snapshotSettings.bufferLimit,
new TransformSnapshot(
timeStamp, // arrival remote timestamp. NOT remote time.
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
position.Value,
rotation.Value,
scale.Value
));
)
);
}
// apply a snapshot to the Transform.

View File

@ -392,13 +392,17 @@ static void RewriteHistory(
// insert a fake one at where we used to be,
// 'sendInterval' behind the new one.
SnapshotInterpolation.InsertIfNotExists(snapshots, new TransformSnapshot(
SnapshotInterpolation.InsertIfNotExists(
snapshots,
NetworkClient.snapshotSettings.bufferLimit,
new TransformSnapshot(
remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time.
localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet
position,
rotation,
scale
));
)
);
}
public override void Reset()

View File

@ -143,6 +143,7 @@ public static void OnTimeSnapshot(TimeSnapshot snap)
// insert into the buffer & initialize / adjust / catchup
SnapshotInterpolation.InsertAndAdjust(
snapshots,
snapshotSettings.bufferLimit,
snap,
ref localTimeline,
ref localTimescale,

View File

@ -80,6 +80,7 @@ public void OnTimeSnapshot(TimeSnapshot snapshot)
// insert into the server buffer & initialize / adjust / catchup
SnapshotInterpolation.InsertAndAdjust(
snapshots,
NetworkClient.snapshotSettings.bufferLimit,
snapshot,
ref remoteTimeline,
ref remoteTimescale,

View File

@ -91,9 +91,16 @@ public static double DynamicAdjustment(
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool InsertIfNotExists<T>(
SortedList<double, T> buffer, // snapshot buffer
int bufferLimit, // don't grow infinitely
T snapshot) // the newly received snapshot
where T : Snapshot
{
// slow clients may not be able to process incoming snapshots fast enough.
// infinitely growing snapshots would make it even worse.
// for example, run NetworkRigidbodyBenchmark while deep profiling client.
// the client just grows and reallocates the buffer forever.
if (buffer.Count >= bufferLimit) return false;
// SortedList does not allow duplicates.
// we don't need to check ContainsKey (which is expensive).
// simply add and compare count before/after for the return value.
@ -136,6 +143,7 @@ public static double TimelineClamp(
// adds / inserts it to the list & initializes local time if needed.
public static void InsertAndAdjust<T>(
SortedList<double, T> buffer, // snapshot buffer
int bufferLimit, // don't grow infinitely
T snapshot, // the newly received snapshot
ref double localTimeline, // local interpolation time based on server time
ref double localTimescale, // timeline multiplier to apply catchup / slowdown over time
@ -167,7 +175,7 @@ public static void InsertAndAdjust<T>(
// note that insert may be called twice for the same key.
// by default, this would throw.
// need to handle it silently.
if (InsertIfNotExists(buffer, snapshot))
if (InsertIfNotExists(buffer, bufferLimit, snapshot))
{
// dynamic buffer adjustment needs delivery interval jitter
if (buffer.Count >= 2)

View File

@ -16,6 +16,9 @@ public class SnapshotInterpolationSettings
[Tooltip("Local simulation is behind by sendInterval * multiplier seconds.\n\nThis guarantees that we always have enough snapshots in the buffer to mitigate lags & jitter.\n\nIncrease this if the simulation isn't smooth. By default, it should be around 2.")]
public double bufferTimeMultiplier = 2;
[Tooltip("If a client can't process snapshots fast enough, don't store too many.")]
public int bufferLimit = 32;
// catchup /////////////////////////////////////////////////////////////
// catchup thresholds in 'frames'.
// half a frame might be too aggressive.
@ -63,6 +66,5 @@ public class SnapshotInterpolationSettings
[Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")]
public int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time
}
}

View File

@ -87,6 +87,7 @@ public void OnMessage(Snapshot3D snap)
// insert into the buffer & initialize / adjust / catchup
SnapshotInterpolation.InsertAndAdjust(
snapshots,
snapshotSettings.bufferLimit,
snap,
ref localTimeline,
ref localTimescale,

View File

@ -36,6 +36,7 @@ public class SnapshotInterpolationTests
const double slowdownSpeed = 0.04;
const double negativeThresh = -0.10; // in seconds
const double positiveThresh = 0.10; // in seconds
const int bufferLimit = 32;
[SetUp]
public void SetUp()
@ -132,28 +133,40 @@ public void InsertIfNotExists()
SimpleSnapshot b = new SimpleSnapshot(3, 0, 43);
// add a
Assert.True(SnapshotInterpolation.InsertIfNotExists(buffer, a));
Assert.True(SnapshotInterpolation.InsertIfNotExists(buffer, bufferLimit, a));
Assert.That(buffer.Count, Is.EqualTo(1));
Assert.That(buffer.Values[0], Is.EqualTo(a));
// add a again - shouldn't do anything
Assert.False(SnapshotInterpolation.InsertIfNotExists(buffer, a));
Assert.False(SnapshotInterpolation.InsertIfNotExists(buffer, bufferLimit, a));
Assert.That(buffer.Count, Is.EqualTo(1));
Assert.That(buffer.Values[0], Is.EqualTo(a));
// add b
Assert.True(SnapshotInterpolation.InsertIfNotExists(buffer, b));
Assert.True(SnapshotInterpolation.InsertIfNotExists(buffer, bufferLimit, b));
Assert.That(buffer.Count, Is.EqualTo(2));
Assert.That(buffer.Values[0], Is.EqualTo(a));
Assert.That(buffer.Values[1], Is.EqualTo(b));
// add b again - shouldn't do anything
Assert.False(SnapshotInterpolation.InsertIfNotExists(buffer, b));
Assert.False(SnapshotInterpolation.InsertIfNotExists(buffer, bufferLimit, b));
Assert.That(buffer.Count, Is.EqualTo(2));
Assert.That(buffer.Values[0], Is.EqualTo(a));
Assert.That(buffer.Values[1], Is.EqualTo(b));
}
[Test]
public void InsertIfNotExists_RespectsBufferLimit()
{
// guarantee that we can never insert more than buffer limit
for (int i = 0; i < bufferLimit * 2; ++i)
{
SimpleSnapshot snap = new SimpleSnapshot(i, i, i);
SnapshotInterpolation.InsertIfNotExists(buffer, bufferLimit, snap);
}
Assert.That(buffer.Count, Is.EqualTo(bufferLimit));
}
// UDP packets may arrive twice with the same snapshot.
// inserting twice needs to be handled without throwing exceptions.
[Test]
@ -168,8 +181,8 @@ public void InsertTwice()
double localTimescale = 0;
// insert twice
SnapshotInterpolation.InsertAndAdjust(buffer, snap, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, snap, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, snap, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, snap, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
// should only be inserted once
Assert.That(buffer.Count, Is.EqualTo(1));
@ -190,8 +203,8 @@ public void Insert_Sorts()
SimpleSnapshot b = new SimpleSnapshot(3, 0, 43);
// insert in reverse order
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
// should be in sorted order
Assert.That(buffer.Count, Is.EqualTo(2));
@ -215,11 +228,11 @@ public void Insert_InitializesLocalTimeline()
SimpleSnapshot b = new SimpleSnapshot(3, 0, 43);
// first insertion should initialize the local timeline to remote time
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, a, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2 - bufferTime)); // initial snapshot - buffer time
// second insertion should not modify the timeline again
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, b, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2 - bufferTime)); // initial snapshot - buffer time
}
@ -240,13 +253,13 @@ public void Insert_ComputesAverageDrift()
SimpleSnapshot c = new SimpleSnapshot(5, 0, 43);
// insert in order
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, a, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2 - bufferTime)); // initial snapshot - buffer time
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, b, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2 - bufferTime)); // initial snapshot - buffer time
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, c, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2 - bufferTime)); // initial snapshot - buffer time
// first insertion initializes localTime to '2'.
@ -272,13 +285,13 @@ public void Insert_ComputesAverageDrift_Scrambled()
SimpleSnapshot c = new SimpleSnapshot(5, 0, 43);
// insert scrambled (not in order)
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, a, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2 - bufferTime)); // initial snapshot - buffer time
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, c, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2 - bufferTime)); // initial snapshot - buffer time
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, b, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2 - bufferTime)); // initial snapshot - buffer time
// first insertion initializes localTime to '2'.
@ -309,13 +322,13 @@ public void Insert_ComputesAverageDeliveryInterval()
SimpleSnapshot c = new SimpleSnapshot(5, 6, 43);
// insert in order
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, a, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2 - bufferTime)); // initial snapshot - buffer time
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, b, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2 - bufferTime)); // initial snapshot - buffer time
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, c, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2 - bufferTime)); // initial snapshot - buffer time
// first insertion doesn't compute delivery interval because we need 2 snaps.
@ -345,13 +358,13 @@ public void Insert_ComputesAverageDeliveryInterval_Scrambled()
SimpleSnapshot c = new SimpleSnapshot(5, 6, 43);
// insert in order
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, a, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2)); // detect wrong timeline immediately
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, c, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2)); // detect wrong timeline immediately
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, b, ref localTimeline, ref localTimescale, 0, 0, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(2)); // detect wrong timeline immediately
@ -380,13 +393,13 @@ public void Sample()
SimpleSnapshot b = new SimpleSnapshot(20, 0, 43);
SimpleSnapshot c = new SimpleSnapshot(30, 0, 44);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, a, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(10-bufferTime)); // initial snapshot - buffer time
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, b, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(10-bufferTime)); // initial snapshot - buffer time
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, c, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(10-bufferTime)); // initial snapshot - buffer time
// sample at a time before the first snapshot
@ -424,13 +437,13 @@ public void Step()
SimpleSnapshot b = new SimpleSnapshot(20, 0, 43);
SimpleSnapshot c = new SimpleSnapshot(30, 0, 44);
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, a, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(10-bufferTime)); // initial snapshot - buffer time
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, b, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(10-bufferTime)); // initial snapshot - buffer time
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, c, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(10-bufferTime)); // initial snapshot - buffer time
// step half way to the next snapshot
@ -455,13 +468,13 @@ public void Step_RemovesOld()
SimpleSnapshot c = new SimpleSnapshot(30, 0, 44);
double bufferTime = 30; // don't move timeline until all 3 inserted
SnapshotInterpolation.InsertAndAdjust(buffer, a, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, a, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(10-bufferTime)); // initial snapshot - buffer time
SnapshotInterpolation.InsertAndAdjust(buffer, b, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, b, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(10-bufferTime)); // initial snapshot - buffer time
SnapshotInterpolation.InsertAndAdjust(buffer, c, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
SnapshotInterpolation.InsertAndAdjust(buffer, bufferLimit, c, ref localTimeline, ref localTimescale, 0, bufferTime, 0.01, 0.01, ref driftEma, 0, 0, ref deliveryIntervalEma);
Assert.That(localTimeline, Is.EqualTo(10-bufferTime)); // initial snapshot - buffer time
// step 1.5 snapshots worth, so way past the first one