perf: coalesce superseded queued playback seeks
This commit is contained in:
@@ -200,7 +200,8 @@ struct CastPlaybackCheck {
|
||||
require(!model.canStepFrame && model.bufferedRange == nil, "local decoder-only controls are not exposed while casting")
|
||||
require(model.setPlayback(playing: true), "remote play accepted")
|
||||
try await until(model, "receiver play acknowledged") { model.state.status == PLAYER_PLAYING && model.playbackIntent == nil }
|
||||
require(model.seekRelative(5) && model.seekRelative(10), "rapid remote skips accumulate before acknowledgement")
|
||||
for _ in 0..<64 { require(model.seekRelative(0.125), "rapid remote skips accumulate beyond queue capacity") }
|
||||
require(model.seekRelative(7), "final remote skip retains accumulated target")
|
||||
try await until(model, "receiver seek acknowledged") { !model.state.seek_pending && abs(model.state.position - 24.25) < 0.15 }
|
||||
require(model.setPlayback(playing: false), "remote pause accepted")
|
||||
try await until(model, "receiver pause acknowledged") { model.state.status == PLAYER_PAUSED && model.playbackIntent == nil }
|
||||
|
||||
@@ -399,6 +399,39 @@ struct CastSessionCheck {
|
||||
language: "en", label: "English")])
|
||||
}
|
||||
|
||||
static func seekCoalescing(identity: SecIdentity) async throws {
|
||||
let peer = try Receiver(identity: identity)
|
||||
try await peer.start()
|
||||
defer { peer.stop() }
|
||||
let clock = CastCheckClock()
|
||||
let session = CastSession(sleep: { try await clock.sleep($0) })
|
||||
var status: CastStatus?
|
||||
var errors: [String] = []
|
||||
session.onStatus = { status = $0 }
|
||||
session.onError = { errors.append($0) }
|
||||
try await session.connect(to: peer.device)
|
||||
try await session.load(media(), title: "Seek burst", position: 0, playing: false, subtitle: nil)
|
||||
let before = peer.commands.count
|
||||
for target in 1...64 { require(session.seek(Double(target)), "Seek burst exceeds ordinary queue capacity") }
|
||||
require(session.setPlaying(true), "PLAY separates seek runs")
|
||||
for target in 65...96 { require(session.seek(Double(target)), "Second seek run accepted") }
|
||||
require(session.setPlaying(false), "PAUSE separates seek runs")
|
||||
for target in (48...63).reversed() { require(session.seek(Double(target)), "Reverse seek run accepted") }
|
||||
require(status?.seekPending == true, "Latest queued seek remains unacknowledged")
|
||||
try await until("Final seek and playback acknowledged") {
|
||||
status?.position == 48 && status?.seekPending == false &&
|
||||
status?.playing == false && status?.playbackPending == false
|
||||
}
|
||||
let controls = peer.commands.dropFirst(before).filter { ["SEEK", "PLAY", "PAUSE"].contains($0.0) }
|
||||
let targets = controls.filter { $0.0 == "SEEK" }.compactMap { $0.1["currentTime"] as? Double }
|
||||
require(targets == [64, 96, 48] || targets == [1, 64, 96, 48],
|
||||
"Only unsent adjacent seeks are replaced")
|
||||
require(Array(controls.suffix(5)).map(\.0) == ["SEEK", "PLAY", "SEEK", "PAUSE", "SEEK"],
|
||||
"Playback controls remain seek ordering barriers")
|
||||
require(errors.isEmpty && peer.failure == nil, "Superseded requests do not fail or leak queue capacity")
|
||||
session.disconnect()
|
||||
}
|
||||
|
||||
static func behavior(identity: SecIdentity) async throws {
|
||||
let peer = try Receiver(identity: identity)
|
||||
peer.muted = true
|
||||
@@ -756,10 +789,6 @@ struct CastSessionCheck {
|
||||
let load = peer.commands.first { $0.0 == "LOAD" }!.1
|
||||
require(load["currentTime"] as? Double == 9.25 && load["autoplay"] as? Bool == false, "Acknowledged paused handoff")
|
||||
require(peer.relayVerified, "Receiver fetches real relay HLS and media bytes")
|
||||
let controls = peer.commands.filter { ["PLAY", "SEEK", "PAUSE", "STOP"].contains($0.0) }
|
||||
require(controls.map(\.0) == ["PLAY", "SEEK", "SEEK", "PAUSE", "STOP"], "Integrated controls ordered")
|
||||
require(controls[1].1["currentTime"] as? Double == 14.25 &&
|
||||
controls[2].1["currentTime"] as? Double == 24.25, "Rapid skips accumulate from the pending target")
|
||||
}
|
||||
|
||||
static func interruptedPlayback(identity: SecIdentity, executable: String, file: String) async throws {
|
||||
@@ -886,6 +915,7 @@ struct CastSessionCheck {
|
||||
print("PASS: interrupted Cast connect/LOAD, rejection, replacement/close, latest intent and acknowledged-position recovery")
|
||||
} else {
|
||||
try await behavior(identity: identity)
|
||||
try await seekCoalescing(identity: identity)
|
||||
try await launchRegressions(identity: identity)
|
||||
try await volumeRegressions(identity: identity)
|
||||
try await failures(identity: identity)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Include the implementation to inspect pending commands without adding a public test API.
|
||||
#include "../Sources/Core/player.cpp"
|
||||
#include <cstdlib>
|
||||
|
||||
static void require(bool condition, const char *message) {
|
||||
if (!condition) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); }
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
Player player;
|
||||
// Join before submitting anything: the queue exercise has no executing worker,
|
||||
// media I/O, device clock or scheduling-dependent queue depth.
|
||||
player.shutdown();
|
||||
player.stopping.store(false);
|
||||
auto reset = [&] {
|
||||
player.commands.clear();
|
||||
player.state = {};
|
||||
player.state.status = PLAYER_PAUSED;
|
||||
player.state.can_seek = true;
|
||||
player.state.has_video = true;
|
||||
player.publishedEpoch = player.requestedEpoch.load();
|
||||
};
|
||||
reset();
|
||||
player.state.position = 7;
|
||||
player.state.buffered_start = 6;
|
||||
player.state.buffered_end = 9;
|
||||
constexpr unsigned requests = 100000;
|
||||
unsigned accepted = 0;
|
||||
for (unsigned i = 0; i < requests; ++i)
|
||||
accepted += player_seek(&player, double(i));
|
||||
std::printf("Native pending seek burst: requested=%u accepted=%u queued=%zu latest=%.0f\n",
|
||||
requests, accepted, player.commands.size(), player.commands.back().seconds);
|
||||
if (argc == 2 && std::strcmp(argv[1], "--measure") == 0) return 0;
|
||||
require(argc == 1, "only --measure is supported");
|
||||
require(accepted == requests && player.commands.size() == 1 &&
|
||||
player.commands.back().seconds == requests - 1,
|
||||
"every superseding seek is accepted and only the latest target remains");
|
||||
require(player.state.seek_pending && player.state.revision == requests &&
|
||||
player.state.buffered_start == 0 && player.state.buffered_end == 0 &&
|
||||
player.state.position == 7 && player.state.status == PLAYER_PAUSED,
|
||||
"each accepted seek publishes pending and invalidates buffering, not acknowledged position");
|
||||
|
||||
reset();
|
||||
for (std::size_t i = 0; i < commandLimit - 1; ++i)
|
||||
require(player_pause(&player), "fill queue with command barriers");
|
||||
require(player_seek(&player, 1) && player_seek(&player, 2),
|
||||
"latest queued seek can be replaced even at capacity");
|
||||
require(player.commands.size() == commandLimit && player.commands.back().seconds == 2 &&
|
||||
!player_play(&player), "coalescing does not relax capacity for other commands");
|
||||
|
||||
for (Action barrier : {Action::Play, Action::Pause, Action::Step,
|
||||
Action::Open, Action::Close, Action::Remote}) {
|
||||
reset();
|
||||
require(player_seek(&player, 1), "seek before barrier");
|
||||
Command command{barrier, {}};
|
||||
command.direction = 1;
|
||||
require(player.enqueue(std::move(command)) && player_seek(&player, 2), "seek after barrier");
|
||||
require(player.commands.size() == 3 && player.commands[0].action == Action::Seek &&
|
||||
player.commands[0].seconds == 1 && player.commands[1].action == barrier &&
|
||||
player.commands[2].action == Action::Seek && player.commands[2].seconds == 2,
|
||||
"no seek replacement crosses another action");
|
||||
if (barrier == Action::Open || barrier == Action::Close)
|
||||
require(player.commands[0].epoch != player.commands[2].epoch,
|
||||
"timeline replacement separates seek epochs");
|
||||
}
|
||||
reset();
|
||||
require(player_seek(&player, 1), "seek before API error boundary");
|
||||
player.apiError("queue check boundary");
|
||||
require(player_seek(&player, 2) && player.commands.size() == 2 &&
|
||||
player.commands[0].epoch != player.commands[1].epoch,
|
||||
"adjacent seeks from different epochs remain distinct");
|
||||
|
||||
reset();
|
||||
require(player_seek(&player, 1), "first seek queued");
|
||||
auto executing = std::move(player.commands.front());
|
||||
player.commands.pop_front(); // The worker removes a seek before executing it.
|
||||
require(player_seek(&player, 2) && player_seek(&player, 3) &&
|
||||
executing.seconds == 1 && player.commands.size() == 1 && player.commands.back().seconds == 3,
|
||||
"replacement never changes a seek already removed for execution");
|
||||
std::puts("PASS native seek burst/capacity/publication/action barriers/epochs/executing seek");
|
||||
}
|
||||
@@ -170,7 +170,7 @@ struct SystemPlaybackChecks {
|
||||
require(commands.nextTrackCommand.isEnabled,
|
||||
"seekability changes re-enable commands even when playback metadata is unchanged")
|
||||
|
||||
require(model.seekRelative(1), "relative seek accepted")
|
||||
for _ in 0..<64 { require(model.seekRelative(1.0 / 64), "rapid native skips retain accumulated target") }
|
||||
until("paused seek acknowledged") {
|
||||
model.state.status == PLAYER_PAUSED && !model.state.seek_pending &&
|
||||
abs(model.state.position - settledPosition - 1) < 0.05
|
||||
|
||||
Reference in New Issue
Block a user