perf(diag): complete trustworthy Slice A capture tooling
Correct whole-frame GPU timestamps so they bracket only the accepted render transaction and associate delayed query results with the owning CPU frame. Add route-wide frame-history summaries, fixed-camera screenshot comparison, process counters, contention traces, a pinned Arwic workload, and credential-safe launch disclosure. The reference hardware/display contract now keeps local and RDP populations separate and defines the screenshot and re-baseline rules needed by later prepared-content gates. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
bf7ec12f68
commit
3ee8ec537a
23 changed files with 1639 additions and 170 deletions
|
|
@ -32,35 +32,25 @@ public enum FrameStage
|
|||
/// together. <c>GpuUs</c> is <c>-1</c> for a frame with no available GPU
|
||||
/// sample (warm-up, or <c>ACDREAM_WB_DIAG=1</c> self-disable).
|
||||
/// </summary>
|
||||
internal readonly struct FrameHistoryRecord(
|
||||
int frameIndex,
|
||||
double timestampMs,
|
||||
long cpuUs,
|
||||
long gpuUs,
|
||||
long allocBytes,
|
||||
long updateUs,
|
||||
long uploadUs,
|
||||
long imGuiUs,
|
||||
long pacingUs)
|
||||
{
|
||||
public readonly int FrameIndex = frameIndex;
|
||||
public readonly double TimestampMs = timestampMs;
|
||||
public readonly long CpuUs = cpuUs;
|
||||
public readonly long GpuUs = gpuUs;
|
||||
public readonly long AllocBytes = allocBytes;
|
||||
public readonly long UpdateUs = updateUs;
|
||||
public readonly long UploadUs = uploadUs;
|
||||
public readonly long ImGuiUs = imGuiUs;
|
||||
public readonly long PacingUs = pacingUs;
|
||||
}
|
||||
internal readonly record struct FrameHistoryRecord(
|
||||
int FrameIndex,
|
||||
double TimestampMs,
|
||||
long CpuUs,
|
||||
long GpuUs,
|
||||
long AllocBytes,
|
||||
long UpdateUs,
|
||||
long UploadUs,
|
||||
long ImGuiUs,
|
||||
long PacingUs);
|
||||
|
||||
/// <summary>
|
||||
/// MP0 (2026-07-05) — the permanent honest frame profiler. One
|
||||
/// <c>FrameBoundary</c> call at the top of <c>GameWindow.OnRender</c>
|
||||
/// <c>FrameBoundary</c> call at the top of the accepted render transaction
|
||||
/// measures CPU frame time as the delta between consecutive boundaries
|
||||
/// (captures the FULL frame including present), brackets the frame in a
|
||||
/// GPU <c>TimeElapsed</c> query (via <see cref="GpuFrameTimer"/>), and
|
||||
/// samples per-frame allocated bytes + GC collection counts. Stage scopes
|
||||
/// (captures the FULL frame including present) and samples per-frame allocated
|
||||
/// bytes + GC collection counts. <see cref="BeginGpuFrame"/> and
|
||||
/// <see cref="EndGpuFrame"/> separately bracket only submitted render work.
|
||||
/// Stage scopes
|
||||
/// (<see cref="BeginStage"/>) attribute CPU time to Update / Upload /
|
||||
/// ImGui. Emits one <c>[frame-prof]</c> line every ~5 s while
|
||||
/// <see cref="RenderingDiagnostics.FrameProfEnabled"/> is true; costs one
|
||||
|
|
@ -79,8 +69,9 @@ internal readonly struct FrameHistoryRecord(
|
|||
/// preallocated, grow-as-needed <see cref="List{T}"/> (1 int + 1 double +
|
||||
/// 7 longs ≈ 72 bytes/record; a multi-hour capture at 165 fps is roughly
|
||||
/// 165 * 3600 * 72 bytes ≈ 43 MB/hour — fine for a bounded diagnostic run,
|
||||
/// not for unattended day-long capture). ZERO frame-thread I/O: the list only grows during
|
||||
/// <see cref="FrameBoundary"/>; the CSV is written once, from
|
||||
/// not for unattended day-long capture). Its initial capacity is about 9 MiB
|
||||
/// and covers the canonical route above 300 FPS without a frame-thread resize.
|
||||
/// ZERO frame-thread I/O: the CSV is written once, from
|
||||
/// <see cref="Dispose"/>, at shutdown. Recording only takes effect while
|
||||
/// <see cref="RenderingDiagnostics.FrameProfEnabled"/> is ALSO true — it
|
||||
/// reuses that instrumentation rather than duplicating it. Does not
|
||||
|
|
@ -90,7 +81,7 @@ internal readonly struct FrameHistoryRecord(
|
|||
public sealed class FrameProfiler : IDisposable
|
||||
{
|
||||
private const int WindowCapacity = 2048; // ~12 s at 165 fps
|
||||
private const int HistoryInitialCapacity = 4096;
|
||||
private const int HistoryInitialCapacity = 131072;
|
||||
private const long ReportIntervalTicks = 5 * TimeSpan.TicksPerSecond;
|
||||
private static readonly int StageCount = Enum.GetValues<FrameStage>().Length;
|
||||
|
||||
|
|
@ -104,7 +95,8 @@ public sealed class FrameProfiler : IDisposable
|
|||
Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG") == "1";
|
||||
private readonly List<FrameHistoryRecord>? _history;
|
||||
private readonly long _profilerStartTimestamp;
|
||||
private int _historyFrameIndex;
|
||||
private readonly DateTime _profilerStartUtc;
|
||||
private int _currentFrameIndex = -1;
|
||||
|
||||
private GpuFrameTimer? _gpuTimer;
|
||||
private long _lastBoundaryTimestamp;
|
||||
|
|
@ -127,12 +119,14 @@ public sealed class FrameProfiler : IDisposable
|
|||
_stageAccumTicks = new long[StageCount];
|
||||
_lastStageUs = new long[StageCount];
|
||||
_profilerStartTimestamp = Stopwatch.GetTimestamp();
|
||||
_profilerStartUtc = DateTime.UtcNow;
|
||||
if (RenderingDiagnostics.FrameHistoryPath is not null)
|
||||
_history = new List<FrameHistoryRecord>(HistoryInitialCapacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call as the FIRST statement of <c>GameWindow.OnRender</c>.
|
||||
/// Call once at the accepted render-transaction boundary, before
|
||||
/// <see cref="BeginGpuFrame"/>.
|
||||
/// </summary>
|
||||
public void FrameBoundary(GL gl)
|
||||
{
|
||||
|
|
@ -150,6 +144,7 @@ public sealed class FrameProfiler : IDisposable
|
|||
_gpuTimer = null;
|
||||
_wasEnabled = false;
|
||||
_lastBoundaryTimestamp = 0;
|
||||
_currentFrameIndex = -1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -164,8 +159,6 @@ public sealed class FrameProfiler : IDisposable
|
|||
long now = Stopwatch.GetTimestamp();
|
||||
long allocNow = GC.GetAllocatedBytesForCurrentThread();
|
||||
|
||||
bool historyFrame = false;
|
||||
long historyCpuUs = 0, historyAllocBytes = 0;
|
||||
if (!_wasEnabled)
|
||||
{
|
||||
// First enabled frame (startup or runtime toggle-on): establish
|
||||
|
|
@ -179,6 +172,7 @@ public sealed class FrameProfiler : IDisposable
|
|||
Array.Clear(_stageAccumTicks);
|
||||
_gc0Base = GC.CollectionCount(0); _gc1Base = GC.CollectionCount(1); _gc2Base = GC.CollectionCount(2);
|
||||
if (_gpuTimer is null && !_wbDiagActive) _gpuTimer = new GpuFrameTimer(gl);
|
||||
_currentFrameIndex = 0;
|
||||
if (_wbDiagActive && !_wbDiagNoticePrinted)
|
||||
{
|
||||
_wbDiagNoticePrinted = true;
|
||||
|
|
@ -199,32 +193,25 @@ public sealed class FrameProfiler : IDisposable
|
|||
_stageAccumTicks[i] = 0;
|
||||
}
|
||||
_framesInWindow++;
|
||||
historyFrame = _history is not null;
|
||||
historyCpuUs = cpuUs;
|
||||
historyAllocBytes = allocDelta;
|
||||
if (_history is not null)
|
||||
{
|
||||
_history.Add(new FrameHistoryRecord(
|
||||
_currentFrameIndex,
|
||||
(now - _profilerStartTimestamp) * 1000.0 / Stopwatch.Frequency,
|
||||
cpuUs,
|
||||
-1L,
|
||||
allocDelta,
|
||||
_lastStageUs[(int)FrameStage.Update],
|
||||
_lastStageUs[(int)FrameStage.Upload],
|
||||
_lastStageUs[(int)FrameStage.ImGui],
|
||||
_lastStageUs[(int)FrameStage.Pacing]));
|
||||
}
|
||||
_currentFrameIndex++;
|
||||
}
|
||||
|
||||
_lastBoundaryTimestamp = now;
|
||||
_lastAllocBytes = allocNow;
|
||||
|
||||
long? gpuUsSample = _gpuTimer?.FrameBoundary();
|
||||
if (gpuUsSample is long gpuUs)
|
||||
_gpuUs.Push(gpuUs);
|
||||
|
||||
if (historyFrame)
|
||||
{
|
||||
_history!.Add(new FrameHistoryRecord(
|
||||
_historyFrameIndex++,
|
||||
(now - _profilerStartTimestamp) * 1000.0 / Stopwatch.Frequency,
|
||||
historyCpuUs,
|
||||
gpuUsSample ?? -1L,
|
||||
historyAllocBytes,
|
||||
_lastStageUs[(int)FrameStage.Update],
|
||||
_lastStageUs[(int)FrameStage.Upload],
|
||||
_lastStageUs[(int)FrameStage.ImGui],
|
||||
_lastStageUs[(int)FrameStage.Pacing]));
|
||||
}
|
||||
|
||||
long nowTicks = DateTime.UtcNow.Ticks;
|
||||
if (nowTicks - _lastReportTicks >= ReportIntervalTicks && _framesInWindow > 0)
|
||||
{
|
||||
|
|
@ -242,6 +229,38 @@ public sealed class FrameProfiler : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begin GPU timing immediately before render-resource, world, and private
|
||||
/// presentation submission. Completed delayed samples are associated with
|
||||
/// their owning history row rather than the frame that happened to poll
|
||||
/// them.
|
||||
/// </summary>
|
||||
public void BeginGpuFrame()
|
||||
{
|
||||
if (!_wasEnabled || _gpuTimer is null || _currentFrameIndex < 0)
|
||||
return;
|
||||
|
||||
Span<GpuFrameSample> completed = stackalloc GpuFrameSample[4];
|
||||
int completedCount = _gpuTimer.BeginFrame(_currentFrameIndex, completed);
|
||||
for (int index = 0; index < completedCount; index++)
|
||||
{
|
||||
GpuFrameSample sample = completed[index];
|
||||
_gpuUs.Push(sample.ElapsedUs);
|
||||
if (_history is not null
|
||||
&& (uint)sample.FrameIndex < (uint)_history.Count)
|
||||
{
|
||||
FrameHistoryRecord row = _history[sample.FrameIndex];
|
||||
_history[sample.FrameIndex] = row with { GpuUs = sample.ElapsedUs };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>End GPU timing immediately after render submission.</summary>
|
||||
public void EndGpuFrame()
|
||||
{
|
||||
_gpuTimer?.EndFrame();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attribute the enclosed CPU time to <paramref name="stage"/>.
|
||||
/// Usage: <c>using var _ = profiler.BeginStage(FrameStage.Update);</c>.
|
||||
|
|
@ -283,14 +302,22 @@ public sealed class FrameProfiler : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>Pure CSV formatter — unit-tested; invariant culture. One header row plus one row per record.</summary>
|
||||
internal static void WriteHistoryCsv(IEnumerable<FrameHistoryRecord> records, TextWriter writer)
|
||||
internal static void WriteHistoryCsv(
|
||||
IEnumerable<FrameHistoryRecord> records,
|
||||
TextWriter writer,
|
||||
DateTime profilerStartUtc)
|
||||
{
|
||||
var ci = CultureInfo.InvariantCulture;
|
||||
writer.WriteLine("frame,timestamp_ms,cpu_us,gpu_us,alloc_bytes,update_us,upload_us,imgui_us,pacing_us");
|
||||
DateTime startUtc = profilerStartUtc.ToUniversalTime();
|
||||
writer.WriteLine(
|
||||
"frame,timestamp_ms,timestamp_utc,cpu_us,gpu_us,alloc_bytes,"
|
||||
+ "update_us,upload_us,imgui_us,pacing_us");
|
||||
foreach (FrameHistoryRecord r in records)
|
||||
{
|
||||
writer.Write(r.FrameIndex.ToString(ci)); writer.Write(',');
|
||||
writer.Write(r.TimestampMs.ToString("0.000", ci)); writer.Write(',');
|
||||
writer.Write(startUtc.AddMilliseconds(r.TimestampMs).ToString("O", ci));
|
||||
writer.Write(',');
|
||||
writer.Write(r.CpuUs.ToString(ci)); writer.Write(',');
|
||||
writer.Write(r.GpuUs.ToString(ci)); writer.Write(',');
|
||||
writer.Write(r.AllocBytes.ToString(ci)); writer.Write(',');
|
||||
|
|
@ -316,7 +343,7 @@ public sealed class FrameProfiler : IDisposable
|
|||
try
|
||||
{
|
||||
using var writer = new StreamWriter(path, append: false);
|
||||
WriteHistoryCsv(_history, writer);
|
||||
WriteHistoryCsv(_history, writer, _profilerStartUtc);
|
||||
Console.WriteLine($"[frame-prof] wrote {_history.Count} history record(s) to '{path}'");
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue