feat(33): structured pump stats + the session planner and its plain-words copy

Slice 2. 588 green, 0 warnings.

- `FramePumpStats` and `FramePump.StatsReported`: the 5 s window the log line already describes, as
  data. `Dropped` is the ENCODER's counter (frames submitted, not accepted); `Stalls`/`AvgRenderMs`
  are the pump's. `FramesRendered` is deliberately not used for classification — a static scene
  reuses the paste cache and renders few frames while completely healthy, so it would read as
  starvation.
- `StepDownPlanner`: one per broadcast, owns the stateful policy, converts stats → sample, exposes the
  landed tier for the restart. It never acts — the VM owns the effects, which is what keeps the ladder
  testable without an encoder.
- `Describe` / `ReportLine`: the sentences a creator sees. Machine cause says LlamaCasty couldn't keep
  up and the connection was not the problem; upload cause blames the upload and promises no further
  thrash. A test asserts the machine copy never leaks "dropped" — mechanism strings stay in the log.

Remaining for the task: VM subscription, the encoder restart at the landed tier, the toast, and
shedding our own capture cost before stepping fps.
This commit is contained in:
2026-11-09 10:06:36 -08:00
parent bc62bd81ec
commit 555ff5c0ac
5 changed files with 225 additions and 1 deletions
+26
View File
@@ -0,0 +1,26 @@
namespace ytLive.Services.Encoder;
/// <summary>One 5-second window of pump and encoder evidence, structured.
///
/// <para>The stats line in the log is for humans; this is for the policy that has to decide whether
/// the machine or the connection is the problem. Note which counter is which:</para>
/// <list type="bullet">
/// <item><see cref="Stalls"/> and <see cref="AvgRenderMs"/> are the MACHINE's evidence — the pump
/// could not produce a frame in time.</item>
/// <item><see cref="Dropped"/> is the ENCODER's — frames submitted but not accepted downstream. It
/// rises when ffmpeg is blocked, which under CBR means the socket, not the renderer.</item>
/// <item><see cref="FramesRendered"/> is deliberately NOT used for classification: a static scene
/// legitimately reuses the paste cache and renders few frames while completely healthy.</item>
/// </list>
/// </summary>
public readonly record struct FramePumpStats(
double WindowSeconds,
int FramesSubmitted,
int FramesRendered,
double AvgRenderMs,
double AvgSubmitMs,
double AvgWaitMs,
double WorstRenderMs,
double WorstSubmitMs,
int Dropped,
int Stalls);