From a3e6dd3cb534cb1238acdbcec0b3a753efa67478 Mon Sep 17 00:00:00 2001 From: mischa Date: Mon, 18 Mar 2024 15:59:36 +0800 Subject: [PATCH] perf: Prediction.CorrectHistory removed O(N) insertion. adjusting successive values is enough. --- Assets/Mirror/Core/Prediction/Prediction.cs | 13 +++-- .../Examples/BenchmarkPrediction/Readme.md | 1 + .../Editor/Prediction/PredictionTests.cs | 55 ++++++++++--------- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/Assets/Mirror/Core/Prediction/Prediction.cs b/Assets/Mirror/Core/Prediction/Prediction.cs index bbfc6eb4d..d66994527 100644 --- a/Assets/Mirror/Core/Prediction/Prediction.cs +++ b/Assets/Mirror/Core/Prediction/Prediction.cs @@ -117,10 +117,15 @@ public static T CorrectHistory( afterIndex -= 1; // we removed the first value so all indices are off by one now } - // insert the corrected state into the history, or overwrite if already exists - // SortedList insertions are O(N)! - history[corrected.timestamp] = corrected; - afterIndex += 1; // we inserted the corrected value before the previous index + // PERFORMANCE OPTIMIZATION: avoid O(N) insertion, only readjust all values after. + // the end result is the same since after.delta and after.position are both recalculated. + // it's technically not correct if we were to reconstruct final position from 0..after..end but + // we never do, we only ever iterate from after..end! + // + // insert the corrected state into the history, or overwrite if already exists + // SortedList insertions are O(N)! + // history[corrected.timestamp] = corrected; + // afterIndex += 1; // we inserted the corrected value before the previous index // the entry behind the inserted one still has the delta from (before, after). // we need to correct it to (corrected, after). diff --git a/Assets/Mirror/Examples/BenchmarkPrediction/Readme.md b/Assets/Mirror/Examples/BenchmarkPrediction/Readme.md index cd0ed609a..247905098 100644 --- a/Assets/Mirror/Examples/BenchmarkPrediction/Readme.md +++ b/Assets/Mirror/Examples/BenchmarkPrediction/Readme.md @@ -20,3 +20,4 @@ Predicted: 2024-03-14: 590 FPS Client, 1700 FPS Server // UpdateGhosting() every 4th frame 2024-03-14: 615 FPS Client, 1700 FPS Server // predictedRigidbodyTransform.GetPositionAndRotation() 2024-03-15: 625 FPS Client, 1700 FPS Server // Vector3.MoveTowardsCustom() + 2024-03-18: 628 FPS Client, 1700 FPS Server // removed O(N) insertion from CorrectHistory() diff --git a/Assets/Mirror/Tests/Editor/Prediction/PredictionTests.cs b/Assets/Mirror/Tests/Editor/Prediction/PredictionTests.cs index 7bea01487..7d75ae7c6 100644 --- a/Assets/Mirror/Tests/Editor/Prediction/PredictionTests.cs +++ b/Assets/Mirror/Tests/Editor/Prediction/PredictionTests.cs @@ -160,8 +160,10 @@ public void CorrectHistory() const int historyLimit = 32; Prediction.CorrectHistory(history, historyLimit, correction, before, after, afterIndex); - // there should be 4 initial + 1 corrected = 5 entries now - Assert.That(history.Count, Is.EqualTo(5)); + // PERFORMANCE OPTIMIZATION: nothing is inserted anymore, values are only adjusted. + // there should be 4 initial + 1 corrected = 5 entries now + // Assert.That(history.Count, Is.EqualTo(5)); + Assert.That(history.Count, Is.EqualTo(4)); // first entry at t=0 should be unchanged, since we corrected after that one. Assert.That(history.Keys[0], Is.EqualTo(0)); @@ -181,16 +183,17 @@ public void CorrectHistory() Assert.That(history.Values[1].angularVelocity.x, Is.EqualTo(1)); Assert.That(history.Values[1].angularVelocityDelta.x, Is.EqualTo(1)); - // third entry at t=1.5 should be the received state. - // absolute values should be the correction, without any deltas since - // server doesn't send those and we don't need them. - Assert.That(history.Keys[2], Is.EqualTo(1.5)); - Assert.That(history.Values[2].position.x, Is.EqualTo(1.6f).Within(0.001f)); - Assert.That(history.Values[2].positionDelta.x, Is.EqualTo(0)); - Assert.That(history.Values[2].velocity.x, Is.EqualTo(1.6f).Within(0.001f)); - Assert.That(history.Values[2].velocityDelta.x, Is.EqualTo(0)); - Assert.That(history.Values[2].angularVelocity.x, Is.EqualTo(1.6f).Within(0.001f)); - Assert.That(history.Values[2].angularVelocityDelta.x, Is.EqualTo(0)); + // PERFORMANCE OPTIMIZATION: nothing is inserted anymore, values are only adjusted. + // third entry at t=1.5 should be the received state. + // absolute values should be the correction, without any deltas since + // server doesn't send those and we don't need them. + // Assert.That(history.Keys[2], Is.EqualTo(1.5)); + // Assert.That(history.Values[2].position.x, Is.EqualTo(1.6f).Within(0.001f)); + // Assert.That(history.Values[2].positionDelta.x, Is.EqualTo(0)); + // Assert.That(history.Values[2].velocity.x, Is.EqualTo(1.6f).Within(0.001f)); + // Assert.That(history.Values[2].velocityDelta.x, Is.EqualTo(0)); + // Assert.That(history.Values[2].angularVelocity.x, Is.EqualTo(1.6f).Within(0.001f)); + // Assert.That(history.Values[2].angularVelocityDelta.x, Is.EqualTo(0)); // fourth entry at t=2: // delta was from t=1.0 @ 1 to t=2.0 @ 2 = 1.0 @@ -198,25 +201,25 @@ public void CorrectHistory() // the delta at t=1.5 would've been 0.5. // => the inserted position is at t=1.6 // => add the relative delta of 0.5 = 2.1 - Assert.That(history.Keys[3], Is.EqualTo(2.0)); - Assert.That(history.Values[3].position.x, Is.EqualTo(2.1).Within(0.001f)); - Assert.That(history.Values[3].positionDelta.x, Is.EqualTo(0.5).Within(0.001f)); - Assert.That(history.Values[3].velocity.x, Is.EqualTo(2.1).Within(0.001f)); - Assert.That(history.Values[3].velocityDelta.x, Is.EqualTo(0.5).Within(0.001f)); - Assert.That(history.Values[3].angularVelocity.x, Is.EqualTo(2.1).Within(0.001f)); - Assert.That(history.Values[3].angularVelocityDelta.x, Is.EqualTo(0.5)); + Assert.That(history.Keys[2], Is.EqualTo(2.0)); + Assert.That(history.Values[2].position.x, Is.EqualTo(2.1).Within(0.001f)); + Assert.That(history.Values[2].positionDelta.x, Is.EqualTo(0.5).Within(0.001f)); + Assert.That(history.Values[2].velocity.x, Is.EqualTo(2.1).Within(0.001f)); + Assert.That(history.Values[2].velocityDelta.x, Is.EqualTo(0.5).Within(0.001f)); + Assert.That(history.Values[2].angularVelocity.x, Is.EqualTo(2.1).Within(0.001f)); + Assert.That(history.Values[2].angularVelocityDelta.x, Is.EqualTo(0.5)); // fifth entry at t=3: // client moved by a delta of 1 here, and that remains unchanged. // absolute position was 3.0 but if we apply the delta of 1 to the one before at 2.1, // we get the new position of 3.1 - Assert.That(history.Keys[4], Is.EqualTo(3.0)); - Assert.That(history.Values[4].position.x, Is.EqualTo(3.1).Within(0.001f)); - Assert.That(history.Values[4].positionDelta.x, Is.EqualTo(1.0).Within(0.001f)); - Assert.That(history.Values[4].velocity.x, Is.EqualTo(3.1).Within(0.001f)); - Assert.That(history.Values[4].velocityDelta.x, Is.EqualTo(1.0).Within(0.001f)); - Assert.That(history.Values[4].angularVelocity.x, Is.EqualTo(3.1).Within(0.001f)); - Assert.That(history.Values[4].angularVelocityDelta.x, Is.EqualTo(1.0).Within(0.001f)); + Assert.That(history.Keys[3], Is.EqualTo(3.0)); + Assert.That(history.Values[3].position.x, Is.EqualTo(3.1).Within(0.001f)); + Assert.That(history.Values[3].positionDelta.x, Is.EqualTo(1.0).Within(0.001f)); + Assert.That(history.Values[3].velocity.x, Is.EqualTo(3.1).Within(0.001f)); + Assert.That(history.Values[3].velocityDelta.x, Is.EqualTo(1.0).Within(0.001f)); + Assert.That(history.Values[3].angularVelocity.x, Is.EqualTo(3.1).Within(0.001f)); + Assert.That(history.Values[3].angularVelocityDelta.x, Is.EqualTo(1.0).Within(0.001f)); } } }