Files
sbox-public/engine/Sandbox.Engine/Scene/GameObjectSystems/NetworkDebugSystem.Commands.cs
Tony Ferguson d85a25686c Network Diagnostic Additions (#4358)
* Can output network diagnostics to file in data/<game> folder, `net_diag_record 1` -> `net_diag_dump`
* Record Sync Vars
* Record per connection traffic
* Record incoming AND outgoing traffic
2026-03-20 16:59:51 +00:00

140 lines
5.1 KiB
C#

using System.Text;
namespace Sandbox;
sealed partial class NetworkDebugSystem
{
[ConCmd( "net_diag_dump", ConVarFlags.Protected )]
public static void NetworkDiag( string arg = "" )
{
var system = NetworkDebugSystem.Current;
if ( system is null )
{
Log.Warning( "NetworkDebugSystem is not active." );
return;
}
if ( arg.Equals( "reset", StringComparison.OrdinalIgnoreCase ) )
{
system.Reset();
Log.Info( "Network diagnostics stats have been reset." );
return;
}
if ( system.InboundStats is null && system.OutboundStats is null
&& system.SyncVarInboundStats is null && system.SyncVarOutboundStats is null )
{
Log.Info( "No network diagnostics data collected yet." );
return;
}
var timestamp = DateTime.Now.ToString( "yyyyMMdd_HHmmss" );
var elapsed = system.TrackingElapsed.TotalSeconds;
var sb = new StringBuilder();
sb.AppendLine( $"=== Network Diagnostics ({elapsed:0.0}s) ===" );
if ( system.OutboundStats is { Count: > 0 } )
{
sb.AppendLine();
sb.Append( BuildStatsTable( "RPC Outbound [to all connections]", system.OutboundStats, elapsed ) );
}
if ( system.InboundStats is { Count: > 0 } )
{
sb.AppendLine();
sb.Append( BuildStatsTable( "RPC Inbound [from all connections]", system.InboundStats, elapsed ) );
}
if ( system.ConnectionStats is { Count: > 0 } )
{
sb.AppendLine();
sb.Append( BuildConnectionTable( "RPC Inbound [per connection]", system.ConnectionStats, elapsed ) );
}
if ( system.SyncVarOutboundStats is { Count: > 0 } )
{
sb.AppendLine();
sb.Append( BuildStatsTable( "Sync Vars Outbound [all connections]", system.SyncVarOutboundStats, elapsed ) );
}
if ( system.SyncVarInboundStats is { Count: > 0 } )
{
sb.AppendLine();
sb.Append( BuildStatsTable( "Sync Vars Inbound [all connections]", system.SyncVarInboundStats, elapsed ) );
}
if ( system.SyncVarConnectionStats is { Count: > 0 } )
{
sb.AppendLine();
sb.Append( BuildConnectionTable( "Sync Vars Outbound [per connection]", system.SyncVarConnectionStats, elapsed ) );
}
var content = sb.ToString();
var filename = $"network_diag_{timestamp}.txt";
foreach ( var line in content.Split( '\n' ) ) Log.Info( line );
FileSystem.Data.WriteAllText( filename, content );
Log.Info( $"Written to {filename}" );
Log.Info( "Run 'network_diag reset' to clear accumulated stats." );
}
private static string BuildStatsTable( string label, Dictionary<string, NetworkDebugSystem.MessageStats> stats, double elapsedSeconds )
{
var sb = new StringBuilder();
var sorted = stats.OrderByDescending( kv => kv.Value.TotalBytes ).ToList();
var totalBytes = sorted.Sum( kv => kv.Value.TotalBytes );
var totalCalls = sorted.Sum( kv => kv.Value.TotalCalls );
var callRate = elapsedSeconds > 0 ? totalCalls / elapsedSeconds : 0;
var kbRate = elapsedSeconds > 0 ? totalBytes / 1024.0 / elapsedSeconds : 0;
sb.AppendLine( $"{label}: {sorted.Count} types, {totalCalls:N0} calls ({callRate:0.0}/s), {totalBytes / 1024f:0.0} KB ({kbRate:0.0} KB/s)" );
sb.AppendLine( $"\t{"#",-4} {"Method",-50} {"Calls/s",8} {"KB/s",8} {"Avg B",7} {"Peak B",7} {"% Total",9}" );
sb.AppendLine( $"\t{new string( '-', 4 )} {new string( '-', 50 )} {new string( '-', 8 )} {new string( '-', 8 )} {new string( '-', 7 )} {new string( '-', 7 )} {new string( '-', 9 )}" );
var rank = 0;
foreach ( var (name, stat) in sorted.Take( 30 ) )
{
rank++;
var pct = totalBytes > 0 ? stat.TotalBytes / (float)totalBytes * 100f : 0f;
var statCallRate = elapsedSeconds > 0 ? stat.TotalCalls / elapsedSeconds : 0;
var statKbRate = elapsedSeconds > 0 ? stat.TotalBytes / 1024.0 / elapsedSeconds : 0;
sb.AppendLine( $"\t{rank,-4} {name,-50} {statCallRate,7:0.0}/s {statKbRate,6:0.00} KB/s {stat.BytesPerMessage,6} B {stat.PeakBytes,6} B {pct,8:0.0}%" );
}
return sb.ToString();
}
private static string BuildConnectionTable( string label, Dictionary<Guid, Dictionary<string, NetworkDebugSystem.MessageStats>> connectionStats, double elapsedSeconds )
{
var sb = new StringBuilder();
sb.AppendLine( $"{label}:" );
var sorted = connectionStats
.Select( kv => (Id: kv.Key, Stats: kv.Value, TotalBytes: kv.Value.Values.Sum( s => s.TotalBytes )) )
.OrderByDescending( x => x.TotalBytes )
.ToList();
foreach ( var (id, stats, totalBytes) in sorted )
{
var conn = Connection.All.FirstOrDefault( c => c.Id == id );
var connName = conn?.DisplayName ?? id.ToString()[..8];
var totalCalls = stats.Values.Sum( s => s.TotalCalls );
var kbRate = elapsedSeconds > 0 ? totalBytes / 1024.0 / elapsedSeconds : 0;
sb.AppendLine( $"\t{connName,-30} {totalCalls,8:N0} calls {totalBytes / 1024f,8:0.0} KB {kbRate,6:0.00} KB/s" );
foreach ( var (name, stat) in stats.OrderByDescending( kv => kv.Value.TotalBytes ).Take( 5 ) )
{
var pct = totalBytes > 0 ? stat.TotalBytes / (float)totalBytes * 100f : 0f;
var parts = name.Split( '.' );
var shortName = parts.Length >= 2 ? $"{parts[^2]}.{parts[^1]}" : name;
sb.AppendLine( $"\t\t{shortName,-48} {stat.TotalCalls,6}x {stat.TotalBytes / 1024f,6:0.0} KB {pct,5:0.0}%" );
}
}
return sb.ToString();
}
}