fix(pump): slice 7 — take the loop off the UI thread (the 'wait 10ms after render 22ms' contradiction resolved)
Take 10 (59a02a5b, slice 6) finally produced a self-contradicting stat: render 22.4ms + submit 2.5 against a 16.7ms deadline, yet avg wait 10ms — a rebasing pacer CANNOT sleep after a blown deadline. The wait was queue time: StartAsync fires from a UI command handler, and async continuations re-capture the current SynchronizationContext — the 'WPF-free, hermetic' frame pump had been rendering ON THE DISPATCHER behind the live preview the entire starvation saga. OBS keeps obs_graphics_thread/video_thread off-UI for exactly this reason (dedicated threads; see docs.obsproject.com/backend-design 'Libobs Threads'). - FramePump: _pumpTask = Task.Run(() => PumpAsync(...)) — null context inside, every continuation stays on the pool. - Audited, not ignored, what that exposes: StaticPixelCache.Get now locks (pool miss-decodes raced UI callers); ChatOverlayLayer.RenderFrame checks its cache off-thread but marshals the rare raster MISS to the dispatcher (DrawingVisual + RenderTargetBitmap are UI-thread objects) and re-validates there; pump events already marshal in the VM. - GCLatencyMode.SustainedLowLatency for the pump's life (restored in finally). - Stats gained 'worst render Xms' — bimodal averages hid per-tick spikes. - Webcam routes through the paste cache (the IsOpaque bypass re-sampled ~156k px every tick even between identical device frames). ONE integration test: Pump_Produces_OffTheStartingContext — an inline-pumping SynchronizationContext makes the old construction run the resolver on the starting thread by capture; the loop must never. 70/70 per-class green, clean build 0 warnings. Docs same commit (ai.md slice 7, TASKS take-11 gate, MyMistakes #6, HANDOFF). take 11: ~300/300 + honest wait -> saga closed, Unit B (two-line top bar spec, fully captured) starts.
This commit is contained in:
@@ -424,4 +424,49 @@ public class FramePumpTests
|
||||
Assert.True(encoder.Backings.Distinct().Count() < encoder.Backings.Count,
|
||||
"every frame rode a fresh buffer — the scratch pool is inert");
|
||||
}
|
||||
|
||||
/// <summary>One integration test for the take-8 off-UI fix: the pump loop must NOT
|
||||
/// run on the starting thread's SynchronizationContext (StartAsync fires from a UI
|
||||
/// command handler; the old loop inherited it, so every frame competed with the
|
||||
/// live preview for the dispatcher — the "wait 10ms after a 22ms render" that the
|
||||
/// quantum stats exposed). An inline-pumping sync context makes the OLD code run
|
||||
/// its resolver on the starting thread; the Task.Run'd loop must never do so.</summary>
|
||||
[Fact]
|
||||
public async Task Pump_Produces_OffTheStartingContext()
|
||||
{
|
||||
var startThread = Environment.CurrentManagedThreadId;
|
||||
var resolverThreads = new List<int>();
|
||||
var firstFrame = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var encoder = new FakeEncoder();
|
||||
using var pump = NewPump(encoder, resolve: _ =>
|
||||
{
|
||||
lock (resolverThreads) resolverThreads.Add(Environment.CurrentManagedThreadId);
|
||||
firstFrame.TrySetResult();
|
||||
return null;
|
||||
});
|
||||
|
||||
var prev = SynchronizationContext.Current;
|
||||
SynchronizationContext.SetSynchronizationContext(new InlineSyncContext());
|
||||
try
|
||||
{
|
||||
await pump.StartAsync();
|
||||
await firstFrame.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
finally
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(prev);
|
||||
await pump.StopAsync();
|
||||
}
|
||||
|
||||
Assert.NotEmpty(resolverThreads);
|
||||
Assert.All(resolverThreads, id => Assert.True(id != startThread,
|
||||
$"resolver ran on the starting thread ({startThread}) — the loop is back on the caller context"));
|
||||
}
|
||||
|
||||
private sealed class InlineSyncContext : SynchronizationContext
|
||||
{
|
||||
public override void Post(SendOrPostCallback d, object? state) => d(state);
|
||||
public override void Send(SendOrPostCallback d, object? state) => d(state);
|
||||
public override SynchronizationContext CreateCopy() => this;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user