Files
RobustToolbox/Robust.Client/GameStates/NetGraphOverlay.cs
T
83c2a1be11 [Dependency] source generator part 2 (#6550)
* [Dependency] source generator

No more reflection, no more codegen at runtime

Also various changes to Roslyn helpers to make this easier to write.

Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback.

No fallback is available for [field: Dependency] properties, due to a Roslyn bug.

Code Fixes exist. We love Roslyn

* Apply dependencies generator changes to all code

* Release notes

* Preprocessor got hands

* Handle nullable dependencies

These are bad but gotta deal with it.

* Apply suggestions from code review

Co-authored-by: Moony <moony@hellomouse.net>

* Fine, let's not use collection expressions

---------

Co-authored-by: Moony <moony@hellomouse.net>
2026-05-08 12:38:33 +02:00

335 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Client.Player;
using Robust.Shared.Log;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
/// <summary>
/// Visual debug overlay for the network diagnostic graph.
/// </summary>
internal sealed partial class NetGraphOverlay : Overlay
{
[Dependency] private IGameTiming _gameTiming = default!;
[Dependency] private IClientNetManager _netManager = default!;
[Dependency] private IClientGameStateManager _gameStateManager = default!;
[Dependency] private IComponentFactory _componentFactory = default!;
[Dependency] private IConsoleHost _host = default!;
[Dependency] private IEntityManager _entManager = default!;
private const int HistorySize = 60 * 5; // number of ticks to keep in history.
private const int TargetPayloadBps = 56000 / 8; // Target Payload size in Bytes per second. A mind-numbing fifty-six thousand bits per second, who would ever need more?
private const int ExtremePayloadBps = 256000 / 8; // I know this is somewhat extreme, but I'm gonna make the graph go up to 256kbs. That's a whopping 32 KILO bytes per second.
private const int MidrangePayloadBps = 19200 / 8; // mid-range line
private const int BaselineBps = 8192 / 8; // mid-range line
private const int LowerGraphOffset = 100; // Offset on the Y axis in pixels of the lower lag/interp graph.
private const int LeftMargin = 500; // X offset, to avoid interfering with the f3 menu.
private const int MsPerPixel = 4; // Latency Milliseconds per pixel, for scaling the graph.
/// <inheritdoc />
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
private readonly Font _font;
private readonly List<(GameTick Tick, int Payload, int lag, int Buffer)> _history = new(HistorySize + 10);
// sum of all data point sizes in bytes
private int _totalHistoryPayload;
public EntityUid WatchEntId { get; set; }
public NetGraphOverlay()
{
IoCManager.InjectDependencies(this);
var cache = IoCManager.Resolve<IResourceCache>();
_font = new VectorFont(cache.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 10);
_gameStateManager.GameStateApplied += HandleGameStateApplied;
}
private void HandleGameStateApplied(GameStateAppliedArgs args)
{
var toSeq = args.AppliedState.ToSequence;
var sz = args.AppliedState.PayloadSize;
// calc lag
var lag = _netManager.ServerChannel!.Ping;
// calc interp info
var buffer = _gameStateManager.GetApplicableStateCount();
_totalHistoryPayload += sz;
_history.Add((toSeq, sz, lag, buffer));
// not watching an ent
if (!WatchEntId.IsValid() || _entManager.IsClientSide(WatchEntId))
return;
string? entStateString = null;
string? entDelString = null;
var conShell = _host.LocalShell;
var entStates = args.AppliedState.EntityStates;
if (entStates.HasContents)
{
var sb = new StringBuilder();
foreach (var entState in entStates.Span)
{
var uid = _entManager.GetEntity(entState.NetEntity);
if (uid != WatchEntId)
continue;
if (!entState.ComponentChanges.HasContents)
{
sb.Append("\n Entered PVS");
break;
}
sb.Append($"\n Changes:");
foreach (var compChange in entState.ComponentChanges.Span)
{
var registration = _componentFactory.GetRegistration(compChange.NetID);
sb.Append($"\n [{compChange.NetID}:{registration.Name}");
if (compChange.State is not null)
sb.Append($"\n STATE:{compChange.State.GetType().Name}");
}
// Note that component deletion is now implicit via the list of network comp ids. So it currently
// doesn't get logged here.
break;
}
entStateString = sb.ToString();
}
foreach (var ent in args.Detached)
{
var uid = _entManager.GetEntity(ent);
if (uid != WatchEntId)
continue;
conShell.WriteLine($"watchEnt: Left PVS at tick {args.AppliedState.ToSequence}, eid={WatchEntId}" + "\n");
}
var entDeletes = args.AppliedState.EntityDeletions;
if (entDeletes.HasContents)
{
foreach (var entDelete in entDeletes.Span)
{
var uid = _entManager.GetEntity(entDelete);
if (uid == WatchEntId)
entDelString = "\n Deleted";
}
}
if (!string.IsNullOrWhiteSpace(entStateString) || !string.IsNullOrWhiteSpace(entDelString))
{
var fullString = $"watchEnt: from={args.AppliedState.FromSequence}, to={args.AppliedState.ToSequence}, eid={WatchEntId}";
if (!string.IsNullOrWhiteSpace(entStateString))
fullString += entStateString;
if (!string.IsNullOrWhiteSpace(entDelString))
fullString += entDelString;
conShell.WriteLine(fullString + "\n");
}
}
/// <inheritdoc />
protected internal override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
var over = _history.Count - HistorySize;
if (over <= 0)
return;
for (int i = 0; i < over; i++)
{
var point = _history[i];
_totalHistoryPayload -= point.Payload;
}
_history.RemoveRange(0, over);
}
private static int BytesToPixels(int bytes)
{
// the minimum size seems to be about 10 bytes.
// 10 bytes = 15 pixel, then doubling every 15 more pixels.
bytes = Math.Max(bytes - 10, 1);
return (int)Math.Round((1 + Math.Log2(bytes)) * 15);
}
protected internal override void Draw(in OverlayDrawArgs args)
{
// remember, 0,0 is top left of ui with +X right and +Y down
var highPayload = BytesToPixels(ExtremePayloadBps / _gameTiming.TickRate);
var targetPayload = BytesToPixels(TargetPayloadBps / _gameTiming.TickRate);
var midPayload = BytesToPixels(MidrangePayloadBps / _gameTiming.TickRate);
var minPayload = BytesToPixels(BaselineBps / _gameTiming.TickRate);
var width = HistorySize;
var height = 500;
var handle = args.ScreenHandle;
// bottom payload line
handle.DrawLine(new Vector2(LeftMargin, height), new Vector2(LeftMargin + width, height), Color.DarkGray.WithAlpha(0.8f));
// bottom lag line
handle.DrawLine(new Vector2(LeftMargin, height + LowerGraphOffset), new Vector2(LeftMargin + width, height + LowerGraphOffset), Color.DarkGray.WithAlpha(0.8f));
int lastLagY = -1;
int lastLagMs = -1;
// data points
for (var i = 0; i < _history.Count; i++)
{
var state = _history[i];
// draw the uncompressed size
var xOff = LeftMargin + i;
var yoff = height - BytesToPixels(state.Payload);
handle.DrawLine(new Vector2(xOff, height), new Vector2(xOff, yoff), Color.LimeGreen.WithAlpha(0.8f));
// second tick marks
if (state.Tick.Value % (_gameTiming.TickRate / 5) == 0)
{
handle.DrawLine(new Vector2(xOff, height), new Vector2(xOff, height + 2), Color.LightGray);
}
// lag data
var lagYoff = height + LowerGraphOffset - state.lag / MsPerPixel;
lastLagY = lagYoff - 1;
lastLagMs = state.lag;
handle.DrawLine(new Vector2(xOff, lagYoff - 2), new Vector2(xOff, lagYoff - 1), Color.Blue.WithAlpha(0.8f));
// interp data
Color interpColor;
if (state.Buffer < _gameStateManager.MinBufferSize)
interpColor = Color.Red;
else if (state.Buffer < _gameStateManager.TargetBufferSize)
interpColor = Color.Yellow;
else
interpColor = Color.Green;
var delta = (state.Buffer - _gameStateManager.MinBufferSize);
handle.DrawLine(new Vector2(xOff, height + LowerGraphOffset), new Vector2(xOff, height + LowerGraphOffset + delta * 6), interpColor.WithAlpha(0.8f));
}
// average payload line
var avg = height - BytesToPixels(_totalHistoryPayload / HistorySize);
var avgEnd = new Vector2(LeftMargin + width, avg) + new Vector2(70, 0);
handle.DrawLine(new Vector2(LeftMargin, avg), avgEnd, Color.DarkGray.WithAlpha(0.8f));
// top payload warning line
var warnYoff = height - targetPayload;
handle.DrawLine(new Vector2(LeftMargin, warnYoff), new Vector2(LeftMargin + width, warnYoff), Color.DarkGray.WithAlpha(0.8f));
// Extreme payload hazard line -- their modem will probably explode at this point.
var extremeYoff = height - highPayload;
handle.DrawLine(new Vector2(LeftMargin, warnYoff), new Vector2(LeftMargin + width, warnYoff), Color.DarkGray.WithAlpha(0.8f));
// mid payload line
var midYoff = height - midPayload;
handle.DrawLine(new Vector2(LeftMargin, midYoff), new Vector2(LeftMargin + width, midYoff), Color.DarkGray.WithAlpha(0.8f));
var minYoff = height - minPayload;
handle.DrawLine(new Vector2(LeftMargin, minYoff), new Vector2(LeftMargin + width, minYoff), Color.DarkGray.WithAlpha(0.8f));
// payload text
handle.DrawString(_font, new Vector2(LeftMargin + width, extremeYoff), "128Kbit/s");
handle.DrawString(_font, new Vector2(LeftMargin + width, warnYoff), "56Kbit/s");
handle.DrawString(_font, new Vector2(LeftMargin + width, midYoff), "33.6Kbit/s");
handle.DrawString(_font, new Vector2(LeftMargin + width, minYoff), "8.19Kbit/s");
// avg text
var lineHeight = _font.GetLineHeight(1) / 2f;
handle.DrawString(_font, avgEnd - new Vector2(lineHeight, lineHeight), "average");
// lag text info
if (lastLagY != -1)
handle.DrawString(_font, new Vector2(LeftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
// buffer text
handle.DrawString(_font, new Vector2(LeftMargin, height + LowerGraphOffset), $"{_gameStateManager.GetApplicableStateCount().ToString()} states");
}
protected override void DisposeBehavior()
{
_gameStateManager.GameStateApplied -= HandleGameStateApplied;
base.DisposeBehavior();
}
private sealed class NetShowGraphCommand : LocalizedCommands
{
public override string Command => "net_graph";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if (!overlayMan.HasOverlay(typeof(NetGraphOverlay)))
{
overlayMan.AddOverlay(new NetGraphOverlay());
shell.WriteLine("Enabled network overlay.");
}
else
{
overlayMan.RemoveOverlay(typeof(NetGraphOverlay));
shell.WriteLine("Disabled network overlay.");
}
}
}
private sealed partial class NetWatchEntCommand : LocalizedCommands
{
[Dependency] private IEntityManager _entManager = default!;
[Dependency] private IOverlayManager _overlayManager = default!;
[Dependency] private IPlayerManager _playerManager = default!;
public override string Command => "net_watchent";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntityUid? entity;
if (args.Length == 0)
{
entity = _playerManager.LocalEntity ?? EntityUid.Invalid;
}
else if (!NetEntity.TryParse(args[0], out var netEntity) || !_entManager.TryGetEntity(netEntity, out entity))
{
shell.WriteError("Invalid argument: Needs to be 0 or an entityId.");
return;
}
if (!_overlayManager.TryGetOverlay(out NetGraphOverlay? overlay))
{
overlay = new NetGraphOverlay();
_overlayManager.AddOverlay(overlay);
}
overlay.WatchEntId = entity.Value;
}
}
}
}