Files
LlamaCasty/Services/CaptureScheduler.cs
T
gramps 5e78065c2d 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
2026-09-10 10:25:41 -07:00

73 lines
2.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace ytLive.Services;
/// <summary>Per-session web capture pace: a DispatcherTimer that drops ticks while a
/// capture is in flight (latest-wins). Raising the naive 10Hz cadence to ~30Hz would
/// stack concurrent CapturePreviewAsync calls (a full 1920×1080 PNG encode costs
/// ~10-30ms), so overlapping ticks are dropped, never queued; the effective cadence
/// is max(interval, capture duration). The interval and the in-flight drop live here,
/// not in the manager, so the scheduler is unit-testable without a WebView2 runtime.</summary>
internal sealed class CaptureScheduler
{
private readonly Dispatcher _dispatcher;
private readonly Func<Task> _capture;
private DispatcherTimer? _timer;
private int _intervalMs;
private bool _inFlight;
private Task _pending = Task.CompletedTask;
public CaptureScheduler(Dispatcher dispatcher, int intervalMs, Func<Task> capture)
{
_dispatcher = dispatcher;
_intervalMs = Math.Max(16, intervalMs);
_capture = capture;
}
/// <summary>The task of the capture currently in flight, or a completed task when
/// idle. Test seam: lets the caller await exactly the running capture.</summary>
internal Task Pending => _pending;
public void Start()
{
if (_timer != null) return;
_timer = new DispatcherTimer(
TimeSpan.FromMilliseconds(_intervalMs),
DispatcherPriority.Background,
(_, _) => Tick(),
_dispatcher);
_timer.Start();
}
public void Stop() => _timer?.Stop();
public void SetInterval(int milliseconds)
{
_intervalMs = Math.Max(16, milliseconds);
if (_timer != null) _timer.Interval = TimeSpan.FromMilliseconds(_intervalMs);
}
/// <summary>Runs the capture unless one is already in flight — the dropped tick is
/// the latest-wins policy. Tick runs on the dispatcher thread (timer + test), so
/// _inFlight needs no locking.</summary>
internal void Tick()
{
if (_inFlight) return;
_inFlight = true;
_pending = RunCapture();
}
private async Task RunCapture()
{
try
{
await _capture();
}
finally
{
_inFlight = false;
}
}
}