RemoteStatistics.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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. protected override void OnValidate()
  123. {
  124. base.OnValidate();
  125. syncMode = SyncMode.Owner;
  126. }
  127. // make sure to call base function when overwriting!
  128. // public so it can also be called from tests (and be overwritten by users)
  129. public override void OnStartServer()
  130. {
  131. NetworkStatistics = NetworkManager.singleton.GetComponent<NetworkStatistics>();
  132. if (NetworkStatistics == null) throw new Exception($"RemoteStatistics requires a NetworkStatistics component on {NetworkManager.singleton.name}!");
  133. // server needs to load the password
  134. LoadPassword();
  135. }
  136. public override void OnStartLocalPlayer()
  137. {
  138. // center the window initially
  139. windowRect.x = Screen.width / 2 - windowRect.width / 2;
  140. windowRect.y = Screen.height / 2 - windowRect.height / 2;
  141. }
  142. [TargetRpc]
  143. void TargetRpcSync(Stats v)
  144. {
  145. // store stats and flag as authenticated
  146. clientAuthenticated = true;
  147. stats = v;
  148. }
  149. [Command]
  150. public void CmdAuthenticate(string v)
  151. {
  152. // was a valid password loaded on the server,
  153. // and did the client send the correct one?
  154. if (!string.IsNullOrWhiteSpace(serverPassword) &&
  155. serverPassword.Equals(v))
  156. {
  157. serverAuthenticated = true;
  158. Debug.Log($"RemoteStatistics: connectionId {connectionToClient.connectionId} authenticated with player {name}");
  159. }
  160. }
  161. void UpdateServer()
  162. {
  163. // only sync if client has authenticated on the server
  164. if (!serverAuthenticated) return;
  165. // NetworkTime.localTime has defines for 2019 / 2020 compatibility
  166. if (NetworkTime.localTime >= lastSendTime + sendInterval)
  167. {
  168. lastSendTime = NetworkTime.localTime;
  169. // target rpc to owner client
  170. TargetRpcSync(new Stats(
  171. // general
  172. NetworkServer.connections.Count,
  173. NetworkTime.time,
  174. NetworkServer.tickRate,
  175. NetworkServer.actualTickRate,
  176. // traffic
  177. NetworkStatistics.serverSentBytesPerSecond,
  178. NetworkStatistics.serverReceivedBytesPerSecond,
  179. // cpu
  180. NetworkServer.tickInterval,
  181. NetworkServer.fullUpdateDuration.average,
  182. NetworkServer.earlyUpdateDuration.average,
  183. NetworkServer.lateUpdateDuration.average,
  184. 0, // TODO ServerTransport.earlyUpdateDuration.average,
  185. 0 // TODO ServerTransport.lateUpdateDuration.average
  186. ));
  187. }
  188. }
  189. void UpdateClient()
  190. {
  191. if (Input.GetKeyDown(hotKey))
  192. showGui = !showGui;
  193. }
  194. void Update()
  195. {
  196. if (isServer) UpdateServer();
  197. if (isLocalPlayer) UpdateClient();
  198. }
  199. void OnGUI()
  200. {
  201. if (!isLocalPlayer) return;
  202. if (!showGui) return;
  203. windowRect = GUILayout.Window(0, windowRect, OnWindow, "Remote Statistics");
  204. windowRect = Utils.KeepInScreen(windowRect);
  205. }
  206. // Text: value
  207. void GUILayout_TextAndValue(string text, string value)
  208. {
  209. GUILayout.BeginHorizontal();
  210. GUILayout.Label(text);
  211. GUILayout.FlexibleSpace();
  212. GUILayout.Label(value);
  213. GUILayout.EndHorizontal();
  214. }
  215. // fake a progress bar via horizontal scroll bar with ratio as width
  216. void GUILayout_ProgressBar(double ratio, int width)
  217. {
  218. // clamp ratio, otherwise >1 would make it extremely large
  219. ratio = Mathd.Clamp01(ratio);
  220. GUILayout.HorizontalScrollbar(0, (float)ratio, 0, 1, GUILayout.Width(width));
  221. }
  222. // need to specify progress bar & caption width,
  223. // otherwise differently sized captions would always misalign the
  224. // progress bars.
  225. void GUILayout_TextAndProgressBar(string text, double ratio, int progressbarWidth, string caption, int captionWidth, Color captionColor)
  226. {
  227. GUILayout.BeginHorizontal();
  228. GUILayout.Label(text);
  229. GUILayout.FlexibleSpace();
  230. GUILayout_ProgressBar(ratio, progressbarWidth);
  231. // coloring the caption is enough. otherwise it's too much.
  232. GUI.color = captionColor;
  233. GUILayout.Label(caption, GUILayout.Width(captionWidth));
  234. GUI.color = Color.white;
  235. GUILayout.EndHorizontal();
  236. }
  237. void GUI_Authenticate()
  238. {
  239. GUILayout.BeginVertical("Box"); // start general
  240. GUILayout.Label("<b>Authentication</b>");
  241. // warning if insecure connection
  242. // if (ClientTransport.IsEncrypted())
  243. // {
  244. // GUILayout.Label("<i>Connection is encrypted!</i>");
  245. // }
  246. // else
  247. // {
  248. GUILayout.Label("<i>Connection is not encrypted. Use with care!</i>");
  249. // }
  250. // input
  251. clientPassword = GUILayout.PasswordField(clientPassword, '*');
  252. // button
  253. GUI.enabled = !string.IsNullOrWhiteSpace(clientPassword);
  254. if (GUILayout.Button("Authenticate"))
  255. {
  256. CmdAuthenticate(clientPassword);
  257. }
  258. GUI.enabled = true;
  259. GUILayout.EndVertical(); // end general
  260. }
  261. void GUI_General(
  262. int connections,
  263. double uptime,
  264. int configuredTickRate,
  265. int actualTickRate)
  266. {
  267. GUILayout.BeginVertical("Box"); // start general
  268. GUILayout.Label("<b>General</b>");
  269. // connections
  270. GUILayout_TextAndValue("Connections:", $"<b>{connections}</b>");
  271. // uptime
  272. GUILayout_TextAndValue("Uptime:", $"<b>{Utils.PrettySeconds(uptime)}</b>"); // TODO
  273. // tick rate
  274. // might be lower under heavy load.
  275. // might be higher in editor if targetFrameRate can't be set.
  276. GUI.color = actualTickRate < configuredTickRate ? Color.red : Color.green;
  277. GUILayout_TextAndValue("Tick Rate:", $"<b>{actualTickRate} Hz / {configuredTickRate} Hz</b>");
  278. GUI.color = Color.white;
  279. GUILayout.EndVertical(); // end general
  280. }
  281. void GUI_Traffic(
  282. long serverSentBytesPerSecond,
  283. long serverReceivedBytesPerSecond)
  284. {
  285. GUILayout.BeginVertical("Box");
  286. GUILayout.Label("<b>Network</b>");
  287. GUILayout_TextAndValue("Outgoing:", $"<b>{Utils.PrettyBytes(serverSentBytesPerSecond) }/s</b>");
  288. GUILayout_TextAndValue("Incoming:", $"<b>{Utils.PrettyBytes(serverReceivedBytesPerSecond)}/s</b>");
  289. GUILayout.EndVertical();
  290. }
  291. void GUI_Cpu(
  292. float serverTickInterval,
  293. double fullUpdateAvg,
  294. double serverEarlyAvg,
  295. double serverLateAvg,
  296. double transportEarlyAvg,
  297. double transportLateAvg)
  298. {
  299. const int barWidth = 120;
  300. const int captionWidth = 90;
  301. GUILayout.BeginVertical("Box");
  302. GUILayout.Label("<b>CPU</b>");
  303. // unity update
  304. // happens every 'tickInterval'. progress bar shows it in relation.
  305. // <= 90% load is green, otherwise red
  306. double fullRatio = fullUpdateAvg / serverTickInterval;
  307. GUILayout_TextAndProgressBar(
  308. "World Update Avg:",
  309. fullRatio,
  310. barWidth, $"<b>{fullUpdateAvg * 1000:F1} ms</b>",
  311. captionWidth,
  312. fullRatio <= 0.9 ? Color.green : Color.red);
  313. // server update
  314. // happens every 'tickInterval'. progress bar shows it in relation.
  315. // <= 90% load is green, otherwise red
  316. double serverRatio = (serverEarlyAvg + serverLateAvg) / serverTickInterval;
  317. GUILayout_TextAndProgressBar(
  318. "Server Update Avg:",
  319. serverRatio,
  320. barWidth, $"<b>{serverEarlyAvg * 1000:F1} + {serverLateAvg * 1000:F1} ms</b>",
  321. captionWidth,
  322. serverRatio <= 0.9 ? Color.green : Color.red);
  323. // transport: early + late update milliseconds.
  324. // for threaded transport, this is the thread's update time.
  325. // happens every 'tickInterval'. progress bar shows it in relation.
  326. // <= 90% load is green, otherwise red
  327. // double transportRatio = (transportEarlyAvg + transportLateAvg) / serverTickInterval;
  328. // GUILayout_TextAndProgressBar(
  329. // "Transport Avg:",
  330. // transportRatio,
  331. // barWidth,
  332. // $"<b>{transportEarlyAvg * 1000:F1} + {transportLateAvg * 1000:F1} ms</b>",
  333. // captionWidth,
  334. // transportRatio <= 0.9 ? Color.green : Color.red);
  335. GUILayout.EndVertical();
  336. }
  337. void GUI_Notice()
  338. {
  339. // for security reasons, let's keep this read-only for now.
  340. // single line keeps input & visuals simple
  341. // GUILayout.BeginVertical("Box");
  342. // GUILayout.Label("<b>Global Notice</b>");
  343. // notice = GUILayout.TextField(notice);
  344. // if (GUILayout.Button("Send"))
  345. // {
  346. // // TODO
  347. // }
  348. // GUILayout.EndVertical();
  349. }
  350. void OnWindow(int windowID)
  351. {
  352. if (!clientAuthenticated)
  353. {
  354. GUI_Authenticate();
  355. }
  356. else
  357. {
  358. GUI_General(
  359. stats.connections,
  360. stats.uptime,
  361. stats.configuredTickRate,
  362. stats.actualTickRate
  363. );
  364. GUI_Traffic(
  365. stats.sentBytesPerSecond,
  366. stats.receiveBytesPerSecond
  367. );
  368. GUI_Cpu(
  369. stats.serverTickInterval,
  370. stats.fullUpdateAvg,
  371. stats.serverEarlyAvg,
  372. stats.serverLateAvg,
  373. stats.transportEarlyAvg,
  374. stats.transportLateAvg
  375. );
  376. GUI_Notice();
  377. }
  378. // dragable window in any case
  379. GUI.DragWindow(new Rect(0, 0, 10000, 10000));
  380. }
  381. }
  382. }