From f4650eff64211e955142a60d310145f089fdde6c Mon Sep 17 00:00:00 2001 From: mischa Date: Wed, 26 Jul 2023 11:24:59 +0800 Subject: [PATCH] buckets --- .../Core/LagCompensation/HistoryBounds.cs | 108 ++++++++++++------ .../LagCompensation/HistoryBoundsTests.cs | 52 +++++---- 2 files changed, 104 insertions(+), 56 deletions(-) diff --git a/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs b/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs index 24807b90e..fdcc8ac92 100644 --- a/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs +++ b/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs @@ -10,29 +10,45 @@ namespace Mirror { public class HistoryBounds { - // history of bounds - readonly Queue history; - public int Count => history.Count; + // FakeByte: gather bounds in smaller buckets. + // for example, bucket(t0,t1,t2), bucket(t3,t4,t5), ... + // instead of removing old bounds t0, t1, ... + // we remove a whole bucket every 3 times: bucket(t0,t1,t2) + // and when building total bounds, we encapsulate a few larger buckets + // instead of many smaller bounds. + // + // => a bucket is encapsulate(bounds0, bounds1, bounds2) so we don't + // need a custom struct, simply reuse bounds but remember that each + // entry includes N timestamps. + // + // => note that simply reducing capture interval is _not_ the same. + // we want to capture in detail in case players run in zig-zag. + // but still grow larger buckets internally. + readonly int boundsPerBucket; + readonly Queue fullBuckets; + + Bounds? currentBucket; + int currentBucketSize; // 0..boundsPerBucket // history limit. oldest bounds will be removed. - public readonly int limit; + public readonly int boundsLimit; + readonly int bucketLimit; - // 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; + // amount of total bounds, including bounds in full buckets + current + public int boundsCount { get; private set; } // total bounds encapsulating all of the bounds history public Bounds total; - public HistoryBounds(int limit, int recalculateEveryNth) + public HistoryBounds(int boundsLimit, int boundsPerBucket) { // 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); + this.boundsPerBucket = boundsPerBucket; + this.boundsLimit = boundsLimit; + this.bucketLimit = (boundsLimit / boundsPerBucket); + + // capacity +1 because it makes the code easier if we insert first, and then remove. + fullBuckets = new Queue(bucketLimit + 1); } // insert new bounds into history. calculates new total bounds. @@ -41,38 +57,62 @@ public void Insert(Bounds bounds) { // initialize 'total' if not initialized yet. // we don't want to call (0,0).Encapsulate(bounds). - if (history.Count == 0) + if (boundsCount == 0) + { total = bounds; + } - // insert and encapsulate the new bounds - history.Enqueue(bounds); + // add to current bucket: + // either initialize new one, or encapsulate into existing one + if (currentBucket == null) + { + currentBucket = bounds; + } + else + { + currentBucket.Value.Encapsulate(bounds); + } + + // current bucket has one more bounds. + // total bounds increased as well. + currentBucketSize += 1; + boundsCount += 1; + + // always encapsulate into total immediately. + // this is free. total.Encapsulate(bounds); - // ensure history stays within limit - if (history.Count > limit) + // current bucket full? + if (currentBucketSize == boundsPerBucket) { - // remove oldest - history.Dequeue(); + // move it to full buckets + fullBuckets.Enqueue(currentBucket.Value); + currentBucket = null; + currentBucketSize = 0; - // optimization: only recalculate every n-th removal. - // accurate enough, and N times faster. - if (++recalculateCounter < recalculateEveryNth) - return; + // full bucket capacity reached? + if (fullBuckets.Count > bucketLimit) + { + // remove oldest bucket + fullBuckets.Dequeue(); + boundsCount -= boundsPerBucket; - // reset counter - recalculateCounter = 0; - - // recalculate total bounds - // (only needed after removing the oldest) - total = bounds; - foreach (Bounds b in history) - total.Encapsulate(b); + // recompute total bounds + // instead of iterating N buckets, we iterate N / boundsPerBucket buckets. + // TODO technically we could reuse 'currentBucket' before clearing instead of encapsulating again + total = bounds; + foreach (Bounds bucket in fullBuckets) + total.Encapsulate(bucket); + } } } public void Reset() { - history.Clear(); + fullBuckets.Clear(); + currentBucket = null; + currentBucketSize = 0; + boundsCount = 0; total = new Bounds(); } } diff --git a/Assets/Mirror/Tests/Editor/LagCompensation/HistoryBoundsTests.cs b/Assets/Mirror/Tests/Editor/LagCompensation/HistoryBoundsTests.cs index ae52839df..38c41cf7f 100644 --- a/Assets/Mirror/Tests/Editor/LagCompensation/HistoryBoundsTests.cs +++ b/Assets/Mirror/Tests/Editor/LagCompensation/HistoryBoundsTests.cs @@ -1,5 +1,7 @@ +using System; using NUnit.Framework; using UnityEngine; +using Random = UnityEngine.Random; namespace Mirror.Tests.LagCompensationTests { @@ -25,16 +27,18 @@ public static Bounds MinMax(float min, float max) => // Unity 2021.3 LTS, release mode: 100k; limit=8 // O(N) Queue implementation: 183 ms // O(N) Queue and recalculate every 2nd: 108 ms + // Buckets of 2: 79 ms + // Buckets of 4: 54 ms [Test] [TestCase(100_000, 8, 1)] [TestCase(100_000, 8, 2)] [TestCase(100_000, 8, 4)] - public void Benchmark(int iterations, int limit, int recalculate) + public void Benchmark(int iterations, int limit, int boundsPerBucket) { // always use the same seed so we get the same test. Random.InitState(0); - HistoryBounds history = new HistoryBounds(limit, recalculate); + HistoryBounds history = new HistoryBounds(limit, boundsPerBucket); for (int i = 0; i < iterations; ++i) { float min = Random.Range(-1, 1); @@ -53,32 +57,32 @@ public void Benchmark(int iterations, int limit, int recalculate) public void Insert_Basic() { const int limit = 3; - HistoryBounds history = new HistoryBounds(limit, recalculateEveryNth: 1); + HistoryBounds history = new HistoryBounds(limit, boundsPerBucket: 1); // insert initial [-1, 1]. // should calculate new bounds == initial. history.Insert(MinMax(-1, 1)); - Assert.That(history.Count, Is.EqualTo(1)); + Assert.That(history.boundsCount, 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.boundsCount, 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, 0)); - Assert.That(history.Count, Is.EqualTo(3)); + Assert.That(history.boundsCount, 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. // new bounds should be [-0.5, 2] history.Insert(MinMax(0, 0)); - Assert.That(history.Count, Is.EqualTo(3)); + Assert.That(history.boundsCount, Is.EqualTo(3)); Assert.That(history.total, Is.EqualTo(MinMax(-0.5f, 2))); } @@ -88,30 +92,30 @@ public void Insert_Basic() public void Insert_Revisit() { const int limit = 3; - HistoryBounds history = new HistoryBounds(limit, recalculateEveryNth: 1); + HistoryBounds history = new HistoryBounds(limit, boundsPerBucket: 1); // insert initial [-1, 1]. // should calculate new bounds == initial. history.Insert(MinMax(-1, 1)); - Assert.That(history.Count, Is.EqualTo(1)); + Assert.That(history.boundsCount, 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.boundsCount, Is.EqualTo(2)); Assert.That(history.total, Is.EqualTo(MinMax(-1, 2))); // visit [-1, 1] again history.Insert(MinMax(-1, 1)); - Assert.That(history.Count, Is.EqualTo(3)); + Assert.That(history.boundsCount, Is.EqualTo(3)); Assert.That(history.total, Is.EqualTo(MinMax(-1, 2))); // insert beyond limit. // oldest one [-1, 1] should be removed. // total should still include it because we revisited [1, 1]. history.Insert(MinMax(0, 0)); - Assert.That(history.Count, Is.EqualTo(3)); + Assert.That(history.boundsCount, Is.EqualTo(3)); Assert.That(history.total, Is.EqualTo(MinMax(-1, 2))); } @@ -121,40 +125,43 @@ public void Insert_Revisit() public void Insert_Far() { const int limit = 3; - HistoryBounds history = new HistoryBounds(limit, recalculateEveryNth: 1); + HistoryBounds history = new HistoryBounds(limit, boundsPerBucket: 1); // insert initial [2, 3]. // should calculate new bounds == initial. history.Insert(MinMax(2, 3)); - Assert.That(history.Count, Is.EqualTo(1)); + Assert.That(history.boundsCount, Is.EqualTo(1)); Assert.That(history.total, Is.EqualTo(MinMax(2, 3))); // insert [3, 4] // should calculate new bounds == [2, 4]. history.Insert(MinMax(3, 4)); - Assert.That(history.Count, Is.EqualTo(2)); + Assert.That(history.boundsCount, Is.EqualTo(2)); Assert.That(history.total, Is.EqualTo(MinMax(2, 4))); // insert one that's smaller than current bounds [0.5, 1] // 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.boundsCount, Is.EqualTo(3)); Assert.That(history.total, Is.EqualTo(MinMax(0.5f, 4))); // insert more than 'limit' // the oldest one [-1, 1] should be discarded. // new bounds should be [-0.5, 2] history.Insert(MinMax(2, 2)); - Assert.That(history.Count, Is.EqualTo(3)); + Assert.That(history.boundsCount, Is.EqualTo(3)); Assert.That(history.total, Is.EqualTo(MinMax(0.5f, 4))); } - // test to check if recalculate works as expected + + // test to check if bounds per bucket works as expected [Test] - public void Insert_Recalculate() + public void Insert_MultipleBoundsPerBUcket() { + throw new NotImplementedException(); + /* const int limit = 3; - HistoryBounds history = new HistoryBounds(limit, recalculateEveryNth: 2); + HistoryBounds history = new HistoryBounds(limit, boundsPerBucket: 2); // insert initial [-1, 1]. // should calculate new bounds == initial. @@ -207,20 +214,21 @@ public void Insert_Recalculate() 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, recalculateEveryNth: 1); + HistoryBounds history = new HistoryBounds(limit, boundsPerBucket: 1); history.Insert(MinMax(1, 2)); history.Insert(MinMax(2, 3)); history.Insert(MinMax(3, 4)); history.Reset(); - Assert.That(history.Count, Is.EqualTo(0)); + Assert.That(history.boundsCount, Is.EqualTo(0)); Assert.That(history.total, Is.EqualTo(MinMax(0, 0))); } }