perf(chat): slice 3 — raster-on-change cache in ChatOverlayLayer (take-5 fix)
Take 5: render 58.9 -> 25.5ms (138/300, still ~2.2x). The blits were fixed; the resolver was not: ResolveOutputFrame -> RenderChatBox ran a FULL WPF raster (FormattedText + RenderTargetBitmap + CopyPixels + channel swap) EVERY tick whenever the chat buffer was non-empty — and the buffer survives sessions, so even a signed-out record-only take paid it. Established answer (OBS text sources): re-render on change, blit the cache every tick. ChatOverlayLayer: content version bumped from Messages.CollectionChanged (covers adds, the 500-cap removal, the fade Clear from any caller) + a config key (size + all Chat* appearance props); RenderFrame returns the cached VideoFrame by identity until either changes (compositor only reads cached frames). Conservative ordering (version latched BEFORE render) makes a mid-render message re-render next tick, never serve stale. ONE integration test: ChatOverlayLayerCacheTests (RealApp, real renderer): Same() for unchanged inputs, NotSame() on message/config change, null on empty. Full regression green (62 across touched classes), clean build 0 warnings. Accepted cost pending take 6: one ~15-25ms tick per arriving message; if live-chat bursts sag n/300, next slice = debounced off-tick re-render. Docs same commit: ai.md pipeline section, TASKS.md TASK 18, MyMistakes recipe (raster-on-change + session-surviving-buffer trap), HANDOFF (take 6 -> then Unit B, spec unchanged).
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using ytLive.Models;
|
||||
using ytLive.Services;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Take-5 starvation fix (slice 3): the chat layer is rendered ON CHANGE, not on
|
||||
/// every pump tick — the per-tick WPF raster (FormattedText + RenderTargetBitmap
|
||||
/// + CopyPixels) was the 25ms render. OBS text sources raster-on-change; this
|
||||
/// pins that contract with the real renderer on the RealApp STA host: identical
|
||||
/// inputs reuse the cached frame by identity, and content or config changes force
|
||||
/// a fresh raster.
|
||||
/// </summary>
|
||||
[Collection("RealApp")]
|
||||
public sealed class ChatOverlayLayerCacheTests
|
||||
{
|
||||
private readonly RealAppHost _app;
|
||||
|
||||
public ChatOverlayLayerCacheTests(RealAppHost app) => _app = app;
|
||||
|
||||
[Fact]
|
||||
public void RenderFrame_RastersOnContentOrConfigChange_ReusesCacheOtherwise()
|
||||
{
|
||||
_app.Run(Run);
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
var layer = new ChatOverlayLayer(new ChatBoxRenderer());
|
||||
var box = new Source { Type = SourceType.ChatBox, Name = "YouTube Chat", Width = 220, Height = 160 };
|
||||
|
||||
Assert.Null(layer.RenderFrame(box)); // empty buffer — nothing to rasterize
|
||||
|
||||
layer.Messages.Add(new ChatMessage { AuthorName = "a", Message = "hello", Timestamp = DateTime.Now });
|
||||
var first = layer.RenderFrame(box);
|
||||
Assert.NotNull(first);
|
||||
|
||||
Assert.Same(first, layer.RenderFrame(box)); // unchanged content + config → the SAME frame
|
||||
Assert.Same(first, layer.RenderFrame(box)); // every tick hits the cache, not the raster
|
||||
|
||||
var resized = new Source { Type = SourceType.ChatBox, Width = 300, Height = 160 };
|
||||
var reRendered = layer.RenderFrame(resized);
|
||||
Assert.NotNull(reRendered);
|
||||
Assert.NotSame(first, reRendered); // config change → fresh raster
|
||||
|
||||
layer.Messages.Add(new ChatMessage { AuthorName = "b", Message = "second", Timestamp = DateTime.Now });
|
||||
var grown = layer.RenderFrame(box);
|
||||
Assert.NotNull(grown);
|
||||
Assert.NotSame(reRendered, grown); // new message → fresh raster
|
||||
|
||||
layer.Messages.Clear(); // the fade path
|
||||
Assert.Null(layer.RenderFrame(box));
|
||||
Assert.Null(layer.RenderFrame(box));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user