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:
@@ -0,0 +1,73 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user