Skip to content

Commit 758ef5c

Browse files
committed
add tick rate sessions, drop 50 connections, simpler report
1 parent 4a4b289 commit 758ef5c

16 files changed

Lines changed: 557 additions & 415 deletions

File tree

.github/scripts/bench-aggregate.sh

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env bash
2-
# Renders one benchmark run (one netcode, one connection count) to the job summary and emits a
3-
# compact datapoint (dp-<netcode>-<connections>.json) for bench-scaling.sh.
2+
# Renders one benchmark run (one netcode, one session) to the job summary and emits a compact
3+
# datapoint (dp-<netcode>-<tag>.json) for bench-scaling.sh.
44
#
55
# Inputs are the per-process JSON files written by BenchRunner (Shared/com.purrnet.netbench):
66
# <results_dir>/server.json server process
@@ -119,13 +119,14 @@ jq -n \
119119
| {
120120
netcode: $netcode,
121121
connections: $connections,
122-
# Requested size from the tag (c100 -> 100) so capped runs (e.g. Fusion at 99) land in the
123-
# same table row as the other netcodes; falls back to the actual connection count.
124-
size: (([$tag | capture("^c(?<n>[0-9]+)$") | .n] | first // ($connections | tostring)) | tonumber),
122+
# Requested size and tick from the tag (c100t60 -> 100 connections at 60 Hz) so capped runs
123+
# (e.g. Fusion at 99) land in the same row as the other netcodes; fall back to the actual values.
124+
size: (([$tag | capture("^c(?<n>[0-9]+)") | .n] | first // ($connections | tostring)) | tonumber),
125+
tick: (([$tag | capture("t(?<t>[0-9]+)$") | .t] | first // ($server.tickRate | tostring)) | tonumber),
125126
tag: $tag,
126127
meta: {
127128
cpuModel: $server.cpuModel, cpuCount: $server.cpuCount, devBuild: $server.devBuild,
128-
tickRate: $server.tickRate, unityVersion: $server.unityVersion,
129+
tickRate: $server.tickRate, requestedTickRate: ($server.requestedTickRate // 0), unityVersion: $server.unityVersion,
129130
connectedAtStart: $server.connectedAtStart, expectedClients: $server.expectedClients,
130131
serverError: (if ($server.error // "") == "" then null else $server.error end), measuredClients: ($clients | length)
131132
},
@@ -145,6 +146,6 @@ jq -n \
145146
}})
146147
| from_entries )
147148
}
148-
' > "$OUT_DIR/dp-${NETCODE}-${TOTAL}.json"
149+
' > "$OUT_DIR/dp-${NETCODE}-${TAG}.json"
149150

150-
echo "Datapoint written to $OUT_DIR/dp-${NETCODE}-${TOTAL}.json"
151+
echo "Datapoint written to $OUT_DIR/dp-${NETCODE}-${TAG}.json"

.github/scripts/bench-scaling.sh

Lines changed: 26 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,47 @@
11
#!/usr/bin/env bash
2-
# Renders the cross-netcode scaling comparison from the datapoints emitted by bench-aggregate.sh
3-
# (dp-<netcode>-<connections>.json). Output goes to the job summary and to <out_dir>/summary.md,
4-
# with the merged datapoints in <out_dir>/scaling.json.
2+
# Merges the datapoints emitted by bench-aggregate.sh (dp-<netcode>-<tag>.json) into
3+
# <out_dir>/scaling.json and writes a short coverage table to the job summary. The readable
4+
# tables come from render-summary.py and the full picture from render-report.py; both read
5+
# scaling.json.
56
#
6-
# Usage: bench-scaling.sh <datapoints_dir> <window_s> <objects> [versions_json] [out_dir]
7+
# Usage: bench-scaling.sh <datapoints_dir> [out_dir]
78
set -euo pipefail
89

910
DP_DIR="${1:?datapoints dir}"
10-
WINDOW="${2:-?}"
11-
OBJECTS="${3:-?}"
12-
VERSIONS="${4:-}"
13-
OUT_DIR="${5:-results-out}"
11+
OUT_DIR="${2:-results-out}"
1412

1513
SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/stdout}"
1614
mkdir -p "$OUT_DIR"
17-
MD="$OUT_DIR/summary.md"
1815

1916
shopt -s nullglob
2017
FILES=("$DP_DIR"/dp-*.json)
2118

2219
if [ ${#FILES[@]} -eq 0 ]; then
23-
echo "## Netcode scaling benchmark" > "$MD"
24-
echo "" >> "$MD"
25-
echo "_No datapoints collected._" >> "$MD"
26-
cat "$MD" >> "$SUMMARY"
20+
echo "[]" > "$OUT_DIR/scaling.json"
21+
printf '## Netcode scaling benchmark\n\n_No datapoints collected._\n' >> "$SUMMARY"
2722
exit 0
2823
fi
2924

30-
VERSIONS_JSON="{}"
31-
[ -n "$VERSIONS" ] && [ -f "$VERSIONS" ] && VERSIONS_JSON=$(cat "$VERSIONS")
32-
3325
jq -s '.' "${FILES[@]}" > "$OUT_DIR/scaling.json"
3426

35-
jq -r --argjson versions "$VERSIONS_JSON" --arg window "$WINDOW" --arg objects "$OBJECTS" '
36-
def r1: if . == null then "-" else "\((.*10|floor)/10)" end;
37-
def r2: if . == null then "-" else "\((.*100|floor)/100)" end;
38-
def kb: if . == null then "-" else "\((./1024*10|floor)/10)" end;
39-
def d1: (. // 0) | (.*10|floor)/10;
40-
def shortCpu: if . == null then "-" else (gsub("\\(R\\)|\\(TM\\)|CPU|Processor|@.*$"; "") | gsub(" +"; " ") | ltrimstr(" ") | rtrimstr(" ")) end;
41-
27+
# Coverage: one row per session, one column per netcode, cell = clients connected at start.
28+
jq -r '
4229
(["purrnet", "fishnet", "mirror", "ngo", "fusion"]) as $order
43-
| ["#2f6feb", "#e36209", "#6f42c1", "#1a7f37", "#cf222e"] as $palette
4430
| (map(.netcode) | unique) as $present
4531
| [ $order[] | select(. as $n | $present | index($n)) ] as $netcodes
46-
# Rows are keyed by the requested size (Fusion capped at 99 still sits in the 100 row); the
47-
# "clients connected" table shows the actual counts.
48-
| (map(.size // .connections) | unique | sort) as $conns
49-
# dp[netcode][size] = datapoint
50-
| (reduce .[] as $d ({}; .[$d.netcode][($d.size // $d.connections) | tostring] = $d)) as $dp
51-
| def cell($n; $c): $dp[$n][$c | tostring];
52-
# Generic table: rows = connection counts, columns = netcodes, cell = f(datapoint)
53-
def table(title; f):
54-
"#### \(title)\n\n| Connections | \($netcodes | join(" | ")) |\n|---|\($netcodes | map("---") | join("|"))|\n"
55-
+ ( [ $conns[] as $c | "| \($c) | " + ( [ $netcodes[] as $n | (cell($n; $c)) as $d | if $d == null then "-" else ($d | f) end ] | join(" | ") ) + " |" ] | join("\n") )
56-
+ "\n\n";
57-
def chart(title; ylabel; f):
58-
( [ $netcodes[] as $n | [ $conns[] as $c | (cell($n; $c)) as $d | if $d == null then 0 else ($d | f | d1) end ] ] ) as $series
59-
| ( [ $series[][] ] | max // 0 ) as $m
60-
| "```mermaid\n%%{init: {\"themeVariables\": {\"xyChart\": {\"plotColorPalette\": \"\([ range(0; $netcodes|length) as $i | $palette[$i] ] | join(","))\"}}}}%%\nxychart-beta\n"
61-
+ " title \"\(title)\"\n"
62-
+ " x-axis [\($conns | map(tostring) | join(", "))]\n"
63-
+ " y-axis \"\(ylabel)\" 0 --> \((($m * 1.1) | floor) + 1)\n"
64-
+ ( [ $series[] | " line [\(map(tostring) | join(", "))]" ] | join("\n") )
65-
+ "\n```\n"
66-
+ "Series: " + ( [ range(0; $netcodes|length) as $i | "\($palette[$i]) \($netcodes[$i])" ] | join(" · ") ) + "\n\n";
67-
"## Netcode scaling benchmark\n\n"
68-
+ "Window: \($window)s per test · Objects per test: \($objects) · Connections: \($conns | map(tostring) | join(" / "))\n\n"
69-
+ "| Netcode | Version |\n|---|---|\n"
70-
+ ( [ $netcodes[] as $n | "| \($n) | \($versions[$n] // "?") |" ] | join("\n") )
71-
+ "\n| unity | \($versions.unity // "?") |\n\n"
72-
+ "Every netcode runs the same scenario: the server spawns N objects and replicates them to every client. MoveY / MoveAllAxis / MoveWander move them each tick; SendRPC fires one observers-RPC with one float per object per tick; Static spawns them and never touches them; SpawnChurn keeps N alive while despawning and spawning N/50 per tick; ClientInput spawns one hub object and every client sends one small server RPC (Vector3 + float) per tick; SyncVars changes one of four synced fields per object per tick. "
73-
+ "Server numbers come from the single server process; client numbers are averages over the single-process measured clients. "
74-
+ "On-wire bandwidth is read from the network interface (headers, ACKs and resends included). CPU is the whole process, all threads, as % of one core with the frame loop capped at 60 fps; nothing is subtracted, so the Idle row (connected, nothing spawned) is what holding the connections costs on its own. "
75-
+ "Fusion is relay-based: server and clients talk to Photon Cloud (traffic measured on the public interface, RTT includes the relay hop); the server still sends one stream per client, so its downstream is comparable.\n\n"
76-
+ "### Machines\n\n"
77-
+ table("Server CPU model (numbers are only comparable within a row when these match)"; "\(.meta.cpuModel | shortCpu) x\(.meta.cpuCount)\(if .meta.devBuild then " (dev)" else "" end)")
78-
+ table("Clients connected at start / expected · measured clients (rows are the requested size; a capped or partially connected run shows its real count here)"; "\(.meta.connectedAtStart)/\(.meta.expectedClients) · \(.meta.measuredClients)\(if .meta.serverError != null then " ⚠️ \(.meta.serverError)" else "" end)")
79-
+ "### Idle baseline (connected, nothing spawned)\n\n"
80-
+ table("Server CPU % · frame p95 ms"; "\(.server.Idle.cpuPercent | r1)% · \(.server.Idle.p95FrameMs | r2) ms")
81-
+ ( [ ("MoveY", "MoveAllAxis", "MoveWander", "SendRPC", "Static", "SpawnChurn", "ClientInput", "SyncVars") as $t
82-
| "### \($t)\n\n"
83-
+ table("\($t) — server downstream on-wire (KB/s, all clients)"; .server[$t].txBytesPerSec | kb)
84-
+ table("\($t) — per-client downstream on-wire (KB/s, client-measured)"; .clients[$t].rxBytesPerSec | kb)
85-
+ table("\($t) — server upstream on-wire (KB/s, all clients)"; .server[$t].rxBytesPerSec | kb)
86-
+ table("\($t) — per-client upstream on-wire (KB/s, client-measured)"; .clients[$t].txBytesPerSec | kb)
87-
+ ( if $t == "ClientInput" then table("ClientInput — server input RPCs received per second (expected ≈ 20 × connections)"; .server[$t].inputsPerSec | if . == null then "-" else floor end) else "" end )
88-
+ table("\($t) — server CPU %"; .server[$t].cpuPercent | if . == null then "-" else "\(. | r1)%" end)
89-
+ table("\($t) — server frame avg / p95 / p99 (ms)"; "\(.server[$t].avgFrameMs | r2) / \(.server[$t].p95FrameMs | r2) / \(.server[$t].p99FrameMs | r2)")
90-
+ table("\($t) — server GC collections · peak RSS"; "\(.server[$t].gcCollections // "-") · \(((.server[$t].peakRssBytes // 0) / 1048576) | floor) MB")
91-
+ table("\($t) — client RTT p50 / p95 (ms, netcode-reported)"; "\(.clients[$t].rttP50Ms | r2) / \(.clients[$t].rttP95Ms | r2)")
92-
+ ( if ($conns | length) < 2 then "_Charts need ≥2 connection sizes._\n\n"
93-
else chart("\($t) — server downstream KB/s vs connections"; "KB/s"; (.server[$t].txBytesPerSec // 0) / 1024)
94-
+ chart("\($t) — per-client downstream KB/s vs connections"; "KB/s"; (.clients[$t].rxBytesPerSec // 0) / 1024)
95-
+ chart("\($t) — server CPU % vs connections"; "CPU %"; .server[$t].cpuPercent // 0)
96-
+ chart("\($t) — client RTT p95 ms vs connections"; "ms"; .clients[$t].rttP95Ms // 0)
97-
end )
98-
] | join("") )
99-
' "$OUT_DIR/scaling.json" > "$MD" || {
100-
echo "_Failed to render datapoints._" > "$MD"
101-
}
102-
103-
cat "$MD" >> "$SUMMARY"
104-
echo "" >> "$SUMMARY"
105-
echo "Summary written to $MD"
32+
| (map({size: .size, tick: .tick}) | unique | sort_by(.size, .tick)) as $sessions
33+
| (reduce .[] as $d ({}; .[$d.netcode + "@" + ($d.size|tostring) + "@" + ($d.tick|tostring)] = $d)) as $dp
34+
| "### Datapoints\n\n| Session | " + ($netcodes | join(" | ")) + " |\n|---|" + ($netcodes | map("---") | join("|")) + "|\n"
35+
+ ( [ $sessions[] as $s
36+
| "| \($s.size) connections @ \($s.tick) Hz | "
37+
+ ( [ $netcodes[] as $n | $dp[$n + "@" + ($s.size|tostring) + "@" + ($s.tick|tostring)] as $d
38+
| if $d == null then "missing"
39+
else "\($d.meta.connectedAtStart)/\($d.meta.expectedClients)"
40+
+ (if $d.meta.tickRate != $s.tick then " ⚠️ \($d.meta.tickRate) Hz" else "" end)
41+
+ (if $d.meta.serverError != null then " ⚠️ \($d.meta.serverError)" else "" end)
42+
end ] | join(" | ") )
43+
+ " |" ] | join("\n") )
44+
+ "\n\nCells are clients connected at start / expected.\n"
45+
' "$OUT_DIR/scaling.json" >> "$SUMMARY"
46+
47+
echo "Merged ${#FILES[@]} datapoints into $OUT_DIR/scaling.json"

0 commit comments

Comments
 (0)