using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace ytLive.Services.Compositor;
///
/// Per-stage composite timing (webcam-cadence unit, 2026-10-16): wall time per render
/// pass, split into named stages — the backdrop, every element (keyed by z-index +
/// name), the global overlays, and the final output scale. The pump owns one instance
/// for the whole session, passes it into /
/// CompositeLayers / BakeStaticBase, and logs an averaged breakdown every
/// stats window — the "which of the 6 elements eats ~140ms" reading the old stats line
/// could not give (it reported render/acquire/resolve, never the culprit layer).
/// Stage names are stable and greppable: backdrop, base,
/// flash, social, ticker, zoom, scale,
/// bake backdrop, bake overlays, and #<index> <name> per
/// element (bake-pass elements are prefixed bake so a one-off rebake cannot
/// masquerade as a per-composite cost). A #<i> <name> raster sub-stage NESTS inside its parent's total
/// (it is the paste-cache rasterize miss — the prime suspect when one element costs a
/// full resample every tick). Stage time INCLUDES that stage's frame resolve; resolve
/// is separately totalled in the stats line.
/// Single writer in production (the pump's render thread — every blit joins its
/// workers before returning); the lock only makes a test able
/// to share one across threads.
///
public sealed class CompositeTimings
{
private readonly object _gate = new();
private readonly Dictionary _stages = new(StringComparer.Ordinal);
/// Accumulate one stage sample. Non-positive ticks are dropped — a
/// skipped layer must not create a zero-cost entry that dilutes the reading.
public void Add(string stage, long ticks)
{
if (ticks <= 0 || stage.Length == 0) return;
lock (_gate)
{
_stages.TryGetValue(stage, out var cur);
_stages[stage] = cur + ticks;
}
}
/// Clear every stage — called once per stats window after the parts line
/// is logged, so each window reports its own breakdown.
public void Reset()
{
lock (_gate) _stages.Clear();
}
/// Average milliseconds per render for every stage at or above
/// , sorted slowest-first:
/// "#2 Webcam 88.1ms, backdrop 61.2ms, #2 Webcam raster 84.0ms".
/// is the divisor (the window's render count, so the
/// numbers compare directly with the stats line's avg render). The floor
/// keeps a healthy pipeline from burying the contender in zeroes; the empty string
/// means nothing was composited (the fully-static shortcut and C4 cache hits do no
/// per-element work — an empty parts line there is the correct reading).
public string Describe(int renders, double minMs = 0.1)
{
List> snap;
lock (_gate)
{
if (_stages.Count == 0) return string.Empty;
snap = _stages.ToList();
}
var freq = (double)Stopwatch.Frequency;
var parts = snap
.Select(kv => (Stage: kv.Key, Ms: kv.Value / freq * 1000.0 / System.Math.Max(1, renders)))
.Where(p => p.Ms >= minMs)
.OrderByDescending(p => p.Ms)
.Select(p => $"{p.Stage} {p.Ms:F1}ms");
return string.Join(", ", parts);
}
}