mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 19:10:32 +00:00
feat: NetworkRuntimeProfiler (#3846)
* feat: NetworkRuntimeProfiler Adds a simple text-based network profiler * Remove unused avg
This commit is contained in:
parent
36dcd0df7d
commit
7cfd7d942d
326
Assets/Mirror/Components/NetworkRuntimeProfiler.cs
Normal file
326
Assets/Mirror/Components/NetworkRuntimeProfiler.cs
Normal file
@ -0,0 +1,326 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Mirror.RemoteCalls;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public class NetworkRuntimeProfiler : MonoBehaviour
|
||||
{
|
||||
[Serializable]
|
||||
public class Sorter : IComparer<Stat>
|
||||
{
|
||||
public SortBy Order;
|
||||
public int Compare(Stat a, Stat b)
|
||||
{
|
||||
if (a == null) return 1;
|
||||
if (b == null) return -1;
|
||||
// Compare B to A for desc order
|
||||
switch (Order)
|
||||
{
|
||||
case SortBy.RecentBytes:
|
||||
return b.RecentBytes.CompareTo(a.RecentBytes);
|
||||
case SortBy.RecentCount:
|
||||
return b.RecentCount.CompareTo(a.RecentCount);
|
||||
case SortBy.TotalBytes:
|
||||
return b.TotalBytes.CompareTo(a.TotalBytes);
|
||||
case SortBy.TotalCount:
|
||||
return b.TotalCount.CompareTo(a.TotalCount);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum SortBy
|
||||
{
|
||||
RecentBytes,
|
||||
RecentCount,
|
||||
TotalBytes,
|
||||
TotalCount,
|
||||
}
|
||||
|
||||
public class Stat
|
||||
{
|
||||
public string Name;
|
||||
public long TotalCount;
|
||||
public long TotalBytes;
|
||||
|
||||
public long RecentCount;
|
||||
public long RecentBytes;
|
||||
|
||||
public void ResetRecent()
|
||||
{
|
||||
RecentCount = 0;
|
||||
RecentBytes = 0;
|
||||
}
|
||||
|
||||
public void Add(int count, int bytes)
|
||||
{
|
||||
this.TotalBytes += bytes;
|
||||
this.TotalCount += count;
|
||||
|
||||
this.RecentBytes += bytes;
|
||||
this.RecentCount += count;
|
||||
}
|
||||
}
|
||||
|
||||
public class MessageStats
|
||||
{
|
||||
public Dictionary<Type, Stat> MessageByType = new Dictionary<Type, Stat>();
|
||||
public Dictionary<ushort, Stat> RpcByHash = new Dictionary<ushort, Stat>();
|
||||
public void Record(NetworkDiagnostics.MessageInfo info)
|
||||
{
|
||||
|
||||
Type type = info.message.GetType();
|
||||
if (!MessageByType.TryGetValue(type, out Stat stat))
|
||||
{
|
||||
stat = new Stat
|
||||
{
|
||||
Name = type.ToString(),
|
||||
TotalCount = 0,
|
||||
TotalBytes = 0,
|
||||
RecentCount = 0,
|
||||
RecentBytes = 0
|
||||
};
|
||||
MessageByType[type] = stat;
|
||||
}
|
||||
stat.Add(info.count, info.bytes * info.count);
|
||||
|
||||
if (info.message is CommandMessage cmd)
|
||||
{
|
||||
RecordRpc(cmd.functionHash, info);
|
||||
}
|
||||
else if (info.message is RpcMessage rpc)
|
||||
{
|
||||
RecordRpc(rpc.functionHash, info);
|
||||
}
|
||||
}
|
||||
|
||||
private void RecordRpc(ushort hash, NetworkDiagnostics.MessageInfo info)
|
||||
{
|
||||
if (!RpcByHash.TryGetValue(hash, out Stat stat))
|
||||
{
|
||||
string name = "n/a";
|
||||
RemoteCallDelegate rpcDelegate = RemoteProcedureCalls.GetDelegate(hash);
|
||||
if (rpcDelegate != null)
|
||||
{
|
||||
name = $"{rpcDelegate.Method.DeclaringType}.{rpcDelegate.GetMethodName().Replace(RemoteProcedureCalls.InvokeRpcPrefix, "")}";
|
||||
}
|
||||
stat = new Stat
|
||||
{
|
||||
Name = name,
|
||||
TotalCount = 0,
|
||||
TotalBytes = 0,
|
||||
RecentCount = 0,
|
||||
RecentBytes = 0
|
||||
};
|
||||
RpcByHash[hash] = stat;
|
||||
}
|
||||
stat.Add(info.count, info.bytes * info.count);
|
||||
}
|
||||
|
||||
public void ResetRecent()
|
||||
{
|
||||
foreach (Stat stat in MessageByType.Values)
|
||||
{
|
||||
stat.ResetRecent();
|
||||
}
|
||||
|
||||
foreach (Stat stat in RpcByHash.Values)
|
||||
{
|
||||
stat.ResetRecent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Tooltip("How many seconds to accumulate 'recent' stats for, this is also the output interval")]
|
||||
public float RecentDuration = 5;
|
||||
public Sorter Sort = new Sorter();
|
||||
public enum OutputType
|
||||
{
|
||||
UnityLog,
|
||||
StdOut,
|
||||
File
|
||||
}
|
||||
public OutputType Output;
|
||||
[Tooltip("If Output is set to 'File', where to the path of that file")]
|
||||
public string OutputFilePath = "network-stats.log";
|
||||
|
||||
private readonly MessageStats _in = new MessageStats();
|
||||
private readonly MessageStats _out = new MessageStats();
|
||||
private StringBuilder _print = new StringBuilder();
|
||||
private float _elapsedSinceReset;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
NetworkDiagnostics.InMessageEvent += HandleMessageIn;
|
||||
NetworkDiagnostics.OutMessageEvent += HandleMessageOut;
|
||||
}
|
||||
private void OnDestroy()
|
||||
{
|
||||
NetworkDiagnostics.InMessageEvent -= HandleMessageIn;
|
||||
NetworkDiagnostics.OutMessageEvent -= HandleMessageOut;
|
||||
}
|
||||
private void HandleMessageOut(NetworkDiagnostics.MessageInfo info)
|
||||
{
|
||||
_out.Record(info);
|
||||
}
|
||||
private void HandleMessageIn(NetworkDiagnostics.MessageInfo info)
|
||||
{
|
||||
_in.Record(info);
|
||||
}
|
||||
private void LateUpdate()
|
||||
{
|
||||
_elapsedSinceReset += Time.deltaTime;
|
||||
if (_elapsedSinceReset > RecentDuration)
|
||||
{
|
||||
_elapsedSinceReset = 0;
|
||||
Print();
|
||||
_in.ResetRecent();
|
||||
_out.ResetRecent();
|
||||
}
|
||||
}
|
||||
private void Print()
|
||||
{
|
||||
_print.Clear();
|
||||
_print.AppendLine($"Stats for {DateTime.Now} ({RecentDuration:N1}s interval)");
|
||||
int nameMaxLength = "OUT Message".Length;
|
||||
|
||||
foreach (Stat stat in _in.MessageByType.Values)
|
||||
{
|
||||
if (stat.Name.Length > nameMaxLength)
|
||||
{
|
||||
nameMaxLength = stat.Name.Length;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Stat stat in _out.MessageByType.Values)
|
||||
{
|
||||
if (stat.Name.Length > nameMaxLength)
|
||||
{
|
||||
nameMaxLength = stat.Name.Length;
|
||||
}
|
||||
}
|
||||
foreach (Stat stat in _in.RpcByHash.Values)
|
||||
{
|
||||
if (stat.Name.Length > nameMaxLength)
|
||||
{
|
||||
nameMaxLength = stat.Name.Length;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Stat stat in _out.RpcByHash.Values)
|
||||
{
|
||||
if (stat.Name.Length > nameMaxLength)
|
||||
{
|
||||
nameMaxLength = stat.Name.Length;
|
||||
}
|
||||
}
|
||||
|
||||
string recentBytes = "Recent Bytes";
|
||||
string recentCount = "Recent Count";
|
||||
string totalBytes = "Total Bytes";
|
||||
string totalCount = "Total Count";
|
||||
int maxBytesLength = FormatBytes(999999).Length;
|
||||
int maxCountLength = FormatCount(999999).Length;
|
||||
|
||||
int recentBytesPad = Mathf.Max(recentBytes.Length, maxBytesLength);
|
||||
int recentCountPad = Mathf.Max(recentCount.Length, maxCountLength);
|
||||
int totalBytesPad = Mathf.Max(totalBytes.Length, maxBytesLength);
|
||||
int totalCountPad = Mathf.Max(totalCount.Length, maxCountLength);
|
||||
string header = $"| {"IN Message".PadLeft(nameMaxLength)} | {recentBytes.PadLeft(recentBytesPad)} | {recentCount.PadLeft(recentCountPad)} | {totalBytes.PadLeft(totalBytesPad)} | {totalCount.PadLeft(totalCountPad)} |";
|
||||
string sep = "".PadLeft(header.Length, '-');
|
||||
_print.AppendLine(sep);
|
||||
_print.AppendLine(header);
|
||||
_print.AppendLine(sep);
|
||||
|
||||
foreach (Stat stat in _in.MessageByType.Values.OrderBy(stat => stat, Sort))
|
||||
{
|
||||
_print.AppendLine($"| {stat.Name.PadLeft(nameMaxLength)} | {FormatBytes(stat.RecentBytes).PadLeft(recentBytesPad)} | {FormatCount(stat.RecentCount).PadLeft(recentCountPad)} | {FormatBytes(stat.TotalBytes).PadLeft(totalBytesPad)} | {FormatCount(stat.TotalCount).PadLeft(totalCountPad)} |");
|
||||
}
|
||||
header = $"| {"IN RPCs".PadLeft(nameMaxLength)} | {recentBytes.PadLeft(recentBytesPad)} | {recentCount.PadLeft(recentCountPad)} | {totalBytes.PadLeft(totalBytesPad)} | {totalCount.PadLeft(totalCountPad)} |";
|
||||
_print.AppendLine(sep);
|
||||
_print.AppendLine(header);
|
||||
_print.AppendLine(sep);
|
||||
foreach (Stat stat in _in.RpcByHash.Values.OrderBy(stat => stat, Sort))
|
||||
{
|
||||
_print.AppendLine($"| {stat.Name.PadLeft(nameMaxLength)} | {FormatBytes(stat.RecentBytes).PadLeft(recentBytesPad)} | {FormatCount(stat.RecentCount).PadLeft(recentCountPad)} | {FormatBytes(stat.TotalBytes).PadLeft(totalBytesPad)} | {FormatCount(stat.TotalCount).PadLeft(totalCountPad)} |");
|
||||
}
|
||||
header = $"| {"OUT Message".PadLeft(nameMaxLength)} | {recentBytes.PadLeft(recentBytesPad)} | {recentCount.PadLeft(recentCountPad)} | {totalBytes.PadLeft(totalBytesPad)} | {totalCount.PadLeft(totalCountPad)} |";
|
||||
_print.AppendLine(sep);
|
||||
_print.AppendLine(header);
|
||||
_print.AppendLine(sep);
|
||||
foreach (Stat stat in _out.MessageByType.Values.OrderBy(stat => stat, Sort))
|
||||
{
|
||||
_print.AppendLine($"| {stat.Name.PadLeft(nameMaxLength)} | {FormatBytes(stat.RecentBytes).PadLeft(recentBytesPad)} | {FormatCount(stat.RecentCount).PadLeft(recentCountPad)} | {FormatBytes(stat.TotalBytes).PadLeft(totalBytesPad)} | {FormatCount(stat.TotalCount).PadLeft(totalCountPad)} |");
|
||||
}
|
||||
|
||||
header = $"| {"OUT RPCs".PadLeft(nameMaxLength)} | {recentBytes.PadLeft(recentBytesPad)} | {recentCount.PadLeft(recentCountPad)} | {totalBytes.PadLeft(totalBytesPad)} | {totalCount.PadLeft(totalCountPad)} |";
|
||||
_print.AppendLine(sep);
|
||||
_print.AppendLine(header);
|
||||
_print.AppendLine(sep);
|
||||
|
||||
foreach (Stat stat in _out.RpcByHash.Values.OrderBy(stat => stat, Sort))
|
||||
{
|
||||
_print.AppendLine($"| {stat.Name.PadLeft(nameMaxLength)} | {FormatBytes(stat.RecentBytes).PadLeft(recentBytesPad)} | {FormatCount(stat.RecentCount).PadLeft(recentCountPad)} | {FormatBytes(stat.TotalBytes).PadLeft(totalBytesPad)} | {FormatCount(stat.TotalCount).PadLeft(totalCountPad)} |");
|
||||
}
|
||||
_print.AppendLine(sep);
|
||||
|
||||
switch (Output)
|
||||
{
|
||||
|
||||
case OutputType.UnityLog:
|
||||
Debug.Log(_print.ToString());
|
||||
break;
|
||||
case OutputType.StdOut:
|
||||
Console.Write(_print);
|
||||
break;
|
||||
case OutputType.File:
|
||||
File.AppendAllText(OutputFilePath, _print.ToString());
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatBytes(long bytes)
|
||||
{
|
||||
const double KiB = 1024;
|
||||
const double MiB = KiB * 1024;
|
||||
const double GiB = MiB * 1024;
|
||||
const double TiB = GiB * 1024;
|
||||
|
||||
if (bytes < KiB)
|
||||
return $"{bytes:N0} B";
|
||||
if (bytes < MiB)
|
||||
return $"{bytes / KiB:N2} KiB";
|
||||
if (bytes < GiB)
|
||||
return $"{bytes / MiB:N2} MiB";
|
||||
if (bytes < TiB)
|
||||
return $"{bytes / GiB:N2} GiB";
|
||||
return $"{bytes / TiB:N2} TiB";
|
||||
}
|
||||
|
||||
private string FormatCount(long count)
|
||||
{
|
||||
const double K = 1000;
|
||||
const double M = K * 1000;
|
||||
const double G = M * 1000;
|
||||
const double T = G * 1000;
|
||||
|
||||
if (count < K)
|
||||
return $"{count:N0}";
|
||||
if (count < M)
|
||||
return $"{count / K:N2} K";
|
||||
if (count < G)
|
||||
return $"{count / M:N2} M";
|
||||
if (count < T)
|
||||
return $"{count / G:N2} G";
|
||||
return $"{count / T:N2} T";
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Components/NetworkRuntimeProfiler.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkRuntimeProfiler.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef8db82aeb77400bb9e80850e39065a9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Loading…
Reference in New Issue
Block a user