Files
gramps 555ff5c0ac 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.
2026-11-09 10:06:36 -08:00

78 lines
3.6 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using ytLive.Models;
using ytLive.Services.Encoder;
namespace ytLive.Services;
/// <summary>
/// TASK 33 slice 2: the session's adaptor between the pump's evidence and the pure policy. One planner
/// per broadcast, because the policy is stateful (budget, cooldown, current tier) and a step must
/// survive across windows.
///
/// <para>It deliberately does not act. It answers "should this session change something, and why"; the
/// view model owns the restart, the toast and the session-report line. Keeping the effects out is what
/// makes the whole ladder testable without an encoder running.</para>
/// </summary>
public sealed class StepDownPlanner
{
private readonly StepDownPolicy _policy;
private readonly DateTime _startedUtc;
public StepDownPlanner(StepDownPolicy policy, DateTime startedUtc)
{
_policy = policy;
_startedUtc = startedUtc;
}
public StepDownPolicy Policy => _policy;
/// <summary>Current tier after any applied steps, in the shape the encoder restart needs.</summary>
public (int Width, int Height, int Fps, double BitrateMbps) Current =>
(_policy.Width, _policy.Height, _policy.Fps, _policy.BitrateMbps);
public StepDownDecision Observe(in FramePumpStats stats, DateTime nowUtc)
{
var sample = new StepDownSample(
WindowSeconds: stats.WindowSeconds,
PumpStalls: stats.Stalls,
PumpDrops: 0, // the pump pads with the last frame; FramesRendered is ambiguous
EncoderDrops: stats.Dropped,
AvgRenderMs: stats.AvgRenderMs,
AvgSubmitWaitMs: stats.AvgSubmitMs);
return _policy.Observe(sample, nowUtc - _startedUtc);
}
/// <summary>The plain sentence the creator sees, chosen by cause. Never a mechanism string: "ffmpeg
/// exited", "dropped 8", an HTTP code are log findings, not an explanation of what happened to them.
/// </summary>
public static string Describe(in StepDownDecision decision, (int Width, int Height, int Fps, double BitrateMbps) landed)
=> decision.Action switch
{
StepAction.LowerFrameRate =>
"LlamaCasty couldn't keep up with your scene, so it lowered the frame rate to " +
$"{landed.Fps}. Your connection wasn't the problem.",
StepAction.LowerResolution =>
"LlamaCasty couldn't keep up with your scene, so it lowered the picture to " +
$"{landed.Width}×{landed.Height}. Your connection wasn't the problem.",
StepAction.LowerBitrate =>
$"Your upload couldn't hold the bitrate, so LlamaCasty lowered it to {landed.BitrateMbps:0.0} Mbps " +
"to stop the freezing. It will stay there for this stream.",
StepAction.RaiseFrameRate or StepAction.RaiseResolution or StepAction.RaiseBitrate =>
"Conditions were clean for a while, so LlamaCasty restored one step of quality.",
_ => string.Empty,
};
/// <summary>One line for the session report, so a later question is answered from data.</summary>
public static string ReportLine(DateTime whenLocal, in StepDownDecision decision) =>
decision.Action switch
{
StepAction.None => string.Empty,
StepAction.LowerFrameRate or StepAction.LowerResolution =>
$"{whenLocal:HH:mm} quality stepped down — encoder ({decision.Why})",
StepAction.LowerBitrate =>
$"{whenLocal:HH:mm} quality stepped down — upload ({decision.Why})",
_ => $"{whenLocal:HH:mm} quality restored ({decision.Why})",
};
}