mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
buckets
This commit is contained in:
parent
0dd504de11
commit
f4650eff64
@ -10,29 +10,45 @@ namespace Mirror
|
|||||||
{
|
{
|
||||||
public class HistoryBounds
|
public class HistoryBounds
|
||||||
{
|
{
|
||||||
// history of bounds
|
// FakeByte: gather bounds in smaller buckets.
|
||||||
readonly Queue<Bounds> history;
|
// for example, bucket(t0,t1,t2), bucket(t3,t4,t5), ...
|
||||||
public int Count => history.Count;
|
// 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<Bounds> fullBuckets;
|
||||||
|
|
||||||
|
Bounds? currentBucket;
|
||||||
|
int currentBucketSize; // 0..boundsPerBucket
|
||||||
|
|
||||||
// history limit. oldest bounds will be removed.
|
// 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.
|
// amount of total bounds, including bounds in full buckets + current
|
||||||
// new entries are still encapsulated on every insertion.
|
public int boundsCount { get; private set; }
|
||||||
// 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
|
// total bounds encapsulating all of the bounds history
|
||||||
public Bounds total;
|
public Bounds total;
|
||||||
|
|
||||||
public HistoryBounds(int limit, int recalculateEveryNth)
|
public HistoryBounds(int boundsLimit, int boundsPerBucket)
|
||||||
{
|
{
|
||||||
// initialize queue with maximum capacity to avoid runtime resizing
|
// initialize queue with maximum capacity to avoid runtime resizing
|
||||||
// +1 because it makes the code easier if we insert first, and then remove.
|
this.boundsPerBucket = boundsPerBucket;
|
||||||
this.limit = limit;
|
this.boundsLimit = boundsLimit;
|
||||||
this.recalculateEveryNth = recalculateEveryNth;
|
this.bucketLimit = (boundsLimit / boundsPerBucket);
|
||||||
history = new Queue<Bounds>(limit + 1);
|
|
||||||
|
// capacity +1 because it makes the code easier if we insert first, and then remove.
|
||||||
|
fullBuckets = new Queue<Bounds>(bucketLimit + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert new bounds into history. calculates new total bounds.
|
// insert new bounds into history. calculates new total bounds.
|
||||||
@ -41,38 +57,62 @@ public void Insert(Bounds bounds)
|
|||||||
{
|
{
|
||||||
// initialize 'total' if not initialized yet.
|
// initialize 'total' if not initialized yet.
|
||||||
// we don't want to call (0,0).Encapsulate(bounds).
|
// we don't want to call (0,0).Encapsulate(bounds).
|
||||||
if (history.Count == 0)
|
if (boundsCount == 0)
|
||||||
|
{
|
||||||
total = bounds;
|
total = bounds;
|
||||||
|
}
|
||||||
|
|
||||||
// insert and encapsulate the new bounds
|
// add to current bucket:
|
||||||
history.Enqueue(bounds);
|
// 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);
|
total.Encapsulate(bounds);
|
||||||
|
|
||||||
// ensure history stays within limit
|
// current bucket full?
|
||||||
if (history.Count > limit)
|
if (currentBucketSize == boundsPerBucket)
|
||||||
{
|
{
|
||||||
// remove oldest
|
// move it to full buckets
|
||||||
history.Dequeue();
|
fullBuckets.Enqueue(currentBucket.Value);
|
||||||
|
currentBucket = null;
|
||||||
|
currentBucketSize = 0;
|
||||||
|
|
||||||
// optimization: only recalculate every n-th removal.
|
// full bucket capacity reached?
|
||||||
// accurate enough, and N times faster.
|
if (fullBuckets.Count > bucketLimit)
|
||||||
if (++recalculateCounter < recalculateEveryNth)
|
{
|
||||||
return;
|
// remove oldest bucket
|
||||||
|
fullBuckets.Dequeue();
|
||||||
|
boundsCount -= boundsPerBucket;
|
||||||
|
|
||||||
// reset counter
|
// recompute total bounds
|
||||||
recalculateCounter = 0;
|
// instead of iterating N buckets, we iterate N / boundsPerBucket buckets.
|
||||||
|
// TODO technically we could reuse 'currentBucket' before clearing instead of encapsulating again
|
||||||
// recalculate total bounds
|
|
||||||
// (only needed after removing the oldest)
|
|
||||||
total = bounds;
|
total = bounds;
|
||||||
foreach (Bounds b in history)
|
foreach (Bounds bucket in fullBuckets)
|
||||||
total.Encapsulate(b);
|
total.Encapsulate(bucket);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
history.Clear();
|
fullBuckets.Clear();
|
||||||
|
currentBucket = null;
|
||||||
|
currentBucketSize = 0;
|
||||||
|
boundsCount = 0;
|
||||||
total = new Bounds();
|
total = new Bounds();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
using System;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using Random = UnityEngine.Random;
|
||||||
|
|
||||||
namespace Mirror.Tests.LagCompensationTests
|
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
|
// Unity 2021.3 LTS, release mode: 100k; limit=8
|
||||||
// O(N) Queue<Bounds> implementation: 183 ms
|
// O(N) Queue<Bounds> implementation: 183 ms
|
||||||
// O(N) Queue and recalculate every 2nd: 108 ms
|
// O(N) Queue and recalculate every 2nd: 108 ms
|
||||||
|
// Buckets of 2: 79 ms
|
||||||
|
// Buckets of 4: 54 ms
|
||||||
[Test]
|
[Test]
|
||||||
[TestCase(100_000, 8, 1)]
|
[TestCase(100_000, 8, 1)]
|
||||||
[TestCase(100_000, 8, 2)]
|
[TestCase(100_000, 8, 2)]
|
||||||
[TestCase(100_000, 8, 4)]
|
[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.
|
// always use the same seed so we get the same test.
|
||||||
Random.InitState(0);
|
Random.InitState(0);
|
||||||
|
|
||||||
HistoryBounds history = new HistoryBounds(limit, recalculate);
|
HistoryBounds history = new HistoryBounds(limit, boundsPerBucket);
|
||||||
for (int i = 0; i < iterations; ++i)
|
for (int i = 0; i < iterations; ++i)
|
||||||
{
|
{
|
||||||
float min = Random.Range(-1, 1);
|
float min = Random.Range(-1, 1);
|
||||||
@ -53,32 +57,32 @@ public void Benchmark(int iterations, int limit, int recalculate)
|
|||||||
public void Insert_Basic()
|
public void Insert_Basic()
|
||||||
{
|
{
|
||||||
const int limit = 3;
|
const int limit = 3;
|
||||||
HistoryBounds history = new HistoryBounds(limit, recalculateEveryNth: 1);
|
HistoryBounds history = new HistoryBounds(limit, boundsPerBucket: 1);
|
||||||
|
|
||||||
// insert initial [-1, 1].
|
// insert initial [-1, 1].
|
||||||
// should calculate new bounds == initial.
|
// should calculate new bounds == initial.
|
||||||
history.Insert(MinMax(-1, 1));
|
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)));
|
Assert.That(history.total, Is.EqualTo(MinMax(-1, 1)));
|
||||||
|
|
||||||
// insert [0, 2]
|
// insert [0, 2]
|
||||||
// should calculate new bounds == [-1, 2].
|
// should calculate new bounds == [-1, 2].
|
||||||
history.Insert(MinMax(0, 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)));
|
Assert.That(history.total, Is.EqualTo(MinMax(-1, 2)));
|
||||||
|
|
||||||
// insert one that's smaller than current bounds [-.5, 0]
|
// insert one that's smaller than current bounds [-.5, 0]
|
||||||
// history needs to contain it even if smaller, because once the oldest
|
// history needs to contain it even if smaller, because once the oldest
|
||||||
// largest one gets removed, this one matters too.
|
// largest one gets removed, this one matters too.
|
||||||
history.Insert(MinMax(-0.5f, 0));
|
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)));
|
Assert.That(history.total, Is.EqualTo(MinMax(-1, 2)));
|
||||||
|
|
||||||
// insert more than 'limit': [0, 0]
|
// insert more than 'limit': [0, 0]
|
||||||
// the oldest one [-1, 1] should be discarded.
|
// the oldest one [-1, 1] should be discarded.
|
||||||
// new bounds should be [-0.5, 2]
|
// new bounds should be [-0.5, 2]
|
||||||
history.Insert(MinMax(0, 0));
|
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)));
|
Assert.That(history.total, Is.EqualTo(MinMax(-0.5f, 2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,30 +92,30 @@ public void Insert_Basic()
|
|||||||
public void Insert_Revisit()
|
public void Insert_Revisit()
|
||||||
{
|
{
|
||||||
const int limit = 3;
|
const int limit = 3;
|
||||||
HistoryBounds history = new HistoryBounds(limit, recalculateEveryNth: 1);
|
HistoryBounds history = new HistoryBounds(limit, boundsPerBucket: 1);
|
||||||
|
|
||||||
// insert initial [-1, 1].
|
// insert initial [-1, 1].
|
||||||
// should calculate new bounds == initial.
|
// should calculate new bounds == initial.
|
||||||
history.Insert(MinMax(-1, 1));
|
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)));
|
Assert.That(history.total, Is.EqualTo(MinMax(-1, 1)));
|
||||||
|
|
||||||
// insert [0, 2]
|
// insert [0, 2]
|
||||||
// should calculate new bounds == [-1, 2].
|
// should calculate new bounds == [-1, 2].
|
||||||
history.Insert(MinMax(0, 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)));
|
Assert.That(history.total, Is.EqualTo(MinMax(-1, 2)));
|
||||||
|
|
||||||
// visit [-1, 1] again
|
// visit [-1, 1] again
|
||||||
history.Insert(MinMax(-1, 1));
|
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)));
|
Assert.That(history.total, Is.EqualTo(MinMax(-1, 2)));
|
||||||
|
|
||||||
// insert beyond limit.
|
// insert beyond limit.
|
||||||
// oldest one [-1, 1] should be removed.
|
// oldest one [-1, 1] should be removed.
|
||||||
// total should still include it because we revisited [1, 1].
|
// total should still include it because we revisited [1, 1].
|
||||||
history.Insert(MinMax(0, 0));
|
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)));
|
Assert.That(history.total, Is.EqualTo(MinMax(-1, 2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,40 +125,43 @@ public void Insert_Revisit()
|
|||||||
public void Insert_Far()
|
public void Insert_Far()
|
||||||
{
|
{
|
||||||
const int limit = 3;
|
const int limit = 3;
|
||||||
HistoryBounds history = new HistoryBounds(limit, recalculateEveryNth: 1);
|
HistoryBounds history = new HistoryBounds(limit, boundsPerBucket: 1);
|
||||||
|
|
||||||
// insert initial [2, 3].
|
// insert initial [2, 3].
|
||||||
// should calculate new bounds == initial.
|
// should calculate new bounds == initial.
|
||||||
history.Insert(MinMax(2, 3));
|
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)));
|
Assert.That(history.total, Is.EqualTo(MinMax(2, 3)));
|
||||||
|
|
||||||
// insert [3, 4]
|
// insert [3, 4]
|
||||||
// should calculate new bounds == [2, 4].
|
// should calculate new bounds == [2, 4].
|
||||||
history.Insert(MinMax(3, 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)));
|
Assert.That(history.total, Is.EqualTo(MinMax(2, 4)));
|
||||||
|
|
||||||
// insert one that's smaller than current bounds [0.5, 1]
|
// insert one that's smaller than current bounds [0.5, 1]
|
||||||
// history needs to contain it even if smaller, because once the oldest
|
// history needs to contain it even if smaller, because once the oldest
|
||||||
// largest one gets removed, this one matters too.
|
// largest one gets removed, this one matters too.
|
||||||
history.Insert(MinMax(0.5f, 1));
|
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)));
|
Assert.That(history.total, Is.EqualTo(MinMax(0.5f, 4)));
|
||||||
|
|
||||||
// insert more than 'limit'
|
// insert more than 'limit'
|
||||||
// the oldest one [-1, 1] should be discarded.
|
// the oldest one [-1, 1] should be discarded.
|
||||||
// new bounds should be [-0.5, 2]
|
// new bounds should be [-0.5, 2]
|
||||||
history.Insert(MinMax(2, 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)));
|
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]
|
[Test]
|
||||||
public void Insert_Recalculate()
|
public void Insert_MultipleBoundsPerBUcket()
|
||||||
{
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
/*
|
||||||
const int limit = 3;
|
const int limit = 3;
|
||||||
HistoryBounds history = new HistoryBounds(limit, recalculateEveryNth: 2);
|
HistoryBounds history = new HistoryBounds(limit, boundsPerBucket: 2);
|
||||||
|
|
||||||
// insert initial [-1, 1].
|
// insert initial [-1, 1].
|
||||||
// should calculate new bounds == initial.
|
// should calculate new bounds == initial.
|
||||||
@ -207,20 +214,21 @@ public void Insert_Recalculate()
|
|||||||
history.Insert(MinMax(0, 0));
|
history.Insert(MinMax(0, 0));
|
||||||
Assert.That(history.Count, Is.EqualTo(3));
|
Assert.That(history.Count, Is.EqualTo(3));
|
||||||
Assert.That(history.total, Is.EqualTo(MinMax(0, 0)));
|
Assert.That(history.total, Is.EqualTo(MinMax(0, 0)));
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
const int limit = 3;
|
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(1, 2));
|
||||||
history.Insert(MinMax(2, 3));
|
history.Insert(MinMax(2, 3));
|
||||||
history.Insert(MinMax(3, 4));
|
history.Insert(MinMax(3, 4));
|
||||||
|
|
||||||
history.Reset();
|
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)));
|
Assert.That(history.total, Is.EqualTo(MinMax(0, 0)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user