diff --git a/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs b/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs index 4800d9503..24807b90e 100644 --- a/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs +++ b/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs @@ -17,14 +17,21 @@ public class HistoryBounds // history limit. oldest bounds will be removed. public readonly int limit; + // only remove old entries every n-th insertion. + // new entries are still encapsulated on every insertion. + // for example, every 2nd insertion is enough, and 2x as fast. + public readonly int recalculateEveryNth; + int recalculateCounter = 0; + // total bounds encapsulating all of the bounds history public Bounds total; - public HistoryBounds(int limit) + public HistoryBounds(int limit, int recalculateEveryNth) { // initialize queue with maximum capacity to avoid runtime resizing // +1 because it makes the code easier if we insert first, and then remove. this.limit = limit; + this.recalculateEveryNth = recalculateEveryNth; history = new Queue(limit + 1); } @@ -47,6 +54,14 @@ public void Insert(Bounds bounds) // remove oldest history.Dequeue(); + // optimization: only recalculate every n-th removal. + // accurate enough, and N times faster. + if (++recalculateCounter < recalculateEveryNth) + return; + + // reset counter + recalculateCounter = 0; + // recalculate total bounds // (only needed after removing the oldest) total = bounds; diff --git a/Assets/Mirror/Tests/Editor/LagCompensation/HistoryBoundsTests.cs b/Assets/Mirror/Tests/Editor/LagCompensation/HistoryBoundsTests.cs index df6aa3c7b..48340e98e 100644 --- a/Assets/Mirror/Tests/Editor/LagCompensation/HistoryBoundsTests.cs +++ b/Assets/Mirror/Tests/Editor/LagCompensation/HistoryBoundsTests.cs @@ -23,12 +23,13 @@ public static Bounds MinMax(float min, float max) => // 64 entries are much more than we would usually use. // // Unity 2021.3 LTS, release mode: 10x000 x 65; limit=8 - // O(N) Queue implementation: 1045 ms + // O(N) Queue implementation: 1045 ms + // O(N) Queue and recalculate every 2nd: 640 ms [Test] - [TestCase(10_000, 64, 8)] - public void Benchmark(int iterations, int insertions, int limit) + [TestCase(10_000, 64, 8, 2)] + public void Benchmark(int iterations, int insertions, int limit, int recalculate) { - HistoryBounds history = new HistoryBounds(limit); + HistoryBounds history = new HistoryBounds(limit, recalculate); // always use the same seed so we get the same test. Random.InitState(0); @@ -58,7 +59,7 @@ public void Benchmark(int iterations, int insertions, int limit) public void Insert_Basic() { const int limit = 3; - HistoryBounds history = new HistoryBounds(limit); + HistoryBounds history = new HistoryBounds(limit, recalculateEveryNth: 1); // insert initial [-1, 1]. // should calculate new bounds == initial. @@ -93,7 +94,7 @@ public void Insert_Basic() public void Insert_Revisit() { const int limit = 3; - HistoryBounds history = new HistoryBounds(limit); + HistoryBounds history = new HistoryBounds(limit, recalculateEveryNth: 1); // insert initial [-1, 1]. // should calculate new bounds == initial. @@ -123,10 +124,10 @@ public void Insert_Revisit() // by default, HistoryBounds.total is new Bounds() which is (0,0). // make sure this isn't included in results by default. [Test] - public void InsertFar() + public void Insert_Far() { const int limit = 3; - HistoryBounds history = new HistoryBounds(limit); + HistoryBounds history = new HistoryBounds(limit, recalculateEveryNth: 1); // insert initial [2, 3]. // should calculate new bounds == initial. @@ -154,12 +155,71 @@ public void InsertFar() Assert.That(history.Count, Is.EqualTo(3)); Assert.That(history.total, Is.EqualTo(MinMax(0.5f, 4))); } + // test to check if recalculate works as expected + [Test] + public void Insert_Recalculate() + { + const int limit = 3; + HistoryBounds history = new HistoryBounds(limit, recalculateEveryNth: 2); + + // insert initial [-1, 1]. + // should calculate new bounds == initial. + history.Insert(MinMax(-1, 1)); + Assert.That(history.Count, Is.EqualTo(1)); + Assert.That(history.total, Is.EqualTo(MinMax(-1, 1))); + + // insert [0, 2] + // should calculate new bounds == [-1, 2]. + history.Insert(MinMax(0, 2)); + Assert.That(history.Count, Is.EqualTo(2)); + Assert.That(history.total, Is.EqualTo(MinMax(-1, 2))); + + // insert one that's smaller than current bounds [-.5, 0] + // history needs to contain it even if smaller, because once the oldest + // largest one gets removed, this one matters too. + history.Insert(MinMax(-0.5f, 1)); + Assert.That(history.Count, Is.EqualTo(3)); + Assert.That(history.total, Is.EqualTo(MinMax(-1, 2))); + + // insert more than 'limit': [0, 0] + // the oldest one [-1, 1] should be discarded. + // recalculate counter is 0+=1. nothing is recalculated yet. + history.Insert(MinMax(0, 0)); + Assert.That(history.Count, Is.EqualTo(3)); + Assert.That(history.total, Is.EqualTo(MinMax(-1, 2))); + + // insert more than 'limit': [0, 0] + // the oldest one [0, 2] should be discarded. + // recalculate counter is 1+=1. new bounds are recalculated. + // recalculate counter should be reset to 0. + history.Insert(MinMax(0, 0)); + Assert.That(history.Count, Is.EqualTo(3)); + Assert.That(history.total, Is.EqualTo(MinMax(-0.5f, 1))); + + // ... test another iteration to ensure recalculate counter was + // reset properly ... + + // insert more than 'limit': [0, 0] + // the oldest one [-0.5, 1] should be discarded. + // recalculate counter is 0+=1. nothing is recalculated yet. + history.Insert(MinMax(0, 0)); + Assert.That(history.Count, Is.EqualTo(3)); + Assert.That(history.total, Is.EqualTo(MinMax(-0.5f, 1))); + + // insert more than 'limit': [0, 0] + // the oldest one [0, 0] should be discarded. + // recalculate counter is 1+=1. new bounds are recalculated. + // recalculate counter should be reset to 0. + history.Insert(MinMax(0, 0)); + Assert.That(history.Count, Is.EqualTo(3)); + Assert.That(history.total, Is.EqualTo(MinMax(0, 0))); + } [Test] public void Reset() { const int limit = 3; - HistoryBounds history = new HistoryBounds(limit); + HistoryBounds history = new HistoryBounds(limit, recalculateEveryNth: 1); history.Insert(MinMax(1, 2)); history.Insert(MinMax(2, 3));