perf(baseline): 1000-frame steady-state capture with quad-proxy draw calls + JSON sink
This commit is contained in:
@@ -0,0 +1,68 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace HellionChat.SelfTests;
|
||||||
|
|
||||||
|
// Disk sink for the B5 performance baseline. Kept separate from the SelfTest
|
||||||
|
// step so the per-frame hot path never references file IO. Writes one
|
||||||
|
// perf-baseline.json into the plugin ConfigDirectory, atomically (tmp + move)
|
||||||
|
// like ThemeRegistry's theme writer, so a mid-write crash leaves either the
|
||||||
|
// old file or the new file, never a half JSON. Field names track §7.5:
|
||||||
|
// steady-state Draw cost (avg/max ms), the quad-proxy draw-call count
|
||||||
|
// (avg/max), and frame delta (avg/max). First-frame-HITCH is read off
|
||||||
|
// drawMs max/avg by the human author, platform-annotated in the notes.
|
||||||
|
internal static class PerformanceBaselineLog
|
||||||
|
{
|
||||||
|
internal static string Write(
|
||||||
|
double avgDrawMs,
|
||||||
|
double maxDrawMs,
|
||||||
|
double avgDrawCallsProxy,
|
||||||
|
double maxDrawCallsProxy,
|
||||||
|
double avgDeltaMs,
|
||||||
|
double maxDeltaMs,
|
||||||
|
int frames
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var targetPath = Path.Join(Plugin.Interface.ConfigDirectory.FullName, "perf-baseline.json");
|
||||||
|
|
||||||
|
var json =
|
||||||
|
"{\n"
|
||||||
|
+ $" \"frames\": {frames},\n"
|
||||||
|
+ $" \"avgDrawMs\": {Fmt(avgDrawMs)},\n"
|
||||||
|
+ $" \"maxDrawMs\": {Fmt(maxDrawMs)},\n"
|
||||||
|
+ $" \"avgDrawCallsProxy\": {Fmt(avgDrawCallsProxy)},\n"
|
||||||
|
+ $" \"maxDrawCallsProxy\": {Fmt(maxDrawCallsProxy)},\n"
|
||||||
|
+ $" \"avgDeltaMs\": {Fmt(avgDeltaMs)},\n"
|
||||||
|
+ $" \"maxDeltaMs\": {Fmt(maxDeltaMs)}\n"
|
||||||
|
+ "}\n";
|
||||||
|
|
||||||
|
// Atomic replace — same volume rename is atomic on POSIX and Windows.
|
||||||
|
var tmpPath = targetPath + ".tmp";
|
||||||
|
File.WriteAllText(tmpPath, json);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Move(tmpPath, targetPath, overwrite: true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Avoid .tmp litter if Move fails (target locked).
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(tmpPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// best effort
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Fmt(double value)
|
||||||
|
{
|
||||||
|
return value.ToString("F2", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,42 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Plugin.SelfTest;
|
using Dalamud.Plugin.SelfTest;
|
||||||
|
|
||||||
namespace HellionChat.SelfTests;
|
namespace HellionChat.SelfTests;
|
||||||
|
|
||||||
// Optional metric capture. Walks one frame's ImGui IO counters and
|
// Optional metric capture. Accumulates 1000 steady-state frames of ImGui IO
|
||||||
// prints a single JSON block so the cycle-notes author can copy/paste
|
// counters plus the plugin's full-Draw wall-time (Plugin.LastDrawMs, B5-1),
|
||||||
// the snapshot without standing up a separate profiling harness.
|
// then writes a single perf-baseline.json into the plugin ConfigDirectory so
|
||||||
// Investigations themselves are deferred to the polish cycle — this
|
// the cycle-notes author can copy the §7.5 figures without a separate
|
||||||
// step only records, it never fails on threshold.
|
// profiling harness. The step only records — it never fails on a threshold
|
||||||
|
// (the budgets are evaluated by a human against the JSON, §7.5 "optional,
|
||||||
|
// manual"). It returns Waiting until the sample window fills, mirroring the
|
||||||
|
// per-frame poll idiom of ThemeSwitchSelfTestStep.
|
||||||
internal sealed class PerformanceBaselineStep : ISelfTestStep
|
internal sealed class PerformanceBaselineStep : ISelfTestStep
|
||||||
{
|
{
|
||||||
|
// §7.5 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;
|
||||||
|
|
||||||
|
// Quad proxy: ImGui emits 4 vertices + 6 indices per quad, so vertices/6
|
||||||
|
// approximates the draw-quad count. NOT the real ImDrawData command count —
|
||||||
|
// the <500/frame budget is checked approximately (API-3).
|
||||||
|
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)
|
public PerformanceBaselineStep(Plugin plugin)
|
||||||
{
|
{
|
||||||
_ = plugin;
|
_plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name => "Hellion Chat - Performance baseline capture";
|
public string Name => "Hellion Chat - Performance baseline capture";
|
||||||
@@ -21,25 +44,69 @@ internal sealed class PerformanceBaselineStep : ISelfTestStep
|
|||||||
public SelfTestStepResult RunStep()
|
public SelfTestStepResult RunStep()
|
||||||
{
|
{
|
||||||
var io = ImGui.GetIO();
|
var io = ImGui.GetIO();
|
||||||
var stopwatch = Stopwatch.StartNew();
|
|
||||||
// No actual probe — we just sample the counters that ImGui keeps
|
|
||||||
// updated each frame. Stopwatch is started so the JSON line
|
|
||||||
// includes a non-zero wall-time figure even when ImGui has not
|
|
||||||
// accumulated frame stats yet.
|
|
||||||
stopwatch.Stop();
|
|
||||||
|
|
||||||
|
// 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(
|
ImGui.Text(
|
||||||
"{ "
|
$"Sampling steady-state… {_frames}/{TargetFrames} frames. "
|
||||||
+ $"\"renderVertices\": {io.MetricsRenderVertices}, "
|
+ "Keep the chat window visible and idle."
|
||||||
+ $"\"renderIndices\": {io.MetricsRenderIndices}, "
|
|
||||||
+ $"\"renderWindows\": {io.MetricsRenderWindows}, "
|
|
||||||
+ $"\"activeWindows\": {io.MetricsActiveWindows}, "
|
|
||||||
+ $"\"deltaTimeMs\": {io.DeltaTime * 1000f:F2}, "
|
|
||||||
+ $"\"sampleWallTimeMs\": {stopwatch.Elapsed.TotalMilliseconds:F2}"
|
|
||||||
+ " }"
|
|
||||||
);
|
);
|
||||||
|
return SelfTestStepResult.Waiting;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logPath ??= WriteBaselineLog();
|
||||||
|
ImGui.Text($"Baseline captured ({TargetFrames} frames). Wrote: {_logPath}");
|
||||||
return SelfTestStepResult.Pass;
|
return SelfTestStepResult.Pass;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CleanUp() { }
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user