From 011490368ba84b41382300f37f3aa066fc142915 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Fri, 15 May 2026 10:14:13 +0200 Subject: [PATCH] perf(draw): defer non-essential first-frame rendering (v1.4.9 R2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cut first-frame HITCH from ~127ms median down to ~76ms median (4-reload sample, threshold lowered to 1ms for measurement) — comfortably under Dalamud's 100ms warning threshold. ChatTwo upstream sits at ~63ms median for comparison; the remaining ~13ms gap is the cost of HellionChat-only features (Sidebar tab view, custom StatusBar, Honorific integration). Mechanism: a single `_firstFrameDone` flag (flipped in Draw's finally block) gates six sections that don't need to render on frame 0: - StatusBar.Draw (~12ms): the bottom status bar - DrawChannelName chunks (~17ms): SeString-Renderer layout, replaced with a plain-text fallback (activeTab.Name) for frame 0 - PositionReset/BoundsCheck (~10ms): EnsureWindowOnScreen viewport iteration, only matters once the user notices a mispositioned window - DrawV061HintBannerIfNeeded (~3-5ms): v0.6.1 migration notice - DrawAutoComplete (~6ms): renders nothing until the user types a command - InputPreview.CalculatePreview (~3-5ms): triggers InputPreview first- frame lazy init, user-typing-driven anyway Frame 1 then renders all of them in ~40ms (still well under the warning threshold), and frames 2+ stay at 0ms as before. User sees the deferred sections ~17ms (60fps) later than before — invisible inside the ~2.5s Atlas-Build window after every plugin reload. Hypothesis triage from the R2-profiling pass: - (a) Atlas-Sync-Fallback: falsified. xllog shows the Atlas-Complete line always lands ~2.5s before the HITCH frame. - (b) Theme-Apply ABGR-Cache-Init: not dominant. PushGlobal is 5ms. - (c) Multiple-Window-Render: falsified in v1.4.9 Stage-2-Lazy-Init diagnose (deferred 4 windows, no measurable delta). - (d) DrawList-Setup-Cost per Window: actual root cause. Layout cost distributes evenly across ~10 ImGui sections inside ChatLogWindow (5-20ms each). No single hot-spot to optimise — the six selective skips above are the pragmatic fix. Co-Authored-By: Claude Opus 4.7 (1M context) --- HellionChat/Ui/ChatLogWindow.cs | 76 ++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 367a9ca..a240da9 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -636,6 +636,15 @@ public sealed class ChatLogWindow : Window IsOpen = true; } + // v1.4.9 R2: defer non-essential rendering on the first Draw call so the + // plugin-load stays under Dalamud's 100ms HITCH warning threshold. First- + // frame ImGui layout cost on a populated ChatLog ~127ms — deferring six + // non-essential sections (StatusBar, ChannelName chunks, PositionReset/ + // BoundsCheck, HintBanner, AutoComplete, InputPreview.CalculatePreview) + // shaves ~33ms down to ~94ms. User sees the deferred sections one frame + // (~17ms at 60fps) late, invisible inside the post-reload Atlas-Build. + private bool _firstFrameDone; + public override void Draw() { DrewThisFrame = true; @@ -643,7 +652,11 @@ public sealed class ChatLogWindow : Window { DrawChatLog(); AddPopOutsToDraw(); - DrawAutoComplete(); + + // v1.4.9 R2: AutoComplete renders nothing until the user starts + // typing a command — safe to skip on the first frame. ~6ms. + if (_firstFrameDone) + DrawAutoComplete(); } catch (Exception ex) { @@ -665,6 +678,13 @@ public sealed class ChatLogWindow : Window // input focus, which breaks every other ImGui window. Activate = false; } + finally + { + // Flag flips after the first Draw completes (success or caught + // exception). Sub-methods read it to decide whether to render + // non-essential UI sections. + _firstFrameDone = true; + } } private static bool IsChatMode => @@ -680,18 +700,25 @@ public sealed class ChatLogWindow : Window LastWindowSize = currentSize; LastWindowPos = ImGui.GetWindowPos(); - // Manual reset snaps unconditionally; on-load check only fires when the - // stored position has no overlap with any visible viewport. - if (RequestPositionReset) + // v1.4.9 R2: skip the bounds-check chain on the first frame. The + // EnsureWindowOnScreen viewport iteration is ~10ms first-frame and + // not user-visible — frame 1 catches the same check before the + // user notices a mispositioned window. + if (_firstFrameDone) { - RequestPositionReset = false; - DidOnLoadBoundsCheck = true; - ApplySafeDefaultPosition("manual-reset"); - } - else if (!DidOnLoadBoundsCheck) - { - DidOnLoadBoundsCheck = true; - EnsureWindowOnScreen("on-load"); + // Manual reset snaps unconditionally; on-load check only fires when the + // stored position has no overlap with any visible viewport. + if (RequestPositionReset) + { + RequestPositionReset = false; + DidOnLoadBoundsCheck = true; + ApplySafeDefaultPosition("manual-reset"); + } + else if (!DidOnLoadBoundsCheck) + { + DidOnLoadBoundsCheck = true; + EnsureWindowOnScreen("on-load"); + } } if (resized) @@ -700,12 +727,17 @@ public sealed class ChatLogWindow : Window LastViewport = ImGui.GetWindowViewport().Handle; WasDocked = ImGui.IsWindowDocked(); - if (IsChatMode && Plugin.InputPreview.IsDrawable) + // v1.4.9 R2: CalculatePreview triggers InputPreview's first-frame + // lazy init (~3-5ms). User-typing-driven, safe to defer one frame. + if (_firstFrameDone && IsChatMode && Plugin.InputPreview.IsDrawable) Plugin.InputPreview.CalculatePreview(); // Render the hint banner first so it sits above the tab area at full // window width. ImGui accounts for its height automatically. - DrawV061HintBannerIfNeeded(); + // v1.4.9 R2: skip on first frame (~3-5ms layout cost). The banner + // is a v0.6.1 migration notice that returns the same result frame 1. + if (_firstFrameDone) + DrawV061HintBannerIfNeeded(); if (Plugin.Config.SidebarTabView) DrawTabSidebar(); @@ -938,7 +970,11 @@ public sealed class ChatLogWindow : Window // v1.2.0 — Bottom-Status-Bar. Letzter Render-Step in DrawChatLog, // damit alle Zeilen-Operationen davor keine Layout-Sprünge auslösen. - Plugin.StatusBar.Draw(Plugin); + // v1.4.9 R2: skip on the first frame; ~12ms of first-frame layout + // cost. User sees the StatusBar 1 frame (~17ms at 60fps) later + // which is hidden inside the post-reload Atlas-Build window. + if (_firstFrameDone) + Plugin.StatusBar.Draw(Plugin); } internal Dictionary GetValidChannels() @@ -989,6 +1025,16 @@ public sealed class ChatLogWindow : Window private void DrawChannelName(Tab activeTab) { + // v1.4.9 R2: plain-text fallback on the first frame. ReadChannelName + // builds SeString chunks and DrawChunks runs SeString-Renderer layout + // — together ~18ms first-frame. Frame 1 renders the real chunks; the + // user sees the tab name for ~17ms during the post-reload window. + if (!_firstFrameDone) + { + ImGui.TextUnformatted(activeTab.Name); + return; + } + var currentChannel = ReadChannelName(activeTab); if (!currentChannel.SequenceEqual(PreviousChannel)) PreviousChannel = currentChannel;