A simple opt-in profiler for the live UI pipeline. It records counts and elapsed ticks for rebuild phases (build, diff, apply, mount) and per-operation kinds, exposes Reset and Report to clear and produce a human-readable summary.
#nullable enable
using System;
using System.Diagnostics;
using System.Text;
namespace Goo;
/// <summary>Opt-in profiler for the live pipeline. Set <see cref="Enabled"/> = true, exercise the UI, then read <see cref="Report"/>. Headless benches cover Build and Diff time and allocation in isolation; the engine Apply path only runs in-game (Panel needs the native layout runtime), so this accumulates per-phase wall time and per-op-kind costs across rebuilds until <see cref="Reset"/>. Allocation tracking is headless-only: the sandbox whitelists Stopwatch but not GC.GetAllocatedBytesForCurrentThread.</summary>
public static class Perf
{
/// <summary>Master switch. Off (the default) costs one branch per rebuild and per op.</summary>
public static bool Enabled;
static readonly int OpKindCount = (int)OpKind.SetLayoutTransition + 1;
static long _rebuilds, _mounts;
static long _buildTicks, _diffTicks, _applyTicks, _mountTicks;
static readonly long[] _opCounts = new long[OpKindCount];
static readonly long[] _opTicks = new long[OpKindCount];
internal static void RecordRebuild(bool mount, long buildTicks, long diffTicks, long applyTicks)
{
_rebuilds++;
_buildTicks += buildTicks; _diffTicks += diffTicks; _applyTicks += applyTicks;
if (mount) { _mounts++; _mountTicks += buildTicks + diffTicks + applyTicks; }
}
internal static void RecordOp(OpKind kind, long ticks)
{
_opCounts[(int)kind]++;
_opTicks[(int)kind] += ticks;
}
/// <summary>Zero every accumulator. Call before the scenario you want to measure.</summary>
public static void Reset()
{
_rebuilds = 0; _mounts = 0;
_buildTicks = 0; _diffTicks = 0; _applyTicks = 0; _mountTicks = 0;
Array.Clear(_opCounts); Array.Clear(_opTicks);
}
/// <summary>Human-readable summary: per-phase totals and averages, mount cost, and a per-op-kind table sorted by total time.</summary>
public static string Report()
{
if (_rebuilds == 0 && TotalOps() == 0) return "goo perf: no samples (set Perf.Enabled = true and exercise the UI)";
var sb = new StringBuilder();
sb.Append("goo perf: rebuilds: ").Append(_rebuilds).Append(", mounts: ").Append(_mounts);
if (_mounts > 0) sb.Append(" (avg ").Append(Us(_mountTicks / _mounts).ToString("F1")).Append("us)");
sb.AppendLine();
AppendPhase(sb, "build", _buildTicks);
AppendPhase(sb, "diff", _diffTicks);
AppendPhase(sb, "apply", _applyTicks);
// Op table, slowest kind first.
var order = new int[OpKindCount];
for (int i = 0; i < OpKindCount; i++) order[i] = i;
Array.Sort(order, (a, b) => _opTicks[b].CompareTo(_opTicks[a]));
foreach (int k in order)
{
if (_opCounts[k] == 0) continue;
sb.Append(" op ").Append((OpKind)k)
.Append(" x").Append(_opCounts[k])
.Append(": ").Append(Us(_opTicks[k]).ToString("F1")).Append("us total, ")
.Append((Us(_opTicks[k]) / _opCounts[k]).ToString("F2")).AppendLine("us avg");
}
return sb.ToString();
}
static void AppendPhase(StringBuilder sb, string name, long ticks)
{
sb.Append(" ").Append(name).Append(": ").Append(Us(ticks).ToString("F1")).Append("us total");
if (_rebuilds > 0) sb.Append(", ").Append((Us(ticks) / _rebuilds).ToString("F2")).Append("us/rebuild");
sb.AppendLine();
}
static double Us(long ticks) => ticks * 1_000_000.0 / Stopwatch.Frequency;
static long TotalOps()
{
long n = 0;
for (int i = 0; i < OpKindCount; i++) n += _opCounts[i];
return n;
}
}