mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-17 18:40:33 +00:00
feature: RemoteStatistics tool (#3254)
This commit is contained in:
parent
d26d12fb55
commit
a2756af514
440
Assets/Mirror/Components/RemoteStatistics.cs
Normal file
440
Assets/Mirror/Components/RemoteStatistics.cs
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
// 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void 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;
|
||||||
|
|
||||||
|
// double for long running servers
|
||||||
|
if (Time.timeAsDouble >= lastSendTime + sendInterval)
|
||||||
|
{
|
||||||
|
lastSendTime = Time.timeAsDouble;
|
||||||
|
|
||||||
|
// target rpc to owner client
|
||||||
|
TargetRpcSync(new Stats(
|
||||||
|
// general
|
||||||
|
NetworkServer.connections.Count,
|
||||||
|
Time.realtimeSinceStartupAsDouble,
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Assets/Mirror/Components/RemoteStatistics.cs.meta
Normal file
3
Assets/Mirror/Components/RemoteStatistics.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ba360e4ff6b44fc6898f56322b90c6c8
|
||||||
|
timeCreated: 1663219738
|
@ -74,6 +74,23 @@ public static partial class NetworkServer
|
|||||||
public static Action<NetworkConnectionToClient> OnDisconnectedEvent;
|
public static Action<NetworkConnectionToClient> OnDisconnectedEvent;
|
||||||
public static Action<NetworkConnectionToClient, TransportError, string> OnErrorEvent;
|
public static Action<NetworkConnectionToClient, TransportError, string> OnErrorEvent;
|
||||||
|
|
||||||
|
// keep track of actual achieved tick rate.
|
||||||
|
// might become lower under heavy load.
|
||||||
|
// very useful for profiling etc.
|
||||||
|
// measured over 1s each, same as frame rate. no EMA here.
|
||||||
|
public static int actualTickRate;
|
||||||
|
static double actualTickRateStart; // start time when counting
|
||||||
|
static int actualTickRateCounter; // current counter since start
|
||||||
|
|
||||||
|
// profiling
|
||||||
|
// includes transport update time, because transport calls handlers etc.
|
||||||
|
// averaged over 1s by passing 'tickRate' to constructor.
|
||||||
|
public static TimeSample earlyUpdateDuration;
|
||||||
|
public static TimeSample lateUpdateDuration;
|
||||||
|
|
||||||
|
// capture full Unity update time from before Early- to after LateUpdate
|
||||||
|
public static TimeSample fullUpdateDuration;
|
||||||
|
|
||||||
// initialization / shutdown ///////////////////////////////////////////
|
// initialization / shutdown ///////////////////////////////////////////
|
||||||
static void Initialize()
|
static void Initialize()
|
||||||
{
|
{
|
||||||
@ -96,6 +113,11 @@ static void Initialize()
|
|||||||
AddTransportHandlers();
|
AddTransportHandlers();
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
|
// profiling
|
||||||
|
earlyUpdateDuration = new TimeSample(sendRate);
|
||||||
|
lateUpdateDuration = new TimeSample(sendRate);
|
||||||
|
fullUpdateDuration = new TimeSample(sendRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void AddTransportHandlers()
|
static void AddTransportHandlers()
|
||||||
@ -1786,6 +1808,13 @@ static void Broadcast()
|
|||||||
// (we add this to the UnityEngine in NetworkLoop)
|
// (we add this to the UnityEngine in NetworkLoop)
|
||||||
internal static void NetworkEarlyUpdate()
|
internal static void NetworkEarlyUpdate()
|
||||||
{
|
{
|
||||||
|
// measure update time for profiling.
|
||||||
|
if (active)
|
||||||
|
{
|
||||||
|
earlyUpdateDuration.Begin();
|
||||||
|
fullUpdateDuration.Begin();
|
||||||
|
}
|
||||||
|
|
||||||
// process all incoming messages first before updating the world
|
// process all incoming messages first before updating the world
|
||||||
if (Transport.active != null)
|
if (Transport.active != null)
|
||||||
Transport.active.ServerEarlyUpdate();
|
Transport.active.ServerEarlyUpdate();
|
||||||
@ -1793,13 +1822,18 @@ internal static void NetworkEarlyUpdate()
|
|||||||
// step each connection's local time interpolation in early update.
|
// step each connection's local time interpolation in early update.
|
||||||
foreach (NetworkConnectionToClient connection in connections.Values)
|
foreach (NetworkConnectionToClient connection in connections.Values)
|
||||||
connection.UpdateTimeInterpolation();
|
connection.UpdateTimeInterpolation();
|
||||||
|
|
||||||
|
if (active) earlyUpdateDuration.End();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void NetworkLateUpdate()
|
internal static void NetworkLateUpdate()
|
||||||
{
|
{
|
||||||
// only broadcast world if active
|
|
||||||
if (active)
|
if (active)
|
||||||
{
|
{
|
||||||
|
// measure update time for profiling.
|
||||||
|
lateUpdateDuration.Begin();
|
||||||
|
|
||||||
|
// only broadcast world if active
|
||||||
// broadcast every sendInterval.
|
// broadcast every sendInterval.
|
||||||
// AccurateInterval to avoid update frequency inaccuracy issues:
|
// AccurateInterval to avoid update frequency inaccuracy issues:
|
||||||
// https://github.com/vis2k/Mirror/pull/3153
|
// https://github.com/vis2k/Mirror/pull/3153
|
||||||
@ -1829,6 +1863,26 @@ internal static void NetworkLateUpdate()
|
|||||||
// (even if not active. still want to process disconnects etc.)
|
// (even if not active. still want to process disconnects etc.)
|
||||||
if (Transport.active != null)
|
if (Transport.active != null)
|
||||||
Transport.active.ServerLateUpdate();
|
Transport.active.ServerLateUpdate();
|
||||||
|
|
||||||
|
// measure actual tick rate every second.
|
||||||
|
if (active)
|
||||||
|
{
|
||||||
|
++actualTickRateCounter;
|
||||||
|
if (Time.timeAsDouble >= actualTickRateStart + 1)
|
||||||
|
{
|
||||||
|
// calculate avg by exact elapsed time.
|
||||||
|
// assuming 1s wouldn't be accurate, usually a few more ms passed.
|
||||||
|
float elapsed = (float)(Time.timeAsDouble - actualTickRateStart);
|
||||||
|
actualTickRate = Mathf.RoundToInt(actualTickRateCounter / elapsed);
|
||||||
|
actualTickRateStart = Time.timeAsDouble;
|
||||||
|
actualTickRateCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// measure total update time. including transport.
|
||||||
|
// because in early update, transport update calls handlers.
|
||||||
|
lateUpdateDuration.End();
|
||||||
|
fullUpdateDuration.End();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
61
Assets/Mirror/Core/TimeSample.cs
Normal file
61
Assets/Mirror/Core/TimeSample.cs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// TimeSample from Mirror II.
|
||||||
|
// simple profiling sample, averaged for display in statistics.
|
||||||
|
// usable in builds without unitiy profiler overhead etc.
|
||||||
|
//
|
||||||
|
// .average may safely be called from main thread while Begin/End is in another.
|
||||||
|
// i.e. worker threads, transport, etc.
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Mirror
|
||||||
|
{
|
||||||
|
public struct TimeSample
|
||||||
|
{
|
||||||
|
// UnityEngine.Time isn't thread safe. use stopwatch instead.
|
||||||
|
readonly Stopwatch watch;
|
||||||
|
|
||||||
|
// remember when Begin was called
|
||||||
|
double beginTime;
|
||||||
|
|
||||||
|
// keep accumulating times over the given interval.
|
||||||
|
// (not readonly. we modify its contents.)
|
||||||
|
ExponentialMovingAverage ema;
|
||||||
|
|
||||||
|
// average in seconds.
|
||||||
|
// code often runs in sub-millisecond time. float is more precise.
|
||||||
|
//
|
||||||
|
// set with Interlocked for thread safety.
|
||||||
|
// can be read from main thread while sampling happens in other thread.
|
||||||
|
public double average; // THREAD SAFE
|
||||||
|
|
||||||
|
// average over N begin/end captures
|
||||||
|
public TimeSample(int n)
|
||||||
|
{
|
||||||
|
watch = new Stopwatch();
|
||||||
|
watch.Start();
|
||||||
|
ema = new ExponentialMovingAverage(n);
|
||||||
|
beginTime = 0;
|
||||||
|
average = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// begin is called before the code to be sampled
|
||||||
|
public void Begin()
|
||||||
|
{
|
||||||
|
// remember when Begin was called.
|
||||||
|
// keep StopWatch running so we can average over the given interval.
|
||||||
|
beginTime = watch.Elapsed.TotalSeconds;
|
||||||
|
// Debug.Log($"Begin @ {beginTime:F4}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// end is called after the code to be sampled
|
||||||
|
public void End()
|
||||||
|
{
|
||||||
|
// add duration in seconds to accumulated durations
|
||||||
|
double elapsed = watch.Elapsed.TotalSeconds - beginTime;
|
||||||
|
ema.Add(elapsed);
|
||||||
|
|
||||||
|
// expose new average thread safely
|
||||||
|
Interlocked.Exchange(ref average, ema.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Core/TimeSample.cs.meta
Normal file
11
Assets/Mirror/Core/TimeSample.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 26c32f6429554546a88d800c846c74ed
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
@ -14,6 +14,7 @@ GameObject:
|
|||||||
- component: {fileID: 1078519278818213949}
|
- component: {fileID: 1078519278818213949}
|
||||||
- component: {fileID: 3679374677650722848}
|
- component: {fileID: 3679374677650722848}
|
||||||
- component: {fileID: 8691745481282286165}
|
- component: {fileID: 8691745481282286165}
|
||||||
|
- component: {fileID: 8079286830074380037}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: Player
|
m_Name: Player
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
@ -152,3 +153,22 @@ MonoBehaviour:
|
|||||||
movementProbability: 0.5
|
movementProbability: 0.5
|
||||||
movementDistance: 20
|
movementDistance: 20
|
||||||
manualSpeed: 10
|
manualSpeed: 10
|
||||||
|
--- !u!114 &8079286830074380037
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 449802645721213856}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: ba360e4ff6b44fc6898f56322b90c6c8, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
syncDirection: 0
|
||||||
|
syncMode: 1
|
||||||
|
syncInterval: 0.1
|
||||||
|
sendInterval: 1
|
||||||
|
showGui: 0
|
||||||
|
hotKey: 292
|
||||||
|
passwordFile: remote_statistics.txt
|
||||||
|
36
Assets/Mirror/Tests/Editor/TimeSampleTests.cs
Normal file
36
Assets/Mirror/Tests/Editor/TimeSampleTests.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Mirror.Tests.Editor
|
||||||
|
{
|
||||||
|
public class TimeSampleTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Sample3()
|
||||||
|
{
|
||||||
|
// sample over 3 measurements
|
||||||
|
TimeSample sample = new TimeSample(3);
|
||||||
|
|
||||||
|
// initial without any values
|
||||||
|
Assert.That(sample.average, Is.EqualTo(0));
|
||||||
|
|
||||||
|
// measure 10ms. average should be 10ms.
|
||||||
|
sample.Begin();
|
||||||
|
Thread.Sleep(10);
|
||||||
|
sample.End();
|
||||||
|
Assert.That(sample.average, Is.EqualTo(0.010).Within(0.002));
|
||||||
|
|
||||||
|
// measure 5ms. average should be 7.5ms
|
||||||
|
sample.Begin();
|
||||||
|
Thread.Sleep(5);
|
||||||
|
sample.End();
|
||||||
|
Assert.That(sample.average, Is.EqualTo(0.0075).Within(0.002));
|
||||||
|
|
||||||
|
// measure 0ms. average should be 5ms.
|
||||||
|
sample.Begin();
|
||||||
|
Thread.Sleep(0);
|
||||||
|
sample.End();
|
||||||
|
Assert.That(sample.average, Is.EqualTo(0.005).Within(0.002));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Assets/Mirror/Tests/Editor/TimeSampleTests.cs.meta
Normal file
3
Assets/Mirror/Tests/Editor/TimeSampleTests.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f52163f56d8e48d49fa9c7164d055019
|
||||||
|
timeCreated: 1663221162
|
1
remote_statistics.txt
Normal file
1
remote_statistics.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Mirror!
|
Loading…
Reference in New Issue
Block a user