diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 06f1875..b761245 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -409,6 +409,7 @@ public sealed class Plugin : IAsyncDalamudPlugin new SelfTests.HonorificHeaderRenderStep(this), new SelfTests.AboutIntegrationsStatusStep(this), new SelfTests.PerformanceBaselineStep(this), + new SelfTests.GlobalStyleScopeAllocStep(this), new SelfTests.MainWindowFocusOpacityStep(this), new SelfTests.MainWindowFlagsStep(this), new SelfTests.SenderNameReformatStep(this), diff --git a/HellionChat/SelfTests/GlobalStyleScopeAllocStep.cs b/HellionChat/SelfTests/GlobalStyleScopeAllocStep.cs new file mode 100644 index 0000000..06df8cf --- /dev/null +++ b/HellionChat/SelfTests/GlobalStyleScopeAllocStep.cs @@ -0,0 +1,61 @@ +using System; +using Dalamud.Bindings.ImGui; +using Dalamud.Plugin.SelfTest; +using HellionChat.Ui.StyleEngine; + +namespace HellionChat.SelfTests; + +// GC-reserve probe for the v1.9.0 B4a refactor: GlobalStyleScope.Push runs +// once per draw frame, so its StackHandle must allocate nothing. Before the +// counter rewrite it boxed 44 ImRaii structs + a List per frame; after, it +// holds two ints and pushes straight onto the ImGui stack. This step drives a +// real Push()->Dispose() cycle and asserts the per-thread allocation delta is +// ~0 (not a non-null-handle check — feedback_hellion_chat_fontmanager_push_trap). +// A warm-up cycle pays the one-time JIT/first-touch cost so the measured cycle +// reflects steady state, matching the real per-frame hot path. +internal sealed class GlobalStyleScopeAllocStep : ISelfTestStep +{ + // Headroom for incidental managed noise (GC bookkeeping, boxing inside + // ImGui bindings we do not control). The pre-fix path allocated ~1-2 KB + // per cycle (44 boxes + List), so anything under this threshold proves + // the StackHandle itself stopped allocating. Tighten only if a future + // binding upgrade removes all incidental noise. + private const long AllocBudgetBytes = 256; + + private readonly Plugin _plugin; + + public GlobalStyleScopeAllocStep(Plugin plugin) + { + _plugin = plugin; + } + + public string Name => "Hellion Chat - GlobalStyleScope GC reserve"; + + public SelfTestStepResult RunStep() + { + var registry = _plugin.ThemeRegistry; + var theme = registry.Active; + var opacity = Plugin.Config.WindowOpacity; + + // Warm-up: JIT the Push/Dispose path + first-touch any lazy ImGui + // stack growth, so the measured cycle is steady-state only. + GlobalStyleScope.Push(theme, registry, opacity).Dispose(); + + var before = GC.GetAllocatedBytesForCurrentThread(); + GlobalStyleScope.Push(theme, registry, opacity).Dispose(); + var delta = GC.GetAllocatedBytesForCurrentThread() - before; + + if (delta > AllocBudgetBytes) + { + ImGui.Text( + $"GlobalStyleScope.Push allocated {delta} bytes/cycle " + + $"(budget {AllocBudgetBytes}) — StackHandle is not GC-free." + ); + return SelfTestStepResult.Fail; + } + + return SelfTestStepResult.Pass; + } + + public void CleanUp() { } +}