fix(web): web-layer capture cadence 10Hz → ~30Hz while recording — widget animations no longer play ~1/6 speed

The recording is 60fps but WebView2 capture was a blind 100ms DispatcherTimer = 10Hz;
each captured web frame repeated ~6x into the file caps web animation at the capture
rate, not the page's (user: 'the animation appears to be too slow').

- New CaptureScheduler (Services/CaptureScheduler.cs): per-session dispatcher timer
  that DROPS a tick while a capture is in flight (latest-wins, never queues) — the
  guard that makes a higher cadence safe: concurrent full-HD PNG CapturePreviewAsync
  calls (~10-30ms each, slow per WebView2Feedback#20) would stack CPU and publish
  stale-after-fresh. Effective cadence = max(interval, capture duration).
- Cadence: SetCaptureInterval(33) on record/stream start, (200) idle — applied via
  MainViewModel.Streaming.Operations.cs.
- De-throttle the hidden page: shared CoreWebView2Environment created BEFORE
  EnsureCoreWebView2Async with --disable-backgrounding-occluded-windows
  --disable-renderer-backgrounding --disable-features=CalculateNativeWinOcclusion.
  Off-screen WebView2 is a hidden page when the host window is unfocused/covered and
  Chromium then parks rAF and clamps timers to ~1s (WebView2Feedback#1172/#3070,
  Chrome-88 timer-throttling blog).
- Telemetry: first 30 captures per session log elapsed ms (PNG encode + decode) to
  startup.log — that decides whether ~30Hz stays or drops to ~20Hz; the FramePump
  drops frames (never time-lapses, slice 10) if UI-thread GC churn starves it.
- ONE test: CaptureScheduler_Drops_Ticks_While_Capture_InFlight_And_Resumes
  (deterministic TCS-driven, no WebView2 runtime). Suite 290/291 — sole failure the
  pre-existing compositor pixel test.
- Docs same-commit: ai.md slice 11, MyMistakes.md, HANDOFF.

References: https://github.com/MicrosoftEdge/WebView2Feedback/issues/1172
https://github.com/MicrosoftEdge/WebView2Feedback/issues/3070
https://github.com/MicrosoftEdge/WebView2Feedback/issues/20
https://developer.chrome.com/blog/timer-throttling-in-chrome-88
This commit is contained in:
2026-09-10 10:25:41 -07:00
parent ec7c734bdd
commit 5e78065c2d
7 changed files with 292 additions and 66 deletions
+32
View File
@@ -214,9 +214,41 @@ Both halves were solved by OBS/libyuv long ago; do not re-derive:
that HOLDS submitted frames now must snapshot them (`Clone`) once the producer
legitimately recycles buffers — mirror the real consumer's copy semantics in the fake.
---
### A web widget captured at 10Hz inside a 60fps recording plays at ~1/6 speed
Derived 2026-09-10 (the "widget animation too slow" report). The recording is 60fps and the
WebView2 capture loop was a blind 100ms `DispatcherTimer` = 10Hz — each captured web frame gets
repeated ~6× in the file, so whatever the page animates at, the OUTPUT is capped at the *capture*
cadence, not the page's. Two stacked throttles, both real:
1. **The capture rate is the hard ceiling.** web-layer motion in the recording can never beat
`CapturePreviewAsync` frequency. But you can't just raise the timer: full-HD PNG capture costs
~10-30ms (WebView2Feedback#20: "CapturePreviewAsync … produces PNG/JPG and is very slow"), so
concurrent captures stack CPU AND can publish stale-after-fresh. The mandatory shape is a
**latest-wins drop**: at most one capture in flight per session; a tick during the in-flight
window is DROPPED, never queued; effective cadence = max(interval, capture duration). Do this
before anyone touches the interval constant.
2. **Chromium throttles hidden pages.** An off-screen WebView2 (we place it at (-5000,-5000)) is a
hidden page the moment the host window is unfocused or covered: `requestAnimationFrame` parks and
JS timers clamp to ~1s (WebView2Feedback#1172 — background-throttled rAF; #3070 — a WebView2 with
`Visibility.Collapsed` slows timers to 1s; Chrome-88 blog — heavy timer throttling of hidden tabs).
There is NO supported per-page opt-out (#5250 still open). The embedder answer is browser args on
the `CoreWebView2EnvironmentOptions` created BEFORE `EnsureCoreWebView2Async`:
`--disable-backgrounding-occluded-windows --disable-renderer-backgrounding
--disable-features=CalculateNativeWinOcclusion` (the Electron/Streamlabs-class fix for
occluded-window animation throttling). Share ONE environment across sessions (one browser process).
3. **Measure before picking the cadence.** Log the first ~30 captures' elapsed ms on the first
recording run; PNG encode + WPF decode of 1920×1080 is the per-capture cost that decides whether
~30Hz is affordable or it must drop to ~20Hz. The FramePump drops frames (never time-lapses) if
UI-thread GC churn starves it, so cost shows up as dropped-frame stats — read them.
---
## Splitting a large file into partials — NEVER `awk … > SRC` while awking SRC
(2026-08-31, Commit D) Tried to split `SocialsDialogViewModel.cs` in one line: