RemoteStatistics.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. // remote statistics panel from Mirror II to show connections, load, etc.
  2. // server syncs statistics to clients if authenticated.
  3. //
  4. // attach this to a player.
  5. // requires NetworkStatistics component on the Network object.
  6. //
  7. // Unity's OnGUI is the easiest to use solution at the moment.
  8. // * playfab is super complex to set up
  9. // * http servers would be nice, but still need to open ports, live refresh, etc
  10. //
  11. // for safety reasons, let's keep this read-only.
  12. // at least until there's safe authentication.
  13. using System;
  14. using System.IO;
  15. using UnityEngine;
  16. namespace Mirror
  17. {
  18. // server -> client
  19. struct Stats
  20. {
  21. // general
  22. public int connections;
  23. public double uptime;
  24. public int configuredTickRate;
  25. public int actualTickRate;
  26. // traffic
  27. public long sentBytesPerSecond;
  28. public long receiveBytesPerSecond;
  29. // cpu
  30. public float serverTickInterval;
  31. public double fullUpdateAvg;
  32. public double serverEarlyAvg;
  33. public double serverLateAvg;
  34. public double transportEarlyAvg;
  35. public double transportLateAvg;
  36. // C# boilerplate
  37. public Stats(
  38. // general
  39. int connections,
  40. double uptime,
  41. int configuredTickRate,
  42. int actualTickRate,
  43. // traffic
  44. long sentBytesPerSecond,
  45. long receiveBytesPerSecond,
  46. // cpu
  47. float serverTickInterval,
  48. double fullUpdateAvg,
  49. double serverEarlyAvg,
  50. double serverLateAvg,
  51. double transportEarlyAvg,
  52. double transportLateAvg
  53. )
  54. {
  55. // general
  56. this.connections = connections;
  57. this.uptime = uptime;
  58. this.configuredTickRate = configuredTickRate;
  59. this.actualTickRate = actualTickRate;
  60. // traffic
  61. this.sentBytesPerSecond = sentBytesPerSecond;
  62. this.receiveBytesPerSecond = receiveBytesPerSecond;
  63. // cpu
  64. this.serverTickInterval = serverTickInterval;
  65. this.fullUpdateAvg = fullUpdateAvg;
  66. this.serverEarlyAvg = serverEarlyAvg;
  67. this.serverLateAvg = serverLateAvg;
  68. this.transportEarlyAvg = transportEarlyAvg;
  69. this.transportLateAvg = transportLateAvg;
  70. }
  71. }
  72. // [RequireComponent(typeof(NetworkStatistics))] <- needs to be on Network GO, not on NI
  73. public class RemoteStatistics : NetworkBehaviour
  74. {
  75. // components ("fake statics" for similar API)
  76. protected NetworkStatistics NetworkStatistics;
  77. // broadcast to client.
  78. // stats are quite huge, let's only send every few seconds via TargetRpc.
  79. // instead of sending multiple times per second via NB.OnSerialize.
  80. [Tooltip("Send stats every 'interval' seconds to client.")]
  81. public float sendInterval = 1;
  82. double lastSendTime;
  83. [Header("GUI")]
  84. public bool showGui;
  85. public KeyCode hotKey = KeyCode.F11;
  86. Rect windowRect = new Rect(0, 0, 400, 400);
  87. // password can't be stored in code or in Unity project.
  88. // it would be available in clients otherwise.
  89. // this is not perfectly secure. that's why RemoteStatistics is read-only.
  90. [Header("Authentication")]
  91. public string passwordFile = "remote_statistics.txt";
  92. protected bool serverAuthenticated; // client needs to authenticate
  93. protected bool clientAuthenticated; // show GUI until authenticated
  94. protected string serverPassword = null; // null means not found, auth impossible
  95. protected string clientPassword = ""; // for GUI
  96. // statistics synced to client
  97. Stats stats;
  98. void LoadPassword()
  99. {
  100. // TODO only load once, not for all players?
  101. // let's avoid static state for now.
  102. // load the password
  103. string path = Path.GetFullPath(passwordFile);
  104. if (File.Exists(path))
  105. {
  106. // don't spam the server logs for every player's loaded file
  107. // Debug.Log($"RemoteStatistics: loading password file: {path}");
  108. try
  109. {
  110. serverPassword = File.ReadAllText(path);
  111. }
  112. catch (Exception exception)
  113. {
  114. Debug.LogWarning($"RemoteStatistics: failed to read password file: {exception}");
  115. }
  116. }
  117. else
  118. {
  119. Debug.LogWarning($"RemoteStatistics: password file has not been created. Authentication will be impossible. Please save the password in: {path}");
  120. }
  121. }
  122. void OnValidate()
  123. {
  124. syncMode = SyncMode.Owner;
  125. }
  126. // make sure to call base function when overwriting!
  127. // public so it can also be called from tests (and be overwritten by users)
  128. public override void OnStartServer()
  129. {
  130. NetworkStatistics = NetworkManager.singleton.GetComponent<NetworkStatistics>();
  131. if (NetworkStatistics == null) throw new Exception($"RemoteStatistics requires a NetworkStatistics component on {NetworkManager.singleton.name}!");
  132. // server needs to load the password
  133. LoadPassword();
  134. }
  135. public override void OnStartLocalPlayer()
  136. {
  137. // center the window initially
  138. windowRect.x = Screen.width / 2 - windowRect.width / 2;
  139. windowRect.y = Screen.height / 2 - windowRect.height / 2;
  140. }
  141. [TargetRpc]
  142. void TargetRpcSync(Stats v)
  143. {
  144. // store stats and flag as authenticated
  145. clientAuthenticated = true;
  146. stats = v;
  147. }
  148. [Command]
  149. public void CmdAuthenticate(string v)
  150. {
  151. // was a valid password loaded on the server,
  152. // and did the client send the correct one?
  153. if (!string.IsNullOrWhiteSpace(serverPassword) &&
  154. serverPassword.Equals(v))
  155. {
  156. serverAuthenticated = true;
  157. Debug.Log($"RemoteStatistics: connectionId {connectionToClient.connectionId} authenticated with player {name}");
  158. }
  159. }
  160. void UpdateServer()
  161. {
  162. // only sync if client has authenticated on the server
  163. if (!serverAuthenticated) return;
  164. // double for long running servers
  165. if (Time.timeAsDouble >= lastSendTime + sendInterval)
  166. {
  167. lastSendTime = Time.timeAsDouble;
  168. // target rpc to owner client
  169. TargetRpcSync(new Stats(
  170. // general
  171. NetworkServer.connections.Count,
  172. Time.realtimeSinceStartupAsDouble,
  173. NetworkServer.tickRate,
  174. NetworkServer.actualTickRate,
  175. // traffic
  176. NetworkStatistics.serverSentBytesPerSecond,
  177. NetworkStatistics.serverReceivedBytesPerSecond,
  178. // cpu
  179. NetworkServer.tickInterval,
  180. NetworkServer.fullUpdateDuration.average,
  181. NetworkServer.earlyUpdateDuration.average,
  182. NetworkServer.lateUpdateDuration.average,
  183. 0, // TODO ServerTransport.earlyUpdateDuration.average,
  184. 0 // TODO ServerTransport.lateUpdateDuration.average
  185. ));
  186. }
  187. }
  188. void UpdateClient()
  189. {
  190. if (Input.GetKeyDown(hotKey))
  191. showGui = !showGui;
  192. }
  193. void Update()
  194. {
  195. if (isServer) UpdateServer();
  196. if (isLocalPlayer) UpdateClient();
  197. }
  198. void OnGUI()
  199. {
  200. if (!isLocalPlayer) return;
  201. if (!showGui) return;
  202. windowRect = GUILayout.Window(0, windowRect, OnWindow, "Remote Statistics");
  203. windowRect = Utils.KeepInScreen(windowRect);
  204. }
  205. // Text: value
  206. void GUILayout_TextAndValue(string text, string value)
  207. {
  208. GUILayout.BeginHorizontal();
  209. GUILayout.Label(text);
  210. GUILayout.FlexibleSpace();
  211. GUILayout.Label(value);
  212. GUILayout.EndHorizontal();
  213. }
  214. // fake a progress bar via horizontal scroll bar with ratio as width
  215. void GUILayout_ProgressBar(double ratio, int width)
  216. {
  217. // clamp ratio, otherwise >1 would make it extremely large
  218. ratio = Mathd.Clamp01(ratio);
  219. GUILayout.HorizontalScrollbar(0, (float)ratio, 0, 1, GUILayout.Width(width));
  220. }
  221. // need to specify progress bar & caption width,
  222. // otherwise differently sized captions would always misalign the
  223. // progress bars.
  224. void GUILayout_TextAndProgressBar(string text, double ratio, int progressbarWidth, string caption, int captionWidth, Color captionColor)
  225. {
  226. GUILayout.BeginHorizontal();
  227. GUILayout.Label(text);
  228. GUILayout.FlexibleSpace();
  229. GUILayout_ProgressBar(ratio, progressbarWidth);
  230. // coloring the caption is enough. otherwise it's too much.
  231. GUI.color = captionColor;
  232. GUILayout.Label(caption, GUILayout.Width(captionWidth));
  233. GUI.color = Color.white;
  234. GUILayout.EndHorizontal();
  235. }
  236. void GUI_Authenticate()
  237. {
  238. GUILayout.BeginVertical("Box"); // start general
  239. GUILayout.Label("<b>Authentication</b>");
  240. // warning if insecure connection
  241. // if (ClientTransport.IsEncrypted())
  242. // {
  243. // GUILayout.Label("<i>Connection is encrypted!</i>");
  244. // }
  245. // else
  246. // {
  247. GUILayout.Label("<i>Connection is not encrypted. Use with care!</i>");
  248. // }
  249. // input
  250. clientPassword = GUILayout.PasswordField(clientPassword, '*');
  251. // button
  252. GUI.enabled = !string.IsNullOrWhiteSpace(clientPassword);
  253. if (GUILayout.Button("Authenticate"))
  254. {
  255. CmdAuthenticate(clientPassword);
  256. }
  257. GUI.enabled = true;
  258. GUILayout.EndVertical(); // end general
  259. }
  260. void GUI_General(
  261. int connections,
  262. double uptime,
  263. int configuredTickRate,
  264. int actualTickRate)
  265. {
  266. GUILayout.BeginVertical("Box"); // start general
  267. GUILayout.Label("<b>General</b>");
  268. // connections
  269. GUILayout_TextAndValue("Connections:", $"<b>{connections}</b>");
  270. // uptime
  271. GUILayout_TextAndValue("Uptime:", $"<b>{Utils.PrettySeconds(uptime)}</b>"); // TODO
  272. // tick rate
  273. // might be lower under heavy load.
  274. // might be higher in editor if targetFrameRate can't be set.
  275. GUI.color = actualTickRate < configuredTickRate ? Color.red : Color.green;
  276. GUILayout_TextAndValue("Tick Rate:", $"<b>{actualTickRate} Hz / {configuredTickRate} Hz</b>");
  277. GUI.color = Color.white;
  278. GUILayout.EndVertical(); // end general
  279. }
  280. void GUI_Traffic(
  281. long serverSentBytesPerSecond,
  282. long serverReceivedBytesPerSecond)
  283. {
  284. GUILayout.BeginVertical("Box");
  285. GUILayout.Label("<b>Network</b>");
  286. GUILayout_TextAndValue("Outgoing:", $"<b>{Utils.PrettyBytes(serverSentBytesPerSecond) }/s</b>");
  287. GUILayout_TextAndValue("Incoming:", $"<b>{Utils.PrettyBytes(serverReceivedBytesPerSecond)}/s</b>");
  288. GUILayout.EndVertical();
  289. }
  290. void GUI_Cpu(
  291. float serverTickInterval,
  292. double fullUpdateAvg,
  293. double serverEarlyAvg,
  294. double serverLateAvg,
  295. double transportEarlyAvg,
  296. double transportLateAvg)
  297. {
  298. const int barWidth = 120;
  299. const int captionWidth = 90;
  300. GUILayout.BeginVertical("Box");
  301. GUILayout.Label("<b>CPU</b>");
  302. // unity update
  303. // happens every 'tickInterval'. progress bar shows it in relation.
  304. // <= 90% load is green, otherwise red
  305. double fullRatio = fullUpdateAvg / serverTickInterval;
  306. GUILayout_TextAndProgressBar(
  307. "World Update Avg:",
  308. fullRatio,
  309. barWidth, $"<b>{fullUpdateAvg * 1000:F1} ms</b>",
  310. captionWidth,
  311. fullRatio <= 0.9 ? Color.green : Color.red);
  312. // server update
  313. // happens every 'tickInterval'. progress bar shows it in relation.
  314. // <= 90% load is green, otherwise red
  315. double serverRatio = (serverEarlyAvg + serverLateAvg) / serverTickInterval;
  316. GUILayout_TextAndProgressBar(
  317. "Server Update Avg:",
  318. serverRatio,
  319. barWidth, $"<b>{serverEarlyAvg * 1000:F1} + {serverLateAvg * 1000:F1} ms</b>",
  320. captionWidth,
  321. serverRatio <= 0.9 ? Color.green : Color.red);
  322. // transport: early + late update milliseconds.
  323. // for threaded transport, this is the thread's update time.
  324. // happens every 'tickInterval'. progress bar shows it in relation.
  325. // <= 90% load is green, otherwise red
  326. // double transportRatio = (transportEarlyAvg + transportLateAvg) / serverTickInterval;
  327. // GUILayout_TextAndProgressBar(
  328. // "Transport Avg:",
  329. // transportRatio,
  330. // barWidth,
  331. // $"<b>{transportEarlyAvg * 1000:F1} + {transportLateAvg * 1000:F1} ms</b>",
  332. // captionWidth,
  333. // transportRatio <= 0.9 ? Color.green : Color.red);
  334. GUILayout.EndVertical();
  335. }
  336. void GUI_Notice()
  337. {
  338. // for security reasons, let's keep this read-only for now.
  339. // single line keeps input & visuals simple
  340. // GUILayout.BeginVertical("Box");
  341. // GUILayout.Label("<b>Global Notice</b>");
  342. // notice = GUILayout.TextField(notice);
  343. // if (GUILayout.Button("Send"))
  344. // {
  345. // // TODO
  346. // }
  347. // GUILayout.EndVertical();
  348. }
  349. void OnWindow(int windowID)
  350. {
  351. if (!clientAuthenticated)
  352. {
  353. GUI_Authenticate();
  354. }
  355. else
  356. {
  357. GUI_General(
  358. stats.connections,
  359. stats.uptime,
  360. stats.configuredTickRate,
  361. stats.actualTickRate
  362. );
  363. GUI_Traffic(
  364. stats.sentBytesPerSecond,
  365. stats.receiveBytesPerSecond
  366. );
  367. GUI_Cpu(
  368. stats.serverTickInterval,
  369. stats.fullUpdateAvg,
  370. stats.serverEarlyAvg,
  371. stats.serverLateAvg,
  372. stats.transportEarlyAvg,
  373. stats.transportLateAvg
  374. );
  375. GUI_Notice();
  376. }
  377. // dragable window in any case
  378. GUI.DragWindow(new Rect(0, 0, 10000, 10000));
  379. }
  380. }
  381. }