Stage 1 rewrite
This commit is contained in:
@@ -129,6 +129,7 @@ void TestBackendPlayoutHealth()
|
||||
Expect(playout.outputFrameEndAccessMilliseconds == 0.5, "backend playout health stores output frame end access duration");
|
||||
Expect(playout.completedFrameIndex == 8, "backend playout health stores completed index");
|
||||
Expect(playout.scheduledFrameIndex == 11, "backend playout health stores scheduled index");
|
||||
Expect(playout.scheduledLeadFrames == 3, "backend playout health stores synthetic scheduled lead");
|
||||
Expect(playout.measuredLagFrames == 2, "backend playout health stores measured lag");
|
||||
Expect(playout.catchUpFrames == 2, "backend playout health stores catch-up frames");
|
||||
Expect(playout.lateStreak == 1, "backend playout health stores late streak");
|
||||
@@ -228,6 +229,28 @@ void TestSystemMemoryPlayoutStats()
|
||||
Expect(playout.systemFrameAgeAtScheduleMilliseconds == 0.0, "system-memory playout clamps negative schedule age");
|
||||
Expect(playout.systemFrameAgeAtCompletionMilliseconds == 0.0, "system-memory playout clamps negative completion age");
|
||||
}
|
||||
|
||||
void TestDeckLinkBufferTelemetry()
|
||||
{
|
||||
HealthTelemetry telemetry;
|
||||
telemetry.RecordDeckLinkBufferTelemetry(true, 4, 5, 0.25, 2);
|
||||
|
||||
HealthTelemetry::BackendPlayoutSnapshot playout = telemetry.GetBackendPlayoutSnapshot();
|
||||
Expect(playout.actualDeckLinkBufferedFramesAvailable, "DeckLink buffer telemetry records availability");
|
||||
Expect(playout.actualDeckLinkBufferedFrames == 4, "DeckLink buffer telemetry stores actual device depth");
|
||||
Expect(playout.targetDeckLinkBufferedFrames == 5, "DeckLink buffer telemetry stores target device depth");
|
||||
Expect(playout.deckLinkScheduleCallMilliseconds == 0.25, "DeckLink buffer telemetry stores schedule call duration");
|
||||
Expect(playout.deckLinkScheduleFailureCount == 2, "DeckLink buffer telemetry stores schedule failures");
|
||||
|
||||
Expect(telemetry.TryRecordDeckLinkBufferTelemetry(false, 9, 3, -1.0, 7),
|
||||
"try DeckLink buffer telemetry succeeds when uncontended");
|
||||
playout = telemetry.GetBackendPlayoutSnapshot();
|
||||
Expect(!playout.actualDeckLinkBufferedFramesAvailable, "DeckLink buffer telemetry records unavailable device depth");
|
||||
Expect(playout.actualDeckLinkBufferedFrames == 0, "unavailable DeckLink device depth clears actual count");
|
||||
Expect(playout.targetDeckLinkBufferedFrames == 3, "try DeckLink buffer telemetry stores target device depth");
|
||||
Expect(playout.deckLinkScheduleCallMilliseconds == 0.0, "DeckLink buffer telemetry clamps negative schedule call duration");
|
||||
Expect(playout.deckLinkScheduleFailureCount == 7, "try DeckLink buffer telemetry stores schedule failures");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
@@ -239,6 +262,7 @@ int main()
|
||||
TestBackendPlayoutHealth();
|
||||
TestOutputRenderPipelineTiming();
|
||||
TestSystemMemoryPlayoutStats();
|
||||
TestDeckLinkBufferTelemetry();
|
||||
|
||||
if (gFailures != 0)
|
||||
{
|
||||
|
||||
172
tests/RenderCadenceControllerTests.cpp
Normal file
172
tests/RenderCadenceControllerTests.cpp
Normal file
@@ -0,0 +1,172 @@
|
||||
#include "RenderCadenceController.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace
|
||||
{
|
||||
int gFailures = 0;
|
||||
|
||||
using Clock = RenderCadenceController::Clock;
|
||||
using Duration = RenderCadenceController::Duration;
|
||||
using TimePoint = RenderCadenceController::TimePoint;
|
||||
|
||||
void Expect(bool condition, const char* message)
|
||||
{
|
||||
if (condition)
|
||||
return;
|
||||
|
||||
std::cerr << "FAIL: " << message << "\n";
|
||||
++gFailures;
|
||||
}
|
||||
|
||||
Duration Ms(int64_t value)
|
||||
{
|
||||
return std::chrono::duration_cast<Duration>(std::chrono::milliseconds(value));
|
||||
}
|
||||
|
||||
void TestExactCadenceAdvancesFrameIndexAndNextTick()
|
||||
{
|
||||
RenderCadenceController controller;
|
||||
const TimePoint start = Clock::time_point(Ms(1000));
|
||||
controller.Configure(Ms(20), start);
|
||||
|
||||
RenderCadenceDecision first = controller.Tick(start);
|
||||
Expect(first.action == RenderCadenceAction::Render, "first exact tick renders");
|
||||
Expect(first.frameIndex == 0, "first exact tick renders frame zero");
|
||||
Expect(first.renderTargetTime == start, "first exact target is configured start");
|
||||
Expect(first.nextRenderTime == start + Ms(20), "first exact tick advances next render time");
|
||||
Expect(first.skippedTicks == 0, "first exact tick skips no ticks");
|
||||
Expect(first.lateness == Duration::zero(), "first exact tick records no lateness");
|
||||
|
||||
RenderCadenceDecision second = controller.Tick(start + Ms(20));
|
||||
Expect(second.action == RenderCadenceAction::Render, "second exact tick renders");
|
||||
Expect(second.frameIndex == 1, "second exact tick renders frame one");
|
||||
Expect(controller.NextFrameIndex() == 2, "controller tracks next frame index after exact ticks");
|
||||
Expect(controller.Metrics().renderedFrameCount == 2, "metrics count exact rendered frames");
|
||||
}
|
||||
|
||||
void TestEarlyTickWaitsWithoutAdvancing()
|
||||
{
|
||||
RenderCadenceController controller;
|
||||
const TimePoint start = Clock::time_point(Ms(0));
|
||||
controller.Configure(Ms(20), start);
|
||||
(void)controller.Tick(start);
|
||||
|
||||
RenderCadenceDecision decision = controller.Tick(start + Ms(10));
|
||||
Expect(decision.action == RenderCadenceAction::Wait, "early tick waits");
|
||||
Expect(decision.waitDuration == Ms(10), "early tick reports wait duration");
|
||||
Expect(decision.frameIndex == 1, "early tick reports next pending frame");
|
||||
Expect(controller.NextFrameIndex() == 1, "early tick does not advance frame index");
|
||||
Expect(controller.NextRenderTime() == start + Ms(20), "early tick does not advance next render time");
|
||||
}
|
||||
|
||||
void TestSlightLatenessRendersAndRecordsMetrics()
|
||||
{
|
||||
RenderCadencePolicy policy;
|
||||
policy.skipThresholdFrames = 3.0;
|
||||
|
||||
RenderCadenceController controller;
|
||||
const TimePoint start = Clock::time_point(Ms(0));
|
||||
controller.Configure(Ms(20), start, policy);
|
||||
|
||||
RenderCadenceDecision decision = controller.Tick(start + Ms(5));
|
||||
Expect(decision.action == RenderCadenceAction::Render, "slightly late tick renders");
|
||||
Expect(decision.frameIndex == 0, "slightly late tick keeps pending frame");
|
||||
Expect(decision.skippedTicks == 0, "slightly late tick skips no ticks");
|
||||
Expect(decision.lateness == Ms(5), "slightly late tick reports lateness");
|
||||
Expect(controller.Metrics().lateFrameCount == 1, "metrics count late rendered frame");
|
||||
Expect(controller.Metrics().lastLateness == Ms(5), "metrics keep last lateness");
|
||||
Expect(controller.Metrics().maxLateness == Ms(5), "metrics keep max lateness");
|
||||
}
|
||||
|
||||
void TestLargeLatenessSkipsTicksAccordingToPolicy()
|
||||
{
|
||||
RenderCadencePolicy policy;
|
||||
policy.skipLateTicks = true;
|
||||
policy.skipThresholdFrames = 2.0;
|
||||
policy.maxSkippedTicksPerDecision = 8;
|
||||
|
||||
RenderCadenceController controller;
|
||||
const TimePoint start = Clock::time_point(Ms(0));
|
||||
controller.Configure(Ms(20), start, policy);
|
||||
|
||||
RenderCadenceDecision decision = controller.Tick(start + Ms(70));
|
||||
Expect(decision.action == RenderCadenceAction::Render, "large late tick renders newest allowed frame");
|
||||
Expect(decision.skippedTicks == 3, "large late tick skips elapsed render ticks");
|
||||
Expect(decision.frameIndex == 3, "large late tick renders skipped-to frame");
|
||||
Expect(decision.renderTargetTime == start + Ms(60), "large late tick targets newest elapsed tick");
|
||||
Expect(decision.lateness == Ms(10), "large late tick measures residual lateness");
|
||||
Expect(controller.NextFrameIndex() == 4, "large late tick advances past rendered frame");
|
||||
Expect(controller.NextRenderTime() == start + Ms(80), "large late tick advances to following cadence");
|
||||
Expect(controller.Metrics().skippedTickCount == 3, "metrics count skipped ticks");
|
||||
}
|
||||
|
||||
void TestSkipPolicyCanDisableOrCapSkippedTicks()
|
||||
{
|
||||
const TimePoint start = Clock::time_point(Ms(0));
|
||||
|
||||
RenderCadencePolicy disabledPolicy;
|
||||
disabledPolicy.skipLateTicks = false;
|
||||
RenderCadenceController disabledController;
|
||||
disabledController.Configure(Ms(20), start, disabledPolicy);
|
||||
RenderCadenceDecision disabled = disabledController.Tick(start + Ms(90));
|
||||
Expect(disabled.skippedTicks == 0, "disabled skip policy renders pending frame");
|
||||
Expect(disabled.frameIndex == 0, "disabled skip policy preserves pending frame index");
|
||||
|
||||
RenderCadencePolicy cappedPolicy;
|
||||
cappedPolicy.skipThresholdFrames = 1.0;
|
||||
cappedPolicy.maxSkippedTicksPerDecision = 2;
|
||||
RenderCadenceController cappedController;
|
||||
cappedController.Configure(Ms(20), start, cappedPolicy);
|
||||
RenderCadenceDecision capped = cappedController.Tick(start + Ms(90));
|
||||
Expect(capped.skippedTicks == 2, "skip policy caps skipped ticks");
|
||||
Expect(capped.frameIndex == 2, "capped skip renders capped frame index");
|
||||
}
|
||||
|
||||
void TestResetRestartsCadenceAndMetrics()
|
||||
{
|
||||
RenderCadenceController controller;
|
||||
const TimePoint start = Clock::time_point(Ms(0));
|
||||
controller.Configure(Ms(20), start);
|
||||
(void)controller.Tick(start + Ms(50));
|
||||
|
||||
const TimePoint restarted = start + Ms(200);
|
||||
controller.Reset(restarted);
|
||||
|
||||
Expect(controller.NextFrameIndex() == 0, "reset restarts frame index");
|
||||
Expect(controller.NextRenderTime() == restarted, "reset restarts next render time");
|
||||
Expect(controller.Metrics().renderedFrameCount == 0, "reset clears rendered metrics");
|
||||
|
||||
RenderCadenceDecision decision = controller.Tick(restarted);
|
||||
Expect(decision.action == RenderCadenceAction::Render, "reset cadence renders at new start");
|
||||
Expect(decision.frameIndex == 0, "reset cadence renders frame zero");
|
||||
}
|
||||
|
||||
void TestActionNames()
|
||||
{
|
||||
Expect(RenderCadenceActionName(RenderCadenceAction::Render) == std::string("Render"), "render action has name");
|
||||
Expect(RenderCadenceActionName(RenderCadenceAction::Wait) == std::string("Wait"), "wait action has name");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
TestExactCadenceAdvancesFrameIndexAndNextTick();
|
||||
TestEarlyTickWaitsWithoutAdvancing();
|
||||
TestSlightLatenessRendersAndRecordsMetrics();
|
||||
TestLargeLatenessSkipsTicksAccordingToPolicy();
|
||||
TestSkipPolicyCanDisableOrCapSkippedTicks();
|
||||
TestResetRestartsCadenceAndMetrics();
|
||||
TestActionNames();
|
||||
|
||||
if (gFailures != 0)
|
||||
{
|
||||
std::cerr << gFailures << " RenderCadenceController test failure(s).\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "RenderCadenceController tests passed.\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -48,10 +48,50 @@ void TestAcquireHonorsCapacityAndFrameShape()
|
||||
|
||||
SystemOutputFramePoolMetrics metrics = pool.GetMetrics();
|
||||
Expect(metrics.freeCount == 0, "all slots are in use");
|
||||
Expect(metrics.renderingCount == 2, "rendering slots are counted");
|
||||
Expect(metrics.acquiredCount == 2, "acquired slots are counted");
|
||||
Expect(metrics.acquireMissCount == 1, "capacity miss is counted");
|
||||
}
|
||||
|
||||
void TestPhase77StateContract()
|
||||
{
|
||||
SystemOutputFramePool pool(MakeConfig(1));
|
||||
|
||||
SystemOutputFramePoolMetrics metrics = pool.GetMetrics();
|
||||
Expect(metrics.freeCount == 1, "new pool starts with one free slot");
|
||||
Expect(metrics.renderingCount == 0, "new pool starts with no rendering slots");
|
||||
Expect(metrics.completedCount == 0, "new pool starts with no completed slots");
|
||||
Expect(metrics.scheduledCount == 0, "new pool starts with no scheduled slots");
|
||||
|
||||
OutputFrameSlot slot;
|
||||
Expect(pool.AcquireRenderingSlot(slot), "free slot moves to rendering");
|
||||
metrics = pool.GetMetrics();
|
||||
Expect(metrics.freeCount == 0, "rendering slot leaves free pool");
|
||||
Expect(metrics.renderingCount == 1, "rendering slot is counted");
|
||||
|
||||
Expect(pool.PublishCompletedSlot(slot), "rendering slot moves to completed");
|
||||
metrics = pool.GetMetrics();
|
||||
Expect(metrics.renderingCount == 0, "completed slot leaves rendering");
|
||||
Expect(metrics.completedCount == 1, "completed slot is counted");
|
||||
Expect(metrics.readyCount == 1, "completed slot is available to scheduler");
|
||||
|
||||
OutputFrameSlot completed;
|
||||
Expect(pool.ConsumeCompletedSlot(completed), "completed slot can be dequeued for scheduling");
|
||||
metrics = pool.GetMetrics();
|
||||
Expect(metrics.completedCount == 1, "dequeued completed slot remains completed until scheduled");
|
||||
Expect(metrics.readyCount == 0, "dequeued completed slot leaves ready queue");
|
||||
|
||||
Expect(pool.MarkScheduled(completed), "completed slot moves to scheduled");
|
||||
metrics = pool.GetMetrics();
|
||||
Expect(metrics.completedCount == 0, "scheduled slot leaves completed state");
|
||||
Expect(metrics.scheduledCount == 1, "scheduled slot is counted");
|
||||
|
||||
Expect(pool.ReleaseScheduledSlot(completed), "scheduled slot returns to free");
|
||||
metrics = pool.GetMetrics();
|
||||
Expect(metrics.freeCount == 1, "released scheduled slot returns to free");
|
||||
Expect(metrics.scheduledCount == 0, "released scheduled slot leaves scheduled state");
|
||||
}
|
||||
|
||||
void TestReadySlotsAreConsumedFifo()
|
||||
{
|
||||
SystemOutputFramePool pool(MakeConfig(2));
|
||||
@@ -78,6 +118,25 @@ void TestReadySlotsAreConsumedFifo()
|
||||
Expect(metrics.readyCount == 0, "ready queue is empty after consumption");
|
||||
}
|
||||
|
||||
void TestCompletedSlotCannotBeAcquiredUntilReleased()
|
||||
{
|
||||
SystemOutputFramePool pool(MakeConfig(1));
|
||||
|
||||
OutputFrameSlot slot;
|
||||
OutputFrameSlot extra;
|
||||
Expect(pool.AcquireRenderingSlot(slot), "single slot can be acquired for rendering");
|
||||
Expect(pool.PublishCompletedSlot(slot), "single slot can be published completed");
|
||||
Expect(!pool.AcquireRenderingSlot(extra), "completed slot is not available for rendering");
|
||||
|
||||
OutputFrameSlot completed;
|
||||
Expect(pool.ConsumeCompletedSlot(completed), "completed slot can be dequeued");
|
||||
Expect(!pool.AcquireRenderingSlot(extra), "dequeued completed slot is still not free");
|
||||
Expect(pool.MarkScheduled(completed), "dequeued completed slot can be scheduled");
|
||||
Expect(!pool.AcquireRenderingSlot(extra), "scheduled slot is still not free");
|
||||
Expect(pool.ReleaseScheduledSlot(completed), "scheduled slot can be released");
|
||||
Expect(pool.AcquireRenderingSlot(extra), "released slot can be acquired again");
|
||||
}
|
||||
|
||||
void TestReadySlotCanBeScheduledByBuffer()
|
||||
{
|
||||
SystemOutputFramePool pool(MakeConfig(1));
|
||||
@@ -153,7 +212,9 @@ void TestEmptyReadyQueueUnderrunIsCounted()
|
||||
int main()
|
||||
{
|
||||
TestAcquireHonorsCapacityAndFrameShape();
|
||||
TestPhase77StateContract();
|
||||
TestReadySlotsAreConsumedFifo();
|
||||
TestCompletedSlotCannotBeAcquiredUntilReleased();
|
||||
TestReadySlotCanBeScheduledByBuffer();
|
||||
TestInvalidTransitionsAreRejected();
|
||||
TestPixelFormatAwareSizing();
|
||||
|
||||
@@ -39,8 +39,11 @@ void TestScheduleAdvancesFromZero()
|
||||
|
||||
void TestLateAndDroppedRecoveryUsesMeasuredPressure()
|
||||
{
|
||||
VideoPlayoutPolicy policy;
|
||||
policy.lateOrDropCatchUpFrames = 2;
|
||||
|
||||
VideoPlayoutScheduler scheduler;
|
||||
scheduler.Configure(1000, 50000);
|
||||
scheduler.Configure(1000, 50000, policy);
|
||||
|
||||
(void)scheduler.NextScheduleTime();
|
||||
VideoPlayoutRecoveryDecision lateDecision = scheduler.AccountForCompletionResult(VideoIOCompletionResult::DisplayedLate, 2);
|
||||
@@ -55,6 +58,18 @@ void TestLateAndDroppedRecoveryUsesMeasuredPressure()
|
||||
Expect(scheduler.NextScheduleTime().streamTime == 5000, "drop recovery advances by measured lag");
|
||||
}
|
||||
|
||||
void TestDefaultPolicyReportsLagWithoutSkippingScheduleTime()
|
||||
{
|
||||
VideoPlayoutScheduler scheduler;
|
||||
scheduler.Configure(1000, 50000);
|
||||
|
||||
(void)scheduler.NextScheduleTime();
|
||||
VideoPlayoutRecoveryDecision decision = scheduler.AccountForCompletionResult(VideoIOCompletionResult::Dropped, 0);
|
||||
Expect(decision.measuredLagFrames > 0, "default policy still measures dropped-frame lag");
|
||||
Expect(decision.catchUpFrames == 0, "default policy does not skip schedule time");
|
||||
Expect(scheduler.NextScheduleTime().streamTime == 1000, "default recovery keeps stream time continuous");
|
||||
}
|
||||
|
||||
void TestMeasuredRecoveryIsCappedByPolicy()
|
||||
{
|
||||
VideoPlayoutPolicy policy;
|
||||
@@ -117,6 +132,7 @@ int main()
|
||||
{
|
||||
TestScheduleAdvancesFromZero();
|
||||
TestLateAndDroppedRecoveryUsesMeasuredPressure();
|
||||
TestDefaultPolicyReportsLagWithoutSkippingScheduleTime();
|
||||
TestMeasuredRecoveryIsCappedByPolicy();
|
||||
TestCleanCompletionTracksCompletedIndexAndClearsStreaks();
|
||||
TestPolicyNormalization();
|
||||
|
||||
Reference in New Issue
Block a user