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); } }