From 73b51aa21a71a55d4becef09965b9441bcf0e90b Mon Sep 17 00:00:00 2001 From: Red Mushie Date: Wed, 1 Jul 2026 10:45:33 +0200 Subject: [PATCH 1/4] BUI performance metrics --- .../Systems/SharedUserInterfaceSystem.cs | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs index 7012d65c729..2743a402d6a 100644 --- a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs @@ -16,10 +16,15 @@ using Robust.Shared.Timing; using Robust.Shared.Utility; +using Prometheus; +using Robust.Shared.Configuration; +using Stopwatch = System.Diagnostics.Stopwatch; + namespace Robust.Shared.GameObjects; public abstract partial class SharedUserInterfaceSystem : EntitySystem { + [Dependency] private IConfigurationManager _cfg = default!; [Dependency] private IDynamicTypeFactory _factory = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private INetManager _netManager = default!; @@ -36,6 +41,45 @@ public abstract partial class SharedUserInterfaceSystem : EntitySystem private ActorRangeCheckJob _rangeJob; + #region Starlight + private static readonly Histogram UpdateHistogram = Metrics.CreateHistogram( + "robust_bui_system_update_usage", + "Time spent in Update per phase", + new HistogramConfiguration + { + LabelNames = new[] { "phase" }, + Buckets = Histogram.ExponentialBuckets(0.000_001, 1.5, 25) + }); + + private static readonly Histogram SetStateHistogram = Metrics.CreateHistogram( + "robust_bui_set_state_usage", + "Time spent in SetUiState per BUI type", + new HistogramConfiguration + { + LabelNames = new[] { "bui" }, + Buckets = Histogram.ExponentialBuckets(0.000_001, 1.5, 25) + }); + + private static readonly Histogram RangeCheckHistogram = Metrics.CreateHistogram( + "robust_bui_range_check_usage", + "Time spent in range checks per BUI key type", + new HistogramConfiguration + { + LabelNames = new[] { "bui" }, + Buckets = Histogram.ExponentialBuckets(0.000_001, 1.5, 25) + }); + + // Pre-allocated children for fixed phases; BUI types are cached on first use. + private static readonly Histogram.Child MonitorRangeQuery = UpdateHistogram.WithLabels("RangeQuery"); + private static readonly Histogram.Child MonitorRangeParallel = UpdateHistogram.WithLabels("RangeParallel"); + private static readonly Histogram.Child MonitorRangeClose = UpdateHistogram.WithLabels("RangeClose"); + private readonly Dictionary _setStateMonitors = new(); + private readonly Dictionary _rangeCheckMonitors = new(); + private readonly Stopwatch _stopwatch = new(); + + public bool MetricsEnabled { get; private set; } + #endregion Starlight + /// /// Defer BUIs during state handling so client doesn't spam a BUI constantly during prediction. /// @@ -45,6 +89,11 @@ public override void Initialize() { base.Initialize(); + // Starlight BEGIN + if (_netManager.IsServer) + Subs.CVar(_cfg, CVars.MetricsEnabled, v => MetricsEnabled = v, true); + // Starlight END + EntityManager.ComponentFactory.RegisterNetworkedFields( nameof(UserInterfaceComponent.Actors), nameof(UserInterfaceComponent.Interfaces), @@ -733,6 +782,17 @@ public void SetUiState(Entity entity, Enum key, BoundUs if (!entity.Comp.Interfaces.ContainsKey(key)) return; + // Starlight BEGIN + Histogram.Child? monitor = null; + if (MetricsEnabled && state != null) + { + var type = state.GetType(); + if (!_setStateMonitors.TryGetValue(type, out monitor)) + _setStateMonitors[type] = monitor = SetStateHistogram.WithLabels(type.Name); + _stopwatch.Restart(); + } + // Starlight END + // Null state if (state == null) { @@ -763,6 +823,7 @@ public void SetUiState(Entity entity, Enum key, BoundUs } DirtyField(entity, nameof(UserInterfaceComponent.States)); + monitor?.Observe(_stopwatch.Elapsed.TotalSeconds); // Starlight } /// @@ -1161,6 +1222,11 @@ public override void Update(float frameTime) _queuedBuis.Clear(); } + // Starlight BEGIN + if (MetricsEnabled) + _stopwatch.Restart(); + // Starlight END + var query = AllEntityQuery(); // Run these in parallel because it's expensive. _rangeJob.ActorRanges.Clear(); @@ -1185,13 +1251,39 @@ public override void Update(float frameTime) if (_netManager.IsClient && !actor.IsValid()) continue; // Client might not have received the entity. Server should log errors. + // Starlight BEGIN + if (MetricsEnabled) + { + var keyType = key.GetType(); + if (!_rangeCheckMonitors.ContainsKey(keyType)) + _rangeCheckMonitors[keyType] = RangeCheckHistogram.WithLabels(keyType.Name); + } + // Starlight END + _rangeJob.ActorRanges.Add((uid, key, data, actor, false)); } } } + // Starlight BEGIN + _rangeJob.RangeMonitors = MetricsEnabled ? _rangeCheckMonitors : null; + if (MetricsEnabled) + { + MonitorRangeQuery.Observe(_stopwatch.Elapsed.TotalSeconds); + _stopwatch.Restart(); + } + // Starlight END + _parallel.ProcessNow(_rangeJob, _rangeJob.ActorRanges.Count); + // Starlight BEGIN + if (MetricsEnabled) + { + MonitorRangeParallel.Observe(_stopwatch.Elapsed.TotalSeconds); + _stopwatch.Restart(); + } + // Starlight END + foreach (var data in _rangeJob.ActorRanges) { var uid = data.Ui; @@ -1203,6 +1295,11 @@ public override void Update(float frameTime) CloseUi((uid, uiComp), key, actor); } + + // Starlight BEGIN + if (MetricsEnabled) + MonitorRangeClose.Observe(_stopwatch.Elapsed.TotalSeconds); + // Starlight END } /// @@ -1268,6 +1365,7 @@ private record struct ActorRangeCheckJob() : IParallelRobustJob { public required EntityQuery XformQuery; public required SharedUserInterfaceSystem System; + public Dictionary? RangeMonitors; // Starlight: Monitoring public readonly List<(EntityUid Ui, Enum Key, InterfaceData Data, EntityUid Actor, bool Result)> ActorRanges = new(); public void Execute(int index) @@ -1281,7 +1379,10 @@ public void Execute(int index) } else { + var start = Stopwatch.GetTimestamp(); // Starlight data.Result = System.CheckRange((data.Ui, uiXform), data.Key, data.Data, (data.Actor, actorXform)); + if (RangeMonitors != null && RangeMonitors.TryGetValue(data.Key.GetType(), out var monitor)) // Starlight + monitor.Observe(Stopwatch.GetElapsedTime(start).TotalSeconds); // Starlight } ActorRanges[index] = data; From 70809262185766e63c7bd4131fab9c3b960474f4 Mon Sep 17 00:00:00 2001 From: Red Mushie Date: Wed, 1 Jul 2026 12:21:02 +0200 Subject: [PATCH 2/4] More touchups --- .../GameObjects/Systems/SharedUserInterfaceSystem.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs index 2743a402d6a..916fb54716e 100644 --- a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs @@ -16,15 +16,17 @@ using Robust.Shared.Timing; using Robust.Shared.Utility; +#region Starlight using Prometheus; using Robust.Shared.Configuration; using Stopwatch = System.Diagnostics.Stopwatch; +#endregion namespace Robust.Shared.GameObjects; public abstract partial class SharedUserInterfaceSystem : EntitySystem { - [Dependency] private IConfigurationManager _cfg = default!; + [Dependency] private IConfigurationManager _cfg = default!; // Starlight [Dependency] private IDynamicTypeFactory _factory = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private INetManager _netManager = default!; @@ -43,7 +45,7 @@ public abstract partial class SharedUserInterfaceSystem : EntitySystem #region Starlight private static readonly Histogram UpdateHistogram = Metrics.CreateHistogram( - "robust_bui_system_update_usage", + "robust_bui_update_usage", "Time spent in Update per phase", new HistogramConfiguration { @@ -69,12 +71,15 @@ public abstract partial class SharedUserInterfaceSystem : EntitySystem Buckets = Histogram.ExponentialBuckets(0.000_001, 1.5, 25) }); - // Pre-allocated children for fixed phases; BUI types are cached on first use. + // The three 'phases' of the Update method. private static readonly Histogram.Child MonitorRangeQuery = UpdateHistogram.WithLabels("RangeQuery"); private static readonly Histogram.Child MonitorRangeParallel = UpdateHistogram.WithLabels("RangeParallel"); private static readonly Histogram.Child MonitorRangeClose = UpdateHistogram.WithLabels("RangeClose"); + + // Reused histogram children per BUI / BUI key type. private readonly Dictionary _setStateMonitors = new(); private readonly Dictionary _rangeCheckMonitors = new(); + private readonly Stopwatch _stopwatch = new(); public bool MetricsEnabled { get; private set; } From 7a879949978768d9851bec082a6467acd76f565d Mon Sep 17 00:00:00 2001 From: Red Mushie Date: Wed, 1 Jul 2026 13:44:26 +0200 Subject: [PATCH 3/4] Add UserInterfaceSystem granular metrics CVar, default disabled --- Robust.Shared/CVars.cs | 9 +++++++++ .../GameObjects/Systems/SharedUserInterfaceSystem.cs | 10 ++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index a987bf0c8f7..d1f03c3fedd 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -634,6 +634,15 @@ protected CVars() public static readonly CVarDef MetricsRuntimeSocket = CVarDef.Create("metrics.runtime_socket", "Counters", CVar.SERVERONLY); + #region Starlight + /// + /// Whether to enable granular metrics in the . + /// This adds various + /// + public static readonly CVarDef MetricsGranularUiSystem = + CVarDef.Create("metrics.granular_ui_system", false, CVar.SERVERONLY); + #endregion + /* * STATUS */ diff --git a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs index 916fb54716e..21c1492d5e0 100644 --- a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs @@ -82,7 +82,10 @@ public abstract partial class SharedUserInterfaceSystem : EntitySystem private readonly Stopwatch _stopwatch = new(); - public bool MetricsEnabled { get; private set; } + private bool _globalMetricsEnabled; + private bool _granularUiSystemMetricsEnabled; + private bool MetricsEnabled => _globalMetricsEnabled && _granularUiSystemMetricsEnabled; + #endregion Starlight /// @@ -96,7 +99,10 @@ public override void Initialize() // Starlight BEGIN if (_netManager.IsServer) - Subs.CVar(_cfg, CVars.MetricsEnabled, v => MetricsEnabled = v, true); + { + Subs.CVar(_cfg, CVars.MetricsEnabled, v => _globalMetricsEnabled = v, true); + Subs.CVar(_cfg, CVars.MetricsGranularUiSystem, v => _granularUiSystemMetricsEnabled = v, true); + } // Starlight END EntityManager.ComponentFactory.RegisterNetworkedFields( From 332a6e64e9091aac8ccecfa13083b0a0259bd77e Mon Sep 17 00:00:00 2001 From: Red Mushie Date: Wed, 1 Jul 2026 13:47:45 +0200 Subject: [PATCH 4/4] Remove half sentence --- Robust.Shared/CVars.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index d1f03c3fedd..77d1a5114ca 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -637,7 +637,6 @@ protected CVars() #region Starlight /// /// Whether to enable granular metrics in the . - /// This adds various /// public static readonly CVarDef MetricsGranularUiSystem = CVarDef.Create("metrics.granular_ui_system", false, CVar.SERVERONLY);