using Dalamud.Bindings.ImGui; using Dalamud.Plugin.SelfTest; namespace HellionChat.SelfTests; // Optional metric capture. Accumulates 1000 steady-state frames of ImGui IO // counters plus the plugin's full-Draw wall-time (Plugin.LastDrawMs), // then writes a single perf-baseline.json into the plugin ConfigDirectory so // the cycle-notes author can copy the baseline figures without a separate // profiling harness. The step only records — it never fails on a threshold // (the budgets are evaluated by a human against the JSON ("optional, // manual"). It returns Waiting until the sample window fills, mirroring the // per-frame poll idiom of ThemeSwitchSelfTestStep. internal sealed class PerformanceBaselineStep : ISelfTestStep { // Steady-state window. 1000 frames ≈ 16s at 60fps, long enough to // average out GC blips without making the manual step tedious. private const int TargetFrames = 1000; // Rough draw-call proxy: ImGui emits 6 indices per quad, so vertices/6 is an // intentional under-count of draw work, not the exact quad count. private const int VerticesPerQuadProxy = 6; private readonly Plugin _plugin; private int _frames; private ulong _lastFrameCount; private double _drawMsSum; private double _drawMsMax; private long _vertexSum; private long _vertexMax; private double _deltaMsSum; private double _deltaMsMax; private string? _logPath; public PerformanceBaselineStep(Plugin plugin) { _plugin = plugin; } public string Name => "Hellion Chat - Performance baseline capture"; public SelfTestStepResult RunStep() { var io = ImGui.GetIO(); // Count each real frame once. Without the FrameCount gate a step that // is polled more than once per frame would inflate the sample count. var frameCount = Plugin.Interface.UiBuilder.FrameCount; if (frameCount != _lastFrameCount) { _lastFrameCount = frameCount; _frames++; var drawMs = _plugin.LastDrawMs; _drawMsSum += drawMs; if (drawMs > _drawMsMax) _drawMsMax = drawMs; long vertices = io.MetricsRenderVertices; _vertexSum += vertices; if (vertices > _vertexMax) _vertexMax = vertices; var deltaMs = io.DeltaTime * 1000f; _deltaMsSum += deltaMs; if (deltaMs > _deltaMsMax) _deltaMsMax = deltaMs; } if (_frames < TargetFrames) { ImGui.Text( $"Sampling steady-state… {_frames}/{TargetFrames} frames. " + "Keep the chat window visible and idle." ); return SelfTestStepResult.Waiting; } _logPath ??= WriteBaselineLog(); ImGui.Text($"Baseline captured ({TargetFrames} frames). Wrote: {_logPath}"); return SelfTestStepResult.Pass; } public void CleanUp() { _frames = 0; _lastFrameCount = 0; _drawMsSum = 0; _drawMsMax = 0; _vertexSum = 0; _vertexMax = 0; _deltaMsSum = 0; _deltaMsMax = 0; _logPath = null; } private string WriteBaselineLog() { // Disk write happens here, never in the per-frame hot path. return PerformanceBaselineLog.Write( avgDrawMs: _drawMsSum / TargetFrames, maxDrawMs: _drawMsMax, avgDrawCallsProxy: _vertexSum / (double)TargetFrames / VerticesPerQuadProxy, maxDrawCallsProxy: _vertexMax / (double)VerticesPerQuadProxy, avgDeltaMs: _deltaMsSum / TargetFrames, maxDeltaMs: _deltaMsMax, frames: TargetFrames ); } }