Files
LlamaCasty/ytLive.Tests/ChatOverlayLayerCacheTests.cs
T
gramps 1c48849853 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).
2026-09-04 11:01:51 -07:00

58 lines
2.2 KiB
C#

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));
}
}