From cc3d703d0ad482da3dcd77b0f327478d37855a8c Mon Sep 17 00:00:00 2001 From: makadore Date: Tue, 25 Aug 2026 17:02:47 +0000 Subject: [PATCH] perf: stop rebuilding the center html panel event on every tick Display is registered as an OnTick listener and the panel only stays on screen while it is being resent, so an open menu goes through this path 64 times a second per player. PrintToCenterHtml creates a game event, fires it and frees it, and doing that each time is most of what an open menu costs. The event is kept for as long as the menu is and refilled with the new markup instead, then freed in Close. Measured on a live server with a 12 item menu by alternating the two paths on every other render, so both meet the same conditions: 36.57us over 2466 sends against 27.54us over 2466. The labels and the builder go with it: AddPageOptions asks the localizer for Prev, Exit and Next unconditionally on every render, even when the buttons are not shown, and each of those runs ReplaceColorTags, which reflects over ChatColors. That is about 1us of the 37, so it is the small half of this, but none of it changes while the menu is open. Colours are still read per render, since a plugin can change them after the menu is built. Sending now returns early on an invalid player rather than throwing the way PrintToCenterHtml does, which suits something that runs every tick; Close already guarded the same way. --- CS2MenuManager/API/Menu/CenterHtmlMenu.cs | 38 ++++++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/CS2MenuManager/API/Menu/CenterHtmlMenu.cs b/CS2MenuManager/API/Menu/CenterHtmlMenu.cs index 643c5ba..194ef86 100644 --- a/CS2MenuManager/API/Menu/CenterHtmlMenu.cs +++ b/CS2MenuManager/API/Menu/CenterHtmlMenu.cs @@ -50,6 +50,14 @@ public class CenterHtmlMenuInstance : BaseMenuInstance /// public override int NumPerPage => 5; + // Display runs on every tick; each of these walks the localization stack and + // ReplaceColorTags reflects over ChatColors, none of which changes here. + private readonly string _prevLabel; + private readonly string _exitLabel; + private readonly string _nextLabel; + private readonly StringBuilder _builder = new(); + private CounterStrikeSharp.API.Core.EventShowSurvivalRespawnStatus? _panelEvent; + /// /// Gets the number of items displayed per page. /// @@ -70,6 +78,10 @@ public CenterHtmlMenuInstance(CCSPlayerController player, IMenu menu) : base(pla if (Menu is CenterHtmlMenu { CenterHtmlMenu_MaxOptionLength: > 0 } centerHtmlMenu) Menu.ItemOptions.ForEach(option => option.Text = option.Text.TruncateHtml(centerHtmlMenu.CenterHtmlMenu_MaxOptionLength)); + _prevLabel = Player.Localizer("Prev"); + _exitLabel = Player.Localizer("Exit"); + _nextLabel = Player.Localizer("Next"); + Menu.Plugin.RegisterListener(Display); } @@ -81,7 +93,8 @@ public override void Display() if (Menu is not CenterHtmlMenu centerHtmlMenu) return; - StringBuilder builder = new(); + StringBuilder builder = _builder; + builder.Clear(); builder.Append($"{centerHtmlMenu.Title}
"); int keyOffset = 1; @@ -102,7 +115,7 @@ public override void Display() } AddPageOptions(centerHtmlMenu, builder); - Player.PrintToCenterHtml(builder.ToString()); + SendPanel(builder.ToString()); } /// @@ -113,15 +126,30 @@ public override void Close(bool exitSound) base.Close(exitSound); Menu.Plugin.RemoveListener(Display); + _panelEvent?.Free(); + _panelEvent = null; + if (Player.IsValid) Player.PrintToCenterHtml(" "); } + // The panel only stays up while it is resent, so a game event is fired every + // tick; building and freeing one each time is most of what that costs. + private void SendPanel(string text) + { + if (!Player.IsValid) + return; + + _panelEvent ??= new CounterStrikeSharp.API.Core.EventShowSurvivalRespawnStatus(true) { Userid = Player, Duration = 5 }; + _panelEvent.LocToken = text; + _panelEvent.FireEventToClient(Player); + } + private void AddPageOptions(CenterHtmlMenu centerHtmlMenu, StringBuilder builder) { - string prevText = $"!8 < {Player.Localizer("Prev")}"; - string closeText = $"!0 X {Player.Localizer("Exit")}"; - string nextText = $"!9 > {Player.Localizer("Next")}"; + string prevText = $"!8 < {_prevLabel}"; + string closeText = $"!0 X {_exitLabel}"; + string nextText = $"!9 > {_nextLabel}"; if (centerHtmlMenu.CenterHtmlMenu_InlinePageOptions) AddInlinePageOptions(prevText, closeText, nextText, builder);