Skip to content

Commit bad7b7b

Browse files
committed
improve benchmarks and websocket dispatch performance
1 parent d502d9c commit bad7b7b

14 files changed

Lines changed: 3491 additions & 33 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System;
2+
using UnityEngine;
3+
4+
#if UNITY_EDITOR
5+
using UnityEditor;
6+
using UnityEditor.SceneManagement;
7+
8+
public static class BenchmarkBatchRunner
9+
{
10+
public static void RunSyncContextBenchmarks()
11+
{
12+
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
13+
14+
var gameObject = new GameObject("BenchmarkRunner");
15+
var runner = gameObject.AddComponent<BenchmarkRunner>();
16+
17+
runner.serverUrl = GetArg("-benchmarkServer", runner.serverUrl);
18+
runner.implementationName = GetArg("-benchmarkImplementation", runner.implementationName);
19+
runner.outputFileName = GetArg("-benchmarkOutput", runner.outputFileName);
20+
runner.runLatency = false;
21+
runner.runThroughput = true;
22+
runner.runThroughputSend = false;
23+
runner.runThroughputReceive = true;
24+
runner.runThroughputBidirectional = false;
25+
runner.runGCAlloc = false;
26+
runner.runFrameTime = true;
27+
runner.runBurst = true;
28+
runner.throughputDurationSec = GetIntArg("-benchmarkThroughputDuration", 5);
29+
runner.burstMessageCount = GetIntArg("-benchmarkBurstCount", runner.burstMessageCount);
30+
runner.exitWhenDone = true;
31+
32+
string payloadSizesArg = GetArg("-benchmarkPayloadSizes", null);
33+
if (!string.IsNullOrEmpty(payloadSizesArg))
34+
{
35+
string[] parts = payloadSizesArg.Split(',');
36+
int[] payloadSizes = new int[parts.Length];
37+
for (int i = 0; i < parts.Length; i++)
38+
{
39+
payloadSizes[i] = int.Parse(parts[i]);
40+
}
41+
runner.payloadSizes = payloadSizes;
42+
}
43+
44+
EditorSceneManager.SaveScene(scene, "Assets/BenchmarkBatchScene.unity");
45+
EditorApplication.EnterPlaymode();
46+
}
47+
48+
private static string GetArg(string name, string fallback)
49+
{
50+
string[] args = Environment.GetCommandLineArgs();
51+
for (int i = 0; i < args.Length - 1; i++)
52+
{
53+
if (args[i] == name)
54+
{
55+
return args[i + 1];
56+
}
57+
}
58+
59+
return fallback;
60+
}
61+
62+
private static int GetIntArg(string name, int fallback)
63+
{
64+
string value = GetArg(name, null);
65+
if (string.IsNullOrEmpty(value))
66+
{
67+
return fallback;
68+
}
69+
70+
int parsed;
71+
if (int.TryParse(value, out parsed))
72+
{
73+
return parsed;
74+
}
75+
76+
return fallback;
77+
}
78+
}
79+
#endif
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System.Collections;
2+
using System.IO;
3+
using UnityEngine;
4+
5+
public class BenchmarkRunner : MonoBehaviour
6+
{
7+
[Header("Server")]
8+
public string serverUrl = "ws://localhost:3000";
9+
10+
[Header("Implementation")]
11+
public string implementationName = "new";
12+
13+
[Header("Output")]
14+
public string outputFileName = "benchmark_results.csv";
15+
16+
[Header("Benchmarks to Run")]
17+
public bool runLatency = true;
18+
public bool runThroughput = true;
19+
public bool runThroughputSend = true;
20+
public bool runThroughputReceive = true;
21+
public bool runThroughputBidirectional = true;
22+
public bool runGCAlloc = true;
23+
public bool runFrameTime = true;
24+
public bool runBurst = true;
25+
26+
[Header("Automation")]
27+
public bool exitWhenDone = false;
28+
29+
[Header("Parameters")]
30+
public int latencyIterations = 1000;
31+
public int throughputDurationSec = 10;
32+
public int burstMessageCount = 100;
33+
public int[] payloadSizes = new int[] { 32, 256, 1024, 8192, 65536 };
34+
35+
private BenchmarkLogger logger;
36+
37+
void Start()
38+
{
39+
string outputPath = Path.Combine(Application.persistentDataPath, outputFileName);
40+
logger = new BenchmarkLogger(implementationName, outputPath);
41+
Debug.Log("Results will be written to: " + outputPath);
42+
StartCoroutine(RunAllBenchmarks());
43+
}
44+
45+
IEnumerator RunAllBenchmarks()
46+
{
47+
Debug.Log(string.Format("=== Starting benchmarks for [{0}] ===", implementationName));
48+
yield return new WaitForSeconds(1f);
49+
50+
if (runLatency)
51+
{
52+
var bench = new LatencyBench();
53+
foreach (int size in payloadSizes)
54+
{
55+
Debug.Log(string.Format("--- Latency benchmark (payload={0}B) ---", size));
56+
yield return StartCoroutine(bench.Run(serverUrl, latencyIterations, size, logger));
57+
yield return new WaitForSeconds(1f);
58+
}
59+
}
60+
61+
if (runThroughput)
62+
{
63+
var bench = new ThroughputBench();
64+
foreach (int size in payloadSizes)
65+
{
66+
Debug.Log(string.Format("--- Throughput benchmark (payload={0}B) ---", size));
67+
yield return StartCoroutine(bench.Run(
68+
serverUrl,
69+
throughputDurationSec,
70+
size,
71+
logger,
72+
runThroughputSend,
73+
runThroughputReceive,
74+
runThroughputBidirectional));
75+
yield return new WaitForSeconds(1f);
76+
}
77+
}
78+
79+
if (runGCAlloc)
80+
{
81+
var bench = new GCAllocBench();
82+
foreach (int size in payloadSizes)
83+
{
84+
Debug.Log(string.Format("--- GC Allocation benchmark (payload={0}B) ---", size));
85+
yield return StartCoroutine(bench.Run(serverUrl, size, logger));
86+
yield return new WaitForSeconds(1f);
87+
}
88+
}
89+
90+
if (runFrameTime)
91+
{
92+
var bench = new FrameTimeBench();
93+
Debug.Log("--- Frame Time benchmark ---");
94+
yield return StartCoroutine(bench.Run(serverUrl, logger));
95+
yield return new WaitForSeconds(1f);
96+
}
97+
98+
if (runBurst)
99+
{
100+
var bench = new BurstBench();
101+
Debug.Log(string.Format("--- Burst benchmark (count={0}) ---", burstMessageCount));
102+
yield return StartCoroutine(bench.Run(serverUrl, burstMessageCount, logger));
103+
yield return new WaitForSeconds(1f);
104+
}
105+
106+
Debug.Log(string.Format("=== All benchmarks complete for [{0}] ===", implementationName));
107+
logger.Close();
108+
109+
#if UNITY_EDITOR
110+
if (exitWhenDone && Application.isBatchMode)
111+
{
112+
UnityEditor.EditorApplication.delayCall += () => UnityEditor.EditorApplication.Exit(0);
113+
}
114+
#endif
115+
}
116+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Diagnostics;
5+
using System.Text;
6+
using UnityEngine;
7+
using NativeWebSocket;
8+
9+
public class FrameTimeBench
10+
{
11+
public IEnumerator Run(string serverUrl, BenchmarkLogger logger)
12+
{
13+
var ws = new WebSocket(serverUrl);
14+
bool connected = false;
15+
bool closed = false;
16+
bool floodStopped = false;
17+
18+
ws.OnOpen += () => connected = true;
19+
ws.OnMessage += (data) =>
20+
{
21+
if (IsExactTextMessage(data, "flood_stopped"))
22+
{
23+
floodStopped = true;
24+
}
25+
};
26+
ws.OnClose += (code) => closed = true;
27+
28+
_ = ws.Connect();
29+
30+
float timeout = 0;
31+
while (!connected && !closed && timeout < 10f)
32+
{
33+
ws.DispatchMessageQueue();
34+
timeout += Time.unscaledDeltaTime;
35+
yield return null;
36+
}
37+
if (!connected) yield break;
38+
39+
int payloadSize = 256;
40+
var sw = new Stopwatch();
41+
42+
// --- Baseline: dispatch time with no pending messages ---
43+
var baselineTimes = new List<double>();
44+
for (int i = 0; i < 300; i++)
45+
{
46+
sw.Restart();
47+
ws.DispatchMessageQueue();
48+
sw.Stop();
49+
baselineTimes.Add(sw.Elapsed.TotalMilliseconds);
50+
yield return null;
51+
}
52+
53+
// --- Under load: sustained flood + measure dispatch time ---
54+
_ = ws.SendText(string.Format("flood_continuous:{0}", payloadSize));
55+
56+
float warmup = 0f;
57+
while (warmup < 0.2f)
58+
{
59+
ws.DispatchMessageQueue();
60+
warmup += Time.unscaledDeltaTime;
61+
yield return null;
62+
}
63+
64+
var dispatchTimes = new List<double>();
65+
var frameTimes = new List<double>();
66+
var frameSw = new Stopwatch();
67+
68+
for (int i = 0; i < 600; i++)
69+
{
70+
frameSw.Restart();
71+
72+
sw.Restart();
73+
ws.DispatchMessageQueue();
74+
sw.Stop();
75+
dispatchTimes.Add(sw.Elapsed.TotalMilliseconds);
76+
77+
frameSw.Stop();
78+
frameTimes.Add(frameSw.Elapsed.TotalMilliseconds);
79+
yield return null;
80+
}
81+
82+
_ = ws.SendText("flood_stop");
83+
timeout = 0;
84+
while (!floodStopped && timeout < 1f)
85+
{
86+
ws.DispatchMessageQueue();
87+
timeout += Time.unscaledDeltaTime;
88+
yield return null;
89+
}
90+
91+
// Log baseline
92+
var baselineStats = new BenchmarkStats(baselineTimes);
93+
logger.LogResult("FrameTime_Baseline", 0, "avg_ms", baselineStats.Average);
94+
logger.LogResult("FrameTime_Baseline", 0, "p99_ms", baselineStats.P99);
95+
logger.LogResult("FrameTime_Baseline", 0, "max_ms", baselineStats.Max);
96+
logger.LogResult("FrameTime_Baseline", 0, "jitter_stddev_ms", baselineStats.StdDev);
97+
98+
// Log dispatch under load
99+
var dispatchStats = new BenchmarkStats(dispatchTimes);
100+
logger.LogResult("FrameTime_Dispatch", payloadSize, "avg_ms", dispatchStats.Average);
101+
logger.LogResult("FrameTime_Dispatch", payloadSize, "median_ms", dispatchStats.Median);
102+
logger.LogResult("FrameTime_Dispatch", payloadSize, "p95_ms", dispatchStats.P95);
103+
logger.LogResult("FrameTime_Dispatch", payloadSize, "p99_ms", dispatchStats.P99);
104+
logger.LogResult("FrameTime_Dispatch", payloadSize, "max_ms", dispatchStats.Max);
105+
logger.LogResult("FrameTime_Dispatch", payloadSize, "jitter_stddev_ms", dispatchStats.StdDev);
106+
logger.LogSummary("FrameTime_Dispatch", payloadSize, dispatchStats);
107+
108+
// Log total frame time under load
109+
var frameStats = new BenchmarkStats(frameTimes);
110+
logger.LogResult("FrameTime_UnderLoad", payloadSize, "avg_ms", frameStats.Average);
111+
logger.LogResult("FrameTime_UnderLoad", payloadSize, "p99_ms", frameStats.P99);
112+
logger.LogResult("FrameTime_UnderLoad", payloadSize, "max_ms", frameStats.Max);
113+
logger.LogResult("FrameTime_UnderLoad", payloadSize, "jitter_stddev_ms", frameStats.StdDev);
114+
115+
UnityEngine.Debug.Log(string.Format(
116+
"Frame time: baseline avg={0:F3}ms, dispatch under load avg={1:F3}ms p99={2:F3}ms",
117+
baselineStats.Average, dispatchStats.Average, dispatchStats.P99));
118+
119+
closed = false;
120+
_ = ws.Close();
121+
timeout = 0;
122+
while (!closed && timeout < 5f)
123+
{
124+
ws.DispatchMessageQueue();
125+
timeout += Time.unscaledDeltaTime;
126+
yield return null;
127+
}
128+
}
129+
130+
private static bool IsExactTextMessage(byte[] data, string expected)
131+
{
132+
if (data == null || expected == null)
133+
{
134+
return false;
135+
}
136+
137+
if (data.Length != Encoding.UTF8.GetByteCount(expected))
138+
{
139+
return false;
140+
}
141+
142+
try
143+
{
144+
return Encoding.UTF8.GetString(data) == expected;
145+
}
146+
catch
147+
{
148+
return false;
149+
}
150+
}
151+
}

0 commit comments

Comments
 (0)