Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion Assets/PredictionTests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,29 @@ Optional args: `-port`, `-serverHost`, `-connectTimeout`. Exit code is non-zero
Policy regression scenarios are included in the normal suite. Pass `-policyRegressionScenariosOnly`
to run just the bootstrap and the three focused policy scenarios.

## Interest management scenario

Pass `-interestScenariosOnly` to run the bootstrap plus `InterestManagementScenario`. This focused
scenario requires a pure server and exactly two clients. It creates one player-owned
`PredictedTransform` anchor per client, two distant deterministic roots, and reciprocal owned
keyed-input movers. It verifies culling, both sides of the LOD hysteresis band, dormant
simulation/view behavior, hierarchy retention, deterministic and input-driven absolute-state
convergence on reentry, and a per-recipient packed-frame byte reduction while culled.

```
PurrDictionTests -batchmode -nographics -role server -count 2 -interestScenariosOnly -results server.json -logFile server.log
PurrDictionTests -batchmode -nographics -role client -count 2 -interestScenariosOnly -results client-1.json -logFile client-1.log
PurrDictionTests -batchmode -nographics -role client -count 2 -interestScenariosOnly -results client-2.json -logFile client-2.log
```

## Server load benchmark

Pass `-serverLoadBenchmark` to run only the bootstrap plus `ServerLoadBenchmarkScenario`: a
`BenchDriver` spawns `-benchObjects` (default 200) input-driven `BenchMover` identities, then the
server samples the `WriteFrameOnServer` sub-markers (`WriteInputHistory`, `WriteStateDeltas`,
`WriteFullFrame`, `WriteEventHandles`, `SendFrame`) plus client ack lag for `-benchSeconds`
(default 20) and reports them in the scenario result message. Use
(default 20), records frame payload bytes per recipient through `TickBandwidthProfiler`, and
reports the normalized `bytesPerClientTick` in the scenario result message. Use
`Tools/PurrDiction/Analysis/Run Server Load Latency Sweep` (or
`-executeMethod PurrNet.Prediction.Benchmarks.Editor.ServerLoadBenchmarkRunner.RunFromCommandLine`)
to build the player and sweep several simulated latencies (`-slbLatencies "0,50,100,200"`,
Expand Down
11 changes: 11 additions & 0 deletions Assets/PredictionTests/Scripts/PredictionBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ private void Awake()
return;
}

if (CommandLineUtils.HasFlag("-interestScenariosOnly"))
{
_scenarios = new Scenario[]
{
this,
gameObject.AddComponent<InterestManagementScenario>()
};
_results = new ScenarioDetails?[_scenarios.Length];
return;
}

if (CommandLineUtils.HasFlag("-includeHistoryStressScenario"))
gameObject.AddComponent<HistoryStressScenario>();

Expand Down
1,901 changes: 1,901 additions & 0 deletions Assets/PredictionTests/Scripts/Scenarios/InterestManagementScenario.cs

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Cysharp.Threading.Tasks;
using PurrNet;
using PurrNet.Prediction;
using PurrNet.Prediction.Profiler;
using UnityEngine;

public class ServerLoadBenchmarkScenario : Scenario
Expand Down Expand Up @@ -119,6 +120,7 @@ await UniTaskUtils.WaitWithTimeout(
await UniTask.WaitForSeconds(_settleSeconds, cancellationToken: ctx.cancellationToken);

var sampler = ScenarioPerformanceSampler.StartDefault();
using var bandwidth = new BandwidthSampler();
var startTick = pm.localTick;
double lagSum = 0;
ulong lagMax = 0;
Expand All @@ -141,7 +143,7 @@ await UniTaskUtils.WaitWithTimeout(

var elapsedTicks = pm.localTick - startTick;
var perf = sampler.Stop(pm);
return ScenarioResult.Ok(BuildReport(perf, elapsedTicks, lagSum, lagMax, lagSamples));
return ScenarioResult.Ok(BuildReport(perf, bandwidth, elapsedTicks, lagSum, lagMax, lagSamples));
}
finally
{
Expand All @@ -151,6 +153,7 @@ await UniTaskUtils.WaitWithTimeout(

private string BuildReport(
ScenarioPerformanceDetails perf,
BandwidthSampler bandwidth,
ulong elapsedTicks,
double lagSum,
ulong lagMax,
Expand All @@ -164,6 +167,12 @@ private string BuildReport(
sb.Append(" ticks=").Append(elapsedTicks);
sb.Append(" ackLagAvg=").Append((lagSamples > 0 ? lagSum / lagSamples : 0).ToString("0.##", CultureInfo.InvariantCulture));
sb.Append(" ackLagMax=").Append(lagMax);
sb.Append(" bandwidthTicks=").Append(bandwidth.tickCount);
sb.Append(" bandwidthClients=").Append(bandwidth.clientCount);
sb.Append(" bandwidthFrames=").Append(bandwidth.frameCount);
sb.Append(" bandwidthBytes=").Append(bandwidth.byteCount);
sb.Append(" bytesPerClientTick=").Append(
bandwidth.bytesPerClientTick.ToString("0.##", CultureInfo.InvariantCulture));

if (perf.markers != null)
{
Expand All @@ -188,4 +197,45 @@ private string BuildReport(

return sb.ToString();
}

private sealed class BandwidthSampler : IDisposable
{
private readonly System.Collections.Generic.HashSet<PlayerID> _players = new();
private long _bitCount;

public int tickCount { get; private set; }
public int frameCount { get; private set; }
public int clientCount => _players.Count;
public long byteCount => _bitCount / 8;
public double bytesPerClientTick => tickCount > 0 && clientCount > 0
? _bitCount / 8.0 / tickCount / clientCount
: 0;

public BandwidthSampler()
{
TickBandwidthProfiler.onTickEnded += OnTickEnded;
}

public void Dispose()
{
TickBandwidthProfiler.onTickEnded -= OnTickEnded;
}

private void OnTickEnded()
{
var frames = TickBandwidthProfiler.wroteFrames;
if (frames.Count == 0)
return;

tickCount++;

for (var i = 0; i < frames.Count; i++)
{
var frame = frames[i];
_players.Add(frame.player);
_bitCount += frame.bitCount;
frameCount++;
}
}
}
}
2 changes: 1 addition & 1 deletion Assets/PurrDiction/Documentation/PurrDocs.url
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Prop3=19,11
[InternetShortcut]
IDList=
URL=https://purrnet.gitbook.io/docs/tools/client-side-prediction
URL=https://purrnet.dev/docs/client-side-prediction
41 changes: 29 additions & 12 deletions Assets/PurrDiction/Editor/PredictedPrefabsEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,37 +38,50 @@ private void SetupReorderableList()
_reorderableList.drawHeaderCallback = (Rect rect) =>
{
float fullWidth = rect.width - REORDERABLE_LIST_BUTTON_WIDTH;
CalculateWidths(fullWidth, out float prefabWidth, out float poolWidth, out float warmupWidth);
CalculateWidths(fullWidth, out float prefabWidth, out float minimumTierWidth,
out float poolWidth, out float warmupWidth);

EditorGUI.LabelField(new Rect(rect.x, rect.y, prefabWidth, rect.height), "Prefab");
EditorGUI.LabelField(
new Rect(rect.x + prefabWidth + SPACING, rect.y, poolWidth + warmupWidth, rect.height), "Pool");
float x = rect.x + prefabWidth + SPACING;
EditorGUI.LabelField(new Rect(x, rect.y, minimumTierWidth, rect.height),
new GUIContent("Min Tier", "Distance-based tier floor. 0 allows full detail; 255 starts culled."));
x += minimumTierWidth + SPACING;
EditorGUI.LabelField(new Rect(x, rect.y, poolWidth, rect.height), "Pool");
x += poolWidth + SPACING;
EditorGUI.LabelField(new Rect(x, rect.y, warmupWidth, rect.height), "Warmup");
};

_reorderableList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
{
SerializedProperty element = _prefabs.GetArrayElementAtIndex(index);
SerializedProperty prefabProp = element.FindPropertyRelative("prefab");
SerializedProperty minimumTierProp = element.FindPropertyRelative("minimumInterestTier");
SerializedProperty poolProp = element.FindPropertyRelative("pooled");
SerializedProperty warmupCountProp = element.FindPropertyRelative("warmupCount");

float fullWidth = rect.width - REORDERABLE_LIST_BUTTON_WIDTH;
CalculateWidths(fullWidth, out float prefabWidth, out float poolWidth, out float warmupWidth);
CalculateWidths(fullWidth, out float prefabWidth, out float minimumTierWidth,
out float poolWidth, out float warmupWidth);

EditorGUI.BeginDisabledGroup(_target.autoGenerate);
EditorGUI.PropertyField(new Rect(rect.x, rect.y, prefabWidth, rect.height), prefabProp,
GUIContent.none);
EditorGUI.EndDisabledGroup();

float x = rect.x + prefabWidth + SPACING;
EditorGUI.PropertyField(new Rect(x, rect.y, minimumTierWidth, rect.height),
minimumTierProp, GUIContent.none);
x += minimumTierWidth + SPACING;

poolProp.boolValue =
EditorGUI.Toggle(new Rect(rect.x + prefabWidth + SPACING, rect.y, poolWidth, rect.height),
poolProp.boolValue);
EditorGUI.Toggle(new Rect(x, rect.y, poolWidth, rect.height), poolProp.boolValue);

x += poolWidth + SPACING;

if (poolProp.boolValue)
{
EditorGUI.PropertyField(
new Rect(rect.x + prefabWidth + poolWidth + (SPACING * 2), rect.y, warmupWidth, rect.height),
warmupCountProp, GUIContent.none);
EditorGUI.PropertyField(new Rect(x, rect.y, warmupWidth, rect.height), warmupCountProp,
GUIContent.none);
}
};

Expand All @@ -83,6 +96,7 @@ private void SetupReorderableList()
element.FindPropertyRelative("prefab").objectReferenceValue = null;
element.FindPropertyRelative("pooled").boolValue = _target.poolByDefault;
element.FindPropertyRelative("warmupCount").intValue = 5;
element.FindPropertyRelative("minimumInterestTier").intValue = 0;
element.FindPropertyRelative("guid").stringValue = string.Empty;
serializedObject.ApplyModifiedProperties();
});
Expand All @@ -101,6 +115,7 @@ private void SetupReorderableList()
element.FindPropertyRelative("prefab").objectReferenceValue = obj;
element.FindPropertyRelative("pooled").boolValue = _target.poolByDefault;
element.FindPropertyRelative("warmupCount").intValue = 5;
element.FindPropertyRelative("minimumInterestTier").intValue = 0;
element.FindPropertyRelative("guid").stringValue =
AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(obj));
}
Expand All @@ -116,11 +131,13 @@ private void SetupReorderableList()
};
}

private void CalculateWidths(float fullWidth, out float prefabWidth, out float poolWidth, out float warmupWidth)
private void CalculateWidths(float fullWidth, out float prefabWidth, out float minimumTierWidth,
out float poolWidth, out float warmupWidth)
{
poolWidth = 20f;
minimumTierWidth = 55f;
poolWidth = 30f;
warmupWidth = 60f;
prefabWidth = fullWidth - poolWidth - warmupWidth - (SPACING * 2);
prefabWidth = fullWidth - minimumTierWidth - poolWidth - warmupWidth - (SPACING * 3);
}

public override void OnInspectorGUI()
Expand Down
51 changes: 51 additions & 0 deletions Assets/PurrDiction/Editor/PredictionLODProfileEditor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using PurrNet.Editor;
using UnityEditor;
using UnityEngine;

namespace PurrNet.Prediction.Editor
{
[CustomEditor(typeof(PredictionLODProfile))]
[CanEditMultipleObjects]
internal sealed class PredictionLODProfileEditor : UnityEditor.Editor
{
private SerializedProperty _networkProfile;
private SerializedProperty _tiers;
private SerializedProperty _culledPolicy;
private bool _showInlineNetworkProfile;

private void OnEnable()
{
_networkProfile = serializedObject.FindProperty("_networkProfile");
_tiers = serializedObject.FindProperty("_tiers");
_culledPolicy = serializedObject.FindProperty("_culledPolicy");
}

public override void OnInspectorGUI()
{
serializedObject.UpdateIfRequiredOrScript();
EditorGUILayout.PropertyField(_networkProfile);

if (!_networkProfile.hasMultipleDifferentValues &&
_networkProfile.objectReferenceValue is NetworkLODProfile networkProfile)
{
NetworkLODProfileEditorGUI.DrawSummary(networkProfile);
_showInlineNetworkProfile = EditorGUILayout.Foldout(
_showInlineNetworkProfile,
"Edit Network Profile Inline",
true);

if (_showInlineNetworkProfile)
{
EditorGUI.indentLevel++;
NetworkLODProfileEditorGUI.DrawInlineEditor(networkProfile);
EditorGUI.indentLevel--;
}
}

EditorGUILayout.Space(5f);
EditorGUILayout.PropertyField(_tiers, true);
EditorGUILayout.PropertyField(_culledPolicy);
serializedObject.ApplyModifiedProperties();
}
}
}
2 changes: 2 additions & 0 deletions Assets/PurrDiction/Editor/PredictionLODProfileEditor.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading