alap
This commit is contained in:
441
Assets/Mirror/Components/RemoteStatistics.cs
Normal file
441
Assets/Mirror/Components/RemoteStatistics.cs
Normal file
@ -0,0 +1,441 @@
|
||||
// remote statistics panel from Mirror II to show connections, load, etc.
|
||||
// server syncs statistics to clients if authenticated.
|
||||
//
|
||||
// attach this to a player.
|
||||
// requires NetworkStatistics component on the Network object.
|
||||
//
|
||||
// Unity's OnGUI is the easiest to use solution at the moment.
|
||||
// * playfab is super complex to set up
|
||||
// * http servers would be nice, but still need to open ports, live refresh, etc
|
||||
//
|
||||
// for safety reasons, let's keep this read-only.
|
||||
// at least until there's safe authentication.
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// server -> client
|
||||
struct Stats
|
||||
{
|
||||
// general
|
||||
public int connections;
|
||||
public double uptime;
|
||||
public int configuredTickRate;
|
||||
public int actualTickRate;
|
||||
|
||||
// traffic
|
||||
public long sentBytesPerSecond;
|
||||
public long receiveBytesPerSecond;
|
||||
|
||||
// cpu
|
||||
public float serverTickInterval;
|
||||
public double fullUpdateAvg;
|
||||
public double serverEarlyAvg;
|
||||
public double serverLateAvg;
|
||||
public double transportEarlyAvg;
|
||||
public double transportLateAvg;
|
||||
|
||||
// C# boilerplate
|
||||
public Stats(
|
||||
// general
|
||||
int connections,
|
||||
double uptime,
|
||||
int configuredTickRate,
|
||||
int actualTickRate,
|
||||
// traffic
|
||||
long sentBytesPerSecond,
|
||||
long receiveBytesPerSecond,
|
||||
// cpu
|
||||
float serverTickInterval,
|
||||
double fullUpdateAvg,
|
||||
double serverEarlyAvg,
|
||||
double serverLateAvg,
|
||||
double transportEarlyAvg,
|
||||
double transportLateAvg
|
||||
)
|
||||
{
|
||||
// general
|
||||
this.connections = connections;
|
||||
this.uptime = uptime;
|
||||
this.configuredTickRate = configuredTickRate;
|
||||
this.actualTickRate = actualTickRate;
|
||||
|
||||
// traffic
|
||||
this.sentBytesPerSecond = sentBytesPerSecond;
|
||||
this.receiveBytesPerSecond = receiveBytesPerSecond;
|
||||
|
||||
// cpu
|
||||
this.serverTickInterval = serverTickInterval;
|
||||
this.fullUpdateAvg = fullUpdateAvg;
|
||||
this.serverEarlyAvg = serverEarlyAvg;
|
||||
this.serverLateAvg = serverLateAvg;
|
||||
this.transportEarlyAvg = transportEarlyAvg;
|
||||
this.transportLateAvg = transportLateAvg;
|
||||
}
|
||||
}
|
||||
|
||||
// [RequireComponent(typeof(NetworkStatistics))] <- needs to be on Network GO, not on NI
|
||||
public class RemoteStatistics : NetworkBehaviour
|
||||
{
|
||||
// components ("fake statics" for similar API)
|
||||
protected NetworkStatistics NetworkStatistics;
|
||||
|
||||
// broadcast to client.
|
||||
// stats are quite huge, let's only send every few seconds via TargetRpc.
|
||||
// instead of sending multiple times per second via NB.OnSerialize.
|
||||
[Tooltip("Send stats every 'interval' seconds to client.")]
|
||||
public float sendInterval = 1;
|
||||
double lastSendTime;
|
||||
|
||||
[Header("GUI")]
|
||||
public bool showGui;
|
||||
public KeyCode hotKey = KeyCode.F11;
|
||||
Rect windowRect = new Rect(0, 0, 400, 400);
|
||||
|
||||
// password can't be stored in code or in Unity project.
|
||||
// it would be available in clients otherwise.
|
||||
// this is not perfectly secure. that's why RemoteStatistics is read-only.
|
||||
[Header("Authentication")]
|
||||
public string passwordFile = "remote_statistics.txt";
|
||||
protected bool serverAuthenticated; // client needs to authenticate
|
||||
protected bool clientAuthenticated; // show GUI until authenticated
|
||||
protected string serverPassword = null; // null means not found, auth impossible
|
||||
protected string clientPassword = ""; // for GUI
|
||||
|
||||
// statistics synced to client
|
||||
Stats stats;
|
||||
|
||||
void LoadPassword()
|
||||
{
|
||||
// TODO only load once, not for all players?
|
||||
// let's avoid static state for now.
|
||||
|
||||
// load the password
|
||||
string path = Path.GetFullPath(passwordFile);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
// don't spam the server logs for every player's loaded file
|
||||
// Debug.Log($"RemoteStatistics: loading password file: {path}");
|
||||
try
|
||||
{
|
||||
serverPassword = File.ReadAllText(path);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogWarning($"RemoteStatistics: failed to read password file: {exception}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"RemoteStatistics: password file has not been created. Authentication will be impossible. Please save the password in: {path}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
syncMode = SyncMode.Owner;
|
||||
}
|
||||
|
||||
// make sure to call base function when overwriting!
|
||||
// public so it can also be called from tests (and be overwritten by users)
|
||||
public override void OnStartServer()
|
||||
{
|
||||
NetworkStatistics = NetworkManager.singleton.GetComponent<NetworkStatistics>();
|
||||
if (NetworkStatistics == null) throw new Exception($"RemoteStatistics requires a NetworkStatistics component on {NetworkManager.singleton.name}!");
|
||||
|
||||
// server needs to load the password
|
||||
LoadPassword();
|
||||
}
|
||||
|
||||
public override void OnStartLocalPlayer()
|
||||
{
|
||||
// center the window initially
|
||||
windowRect.x = Screen.width / 2 - windowRect.width / 2;
|
||||
windowRect.y = Screen.height / 2 - windowRect.height / 2;
|
||||
}
|
||||
|
||||
[TargetRpc]
|
||||
void TargetRpcSync(Stats v)
|
||||
{
|
||||
// store stats and flag as authenticated
|
||||
clientAuthenticated = true;
|
||||
stats = v;
|
||||
}
|
||||
|
||||
[Command]
|
||||
public void CmdAuthenticate(string v)
|
||||
{
|
||||
// was a valid password loaded on the server,
|
||||
// and did the client send the correct one?
|
||||
if (!string.IsNullOrWhiteSpace(serverPassword) &&
|
||||
serverPassword.Equals(v))
|
||||
{
|
||||
serverAuthenticated = true;
|
||||
Debug.Log($"RemoteStatistics: connectionId {connectionToClient.connectionId} authenticated with player {name}");
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateServer()
|
||||
{
|
||||
// only sync if client has authenticated on the server
|
||||
if (!serverAuthenticated) return;
|
||||
|
||||
// NetworkTime.localTime has defines for 2019 / 2020 compatibility
|
||||
if (NetworkTime.localTime >= lastSendTime + sendInterval)
|
||||
{
|
||||
lastSendTime = NetworkTime.localTime;
|
||||
|
||||
// target rpc to owner client
|
||||
TargetRpcSync(new Stats(
|
||||
// general
|
||||
NetworkServer.connections.Count,
|
||||
NetworkTime.time,
|
||||
NetworkServer.tickRate,
|
||||
NetworkServer.actualTickRate,
|
||||
|
||||
// traffic
|
||||
NetworkStatistics.serverSentBytesPerSecond,
|
||||
NetworkStatistics.serverReceivedBytesPerSecond,
|
||||
|
||||
// cpu
|
||||
NetworkServer.tickInterval,
|
||||
NetworkServer.fullUpdateDuration.average,
|
||||
NetworkServer.earlyUpdateDuration.average,
|
||||
NetworkServer.lateUpdateDuration.average,
|
||||
0, // TODO ServerTransport.earlyUpdateDuration.average,
|
||||
0 // TODO ServerTransport.lateUpdateDuration.average
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClient()
|
||||
{
|
||||
if (Input.GetKeyDown(hotKey))
|
||||
showGui = !showGui;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (isServer) UpdateServer();
|
||||
if (isLocalPlayer) UpdateClient();
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (!isLocalPlayer) return;
|
||||
if (!showGui) return;
|
||||
|
||||
windowRect = GUILayout.Window(0, windowRect, OnWindow, "Remote Statistics");
|
||||
windowRect = Utils.KeepInScreen(windowRect);
|
||||
}
|
||||
|
||||
// Text: value
|
||||
void GUILayout_TextAndValue(string text, string value)
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label(text);
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.Label(value);
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
// fake a progress bar via horizontal scroll bar with ratio as width
|
||||
void GUILayout_ProgressBar(double ratio, int width)
|
||||
{
|
||||
// clamp ratio, otherwise >1 would make it extremely large
|
||||
ratio = Mathd.Clamp01(ratio);
|
||||
GUILayout.HorizontalScrollbar(0, (float)ratio, 0, 1, GUILayout.Width(width));
|
||||
}
|
||||
|
||||
// need to specify progress bar & caption width,
|
||||
// otherwise differently sized captions would always misalign the
|
||||
// progress bars.
|
||||
void GUILayout_TextAndProgressBar(string text, double ratio, int progressbarWidth, string caption, int captionWidth, Color captionColor)
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label(text);
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout_ProgressBar(ratio, progressbarWidth);
|
||||
|
||||
// coloring the caption is enough. otherwise it's too much.
|
||||
GUI.color = captionColor;
|
||||
GUILayout.Label(caption, GUILayout.Width(captionWidth));
|
||||
GUI.color = Color.white;
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
void GUI_Authenticate()
|
||||
{
|
||||
GUILayout.BeginVertical("Box"); // start general
|
||||
GUILayout.Label("<b>Authentication</b>");
|
||||
|
||||
// warning if insecure connection
|
||||
// if (ClientTransport.IsEncrypted())
|
||||
// {
|
||||
// GUILayout.Label("<i>Connection is encrypted!</i>");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
GUILayout.Label("<i>Connection is not encrypted. Use with care!</i>");
|
||||
// }
|
||||
|
||||
// input
|
||||
clientPassword = GUILayout.PasswordField(clientPassword, '*');
|
||||
|
||||
// button
|
||||
GUI.enabled = !string.IsNullOrWhiteSpace(clientPassword);
|
||||
if (GUILayout.Button("Authenticate"))
|
||||
{
|
||||
CmdAuthenticate(clientPassword);
|
||||
}
|
||||
GUI.enabled = true;
|
||||
|
||||
GUILayout.EndVertical(); // end general
|
||||
}
|
||||
|
||||
void GUI_General(
|
||||
int connections,
|
||||
double uptime,
|
||||
int configuredTickRate,
|
||||
int actualTickRate)
|
||||
{
|
||||
GUILayout.BeginVertical("Box"); // start general
|
||||
GUILayout.Label("<b>General</b>");
|
||||
|
||||
// connections
|
||||
GUILayout_TextAndValue("Connections:", $"<b>{connections}</b>");
|
||||
|
||||
// uptime
|
||||
GUILayout_TextAndValue("Uptime:", $"<b>{Utils.PrettySeconds(uptime)}</b>"); // TODO
|
||||
|
||||
// tick rate
|
||||
// might be lower under heavy load.
|
||||
// might be higher in editor if targetFrameRate can't be set.
|
||||
GUI.color = actualTickRate < configuredTickRate ? Color.red : Color.green;
|
||||
GUILayout_TextAndValue("Tick Rate:", $"<b>{actualTickRate} Hz / {configuredTickRate} Hz</b>");
|
||||
GUI.color = Color.white;
|
||||
|
||||
GUILayout.EndVertical(); // end general
|
||||
}
|
||||
|
||||
void GUI_Traffic(
|
||||
long serverSentBytesPerSecond,
|
||||
long serverReceivedBytesPerSecond)
|
||||
{
|
||||
GUILayout.BeginVertical("Box");
|
||||
GUILayout.Label("<b>Network</b>");
|
||||
|
||||
GUILayout_TextAndValue("Outgoing:", $"<b>{Utils.PrettyBytes(serverSentBytesPerSecond) }/s</b>");
|
||||
GUILayout_TextAndValue("Incoming:", $"<b>{Utils.PrettyBytes(serverReceivedBytesPerSecond)}/s</b>");
|
||||
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
void GUI_Cpu(
|
||||
float serverTickInterval,
|
||||
double fullUpdateAvg,
|
||||
double serverEarlyAvg,
|
||||
double serverLateAvg,
|
||||
double transportEarlyAvg,
|
||||
double transportLateAvg)
|
||||
{
|
||||
const int barWidth = 120;
|
||||
const int captionWidth = 90;
|
||||
|
||||
GUILayout.BeginVertical("Box");
|
||||
GUILayout.Label("<b>CPU</b>");
|
||||
|
||||
// unity update
|
||||
// happens every 'tickInterval'. progress bar shows it in relation.
|
||||
// <= 90% load is green, otherwise red
|
||||
double fullRatio = fullUpdateAvg / serverTickInterval;
|
||||
GUILayout_TextAndProgressBar(
|
||||
"World Update Avg:",
|
||||
fullRatio,
|
||||
barWidth, $"<b>{fullUpdateAvg * 1000:F1} ms</b>",
|
||||
captionWidth,
|
||||
fullRatio <= 0.9 ? Color.green : Color.red);
|
||||
|
||||
// server update
|
||||
// happens every 'tickInterval'. progress bar shows it in relation.
|
||||
// <= 90% load is green, otherwise red
|
||||
double serverRatio = (serverEarlyAvg + serverLateAvg) / serverTickInterval;
|
||||
GUILayout_TextAndProgressBar(
|
||||
"Server Update Avg:",
|
||||
serverRatio,
|
||||
barWidth, $"<b>{serverEarlyAvg * 1000:F1} + {serverLateAvg * 1000:F1} ms</b>",
|
||||
captionWidth,
|
||||
serverRatio <= 0.9 ? Color.green : Color.red);
|
||||
|
||||
// transport: early + late update milliseconds.
|
||||
// for threaded transport, this is the thread's update time.
|
||||
// happens every 'tickInterval'. progress bar shows it in relation.
|
||||
// <= 90% load is green, otherwise red
|
||||
// double transportRatio = (transportEarlyAvg + transportLateAvg) / serverTickInterval;
|
||||
// GUILayout_TextAndProgressBar(
|
||||
// "Transport Avg:",
|
||||
// transportRatio,
|
||||
// barWidth,
|
||||
// $"<b>{transportEarlyAvg * 1000:F1} + {transportLateAvg * 1000:F1} ms</b>",
|
||||
// captionWidth,
|
||||
// transportRatio <= 0.9 ? Color.green : Color.red);
|
||||
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
void GUI_Notice()
|
||||
{
|
||||
// for security reasons, let's keep this read-only for now.
|
||||
|
||||
// single line keeps input & visuals simple
|
||||
// GUILayout.BeginVertical("Box");
|
||||
// GUILayout.Label("<b>Global Notice</b>");
|
||||
// notice = GUILayout.TextField(notice);
|
||||
// if (GUILayout.Button("Send"))
|
||||
// {
|
||||
// // TODO
|
||||
// }
|
||||
// GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
void OnWindow(int windowID)
|
||||
{
|
||||
if (!clientAuthenticated)
|
||||
{
|
||||
GUI_Authenticate();
|
||||
}
|
||||
else
|
||||
{
|
||||
GUI_General(
|
||||
stats.connections,
|
||||
stats.uptime,
|
||||
stats.configuredTickRate,
|
||||
stats.actualTickRate
|
||||
);
|
||||
|
||||
GUI_Traffic(
|
||||
stats.sentBytesPerSecond,
|
||||
stats.receiveBytesPerSecond
|
||||
);
|
||||
|
||||
GUI_Cpu(
|
||||
stats.serverTickInterval,
|
||||
stats.fullUpdateAvg,
|
||||
stats.serverEarlyAvg,
|
||||
stats.serverLateAvg,
|
||||
stats.transportEarlyAvg,
|
||||
stats.transportLateAvg
|
||||
);
|
||||
|
||||
GUI_Notice();
|
||||
}
|
||||
|
||||
// dragable window in any case
|
||||
GUI.DragWindow(new Rect(0, 0, 10000, 10000));
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user