Per-element composite timing + scene dump (measurement only)

The cadence work ended with a question the log could not answer: the compositor
manages 6.8 composites/s at ~141ms while the 10:03 take did 29-34ms on the "same
scene shape" (full-render split=0 elements=6 dynamic=5). Nothing in the stats line
said WHICH of the six elements cost what, and nothing recorded the scene itself, so
the two takes could only be compared by eye. This closes that gap and nothing else:
no fix is attempted until a real take names the culprit.

- Services/Compositor/CompositeTimings.cs (new): per-window collector.
  Add(stage, ticks) accumulates; Describe(renders, minMs = 0.1) prints the averaged
  breakdown slowest-first, dropping sub-0.1ms noise.
- SceneCompositor.Render / CompositeLayers / BakeStaticBase / BlitBakedElement /
  BlitCachedLayer + SceneGraph.GetBakedBase: trailing optional CompositeTimings?
  Every stage is timed (backdrop, base, flash, social, ticker, zoom, scale, overlay,
  bake backdrop, bake overlays) and EVERY element gets its own entry keyed by
  z-index + name ("#2 Webcam"), with the nested "raster" sub-stage recording the
  paste-cache rasterize miss - the split between "re-samples every tick" and "slow
  paste" without a second session. Bake-pass elements are prefixed "bake #..." so a
  one-off rebake cannot masquerade as a per-composite cost. Callers that pass
  nothing (thumbnails, existing tests) are unaffected and pay nothing.
- FramePump: owns the collector, threads it through RenderScene/RenderFull/
  GetBakedBase/CompositeLayers, and logs "FramePump parts (N renders/5s): ..." once
  per 5s window beside the stats line (averaged per render, so it compares directly
  with "avg render"), then resets it. Also dumps the scene - "FramePump scene dump:"
  header plus one line per element (z-index, name, kind, source type, visibility,
  rect, opacity, clip) - at pump start and on every scene change.
- One Good Dog integration test (ytLive.Tests/CompositeTimingTests.cs): a real
  composite of a full-canvas element plus a 64x64 one through the real collector
  must rank the full-canvas element first, carry the raster sub-stage and the fixed
  stages, and the dump must name both elements with z-index/type/rect and echo the
  split point.

Derivative work (AGENTS.md): OBS's answer to "which source is slow" is the Source
Profiler - average execution time of each source's tick and render functions
(https://docs.obsproject.com/reference-libobs-util-source-profiler, util/profiler.h).
Same shape here: one timer per element per composite, reported as an average, plus
the stage split this case needs.

Docs in the same commit: ai.md (new subsection "Per-element composite timing + scene
dump", the closed gap corrected in "Frame cadence"), Services/index.md (new file row
+ FramePump/SceneCompositor rows), TASKS.md (unit marked shipped, next = creator
take), HANDOFF.md (rewritten: unpushed commit, next step, verify.sh whitelist hole).

Also recorded this session (from the creator): the launch command is
`dotnet run -c Release` - Release is the DEFAULT, Debug only when explicitly named,
and perf takes must read Release. ai.md -> Run updated.

Full suite 428 tests, 0 warnings; the only failures are the known audio env flakes
(both pass in isolation). Scope check passed.
This commit is contained in:
2026-10-16 18:23:20 -07:00
parent 3cb29ea45c
commit a3a4681aac
9 changed files with 408 additions and 60 deletions
+47 -23
View File
@@ -1,42 +1,66 @@
# HANDOFF — 2026-10-14 (clean tree; all units committed AND pushed)
# HANDOFF — 2026-10-16 (work committed locally; NOT pushed — push at the next milestone)
## At a glance
- **Branch** main, tracking `origin/main`. **HEAD = `origin/main` = `931ab46`** — 0 ahead, 0 behind.
Everything below is pushed; nothing is local-only.
- Working tree is **clean** (the stray `Assets/llamacasty-text-nobg-2.png~` backup was deleted 2026-10-14).
- **Branch** main, tracking `origin/main`. **origin/main = `931ab46`; HEAD = the tip commit "Per-element
composite timing + scene dump (measurement only)"** — **2 ahead, 0 behind** (nothing pushed this
session; per the commit/push policy, push only at sub-milestones/milestones, when the creator says
so). This file cannot record its own commit's hash — amending a commit changes that hash — so the tip
is named by subject.
- The two local-only commits are `3cb29ea` (**the previous session's HANDOFF refresh — the old
handoff claimed it was pushed; it never was**) and the tip. So "0 ahead" in the 2026-10-14 handoff
was wrong — check `git log origin/main..HEAD`, never the handoff's own claim.
- Working tree **clean** after the commit.
- No app run, recording, or test session happened 2026-10-11 → 10-16 (verified against
`%APPDATA%\ytLlive\startup.log`: last activity 2026-10-10 13:26). The creator was away for a family
emergency and has only just returned — **there is still no take of the instrumented build.**
## Recent units (all committed + pushed)
- `931ab46` — pinned chat: `ChatMessage.IsPinned`, `MainViewModel.PinnedChatMessage` +
Pin/Clear commands (`Helpers/RelayCommandGeneric.cs`), LeftPanel pinned banner via `NotNullToVis`.
Plus brand wordmark assets. (This was previously mislabeled in the old handoff as "not mine — do NOT commit"; it is committed.)
- `8e1d6f4` — strip tile artwork + on-air indicator (per-scene `Scene.ThumbnailAssetId`, shared
`OnAirImageAssetId`, PreviewPane green rect removed, tile labels FontSize 20, Asset-pruner keep-set
fix). 427 pass, 0 warnings.
- `06f88f4` — scene strip renders in canonical F1–F5 order (`ReorderScenesToCanonical`).
- `63bf844` — desktop lock: `ShowActivated = !FullscreenGameRunningNow` + `FullScreenDetectorOverride`.
## This session's unit (committed, unpushed — the tip commit)
- **Per-element composite timing + scene dump — MEASUREMENT ONLY (2026-10-16).** Closes the gap the
cadence work left: the 141ms take and the 30ms take could not be compared because the log named no
element and recorded no scene.
- `Services/Compositor/CompositeTimings.cs` (new): collector; `Describe(renders, minMs = 0.1)`
prints the averaged breakdown slowest-first.
- `SceneCompositor.Render` / `CompositeLayers` / `BakeStaticBase` / `BlitBakedElement` /
`BlitCachedLayer` + `SceneGraph.GetBakedBase`: trailing optional `CompositeTimings?` — every stage
and every element timed (`#<i> <name>`, nested `raster` = paste-cache miss). Existing callers
unaffected.
- `FramePump`: `FramePump parts (N renders/5s): …` per stats window; `FramePump scene dump:` block
(header + one line per element) at pump start and on scene change.
- Gate test: `ytLive.Tests/CompositeTimingTests.cs` (one `[Fact]`, real composite through the real
collector + dump formatter). Full suite 428 tests; 0 warnings; scope check passed.
- Derivative citation: OBS Source Profiler (per-source tick/render average) — see `ai.md`
"Per-element composite timing + scene dump".
## Next step
- **Webcam cadence work** (creator-queued 2026-10-05; see TASKS.md → "Webcam cadence → resolution").
**ACTUAL NEXT UNIT: per-element composite timing + a scene dump at pump start.** Measurement only —
no fix until it names which of the 6 elements eats ~140ms. Needs a creator recording/session to read.
The compositor manages **6.8 composites/s at ~141ms** and the file is **88.7% duplicate frames** — that
is the judder; the camera itself delivers 30.0/s clean.
- **Creator take on the instrumented build, Release** (`"/mnt/c/Program Files/dotnet/dotnet.exe" run
-c Release`). Read the two new lines in `%APPDATA%\ytLlive\startup.log`:
`FramePump parts (N renders/5s): …` (which element / which stage eats the budget) and the
`FramePump scene dump:` block (the shape the take was made from). **No fix is to be attempted until
the parts line names the element** — the 10:03 → 12:01 regression (29–34ms → 143–155ms) is still
unexplained, and the competing explanations in the map (21:9 source rect vs. same-shape regression)
are unresolved on purpose until a reading decides it.
## Also still open (operational)
- **verify.sh's known-failure whitelist is too loose (found 2026-10-16):** it counts matching names and
passes if `KNOWN > 0`, so a NEW third failure rides along unnoticed. This session's full run showed
`AudioPipelineTests.MasterLimiter_CapsTheLiveMix_OnThePipe` failing beside the two known ones — it
passes in isolation (29/29), so it is an audio-device env flake like
`Mix_HonorsProviderGains…` (system audio playing). **Follow-up: tighten the script** (require EVERY
failure line to be in the whitelist) and add `MasterLimiter_CapsTheLiveMix_OnThePipe` to the list.
Not fixed in this unit — out of scope (`scripts/verify.sh` was not in the declared file list).
- **Vertical-format regression (STALE, not fixed):** 1080p60-vertical selection no longer shrinks the
capture border in the preview. Mechanism mapped in `Controls/PreviewPane.xaml:319-322` (crop Rectangle
bound to `OutputRectX/Y/W/H` + `IsOutputCropped`, driven by `MainViewModel.Streaming.cs`
`ApplyStreamQuality`; `DimRects` in `MainViewModel.cs:91-94`). Root cause unknown — paused.
- ⛔ Sudo password rotation pending (user; then update both password lines in `CREDENTIALS.md`).
- TASK 56 secret scan before `gramps/LlamaCasty` goes public.
- TASK 55 disk hygiene queued (`startup.log` uncapped).
- TASK 55 disk hygiene queued (`startup.log` uncapped — now 22MB and growing with the new lines).
## Tests
- Full suite **427 passed / 0 failed / 0 skipped / 0 warnings** (as of `8e1d6f4`). Known env flakes:
`Mix_HonorsProviderGains…` (fails while system audio plays), `RealMouseDrag…` (needs an idle desktop),
`Round_Clip_Corner…` (grab/geometry under active mouse). The verify gate whitelists only the latter two —
`RealMouseDrag…` failing mid-session needs one re-run when the desktop idles.
- Full suite **426 passed / 2 failed / 0 skipped / 0 warnings** (2026-10-16, 428 total incl. the new
test). Both failures are the audio env flake (see above) and pass in isolation.
- Pre-commit gate: `./scripts/verify.sh "<file1>" "<file2>" ...` (clean build + full suite + scope check).
**Read its output — it can pass with an unlisted failure present (see the landmine above).**
## Secrets / paths (do not re-hunt)
- Layout DB: `%APPDATA%\ytLlive\ytLlive.db` (SQLite, full-rewrite per save). Auth: `%APPDATA%\ytLlive\ytLlive.auth`