perf: prioritize audio preroll after paused network previews

This commit is contained in:
2026-10-17 02:00:04 +02:00
parent e1284936f9
commit 0aa0ed781f
5 changed files with 118 additions and 47 deletions
+53 -42
View File
@@ -133,7 +133,9 @@ static bool uncached(PlayerState state, double target) {
int main(int argc, char **argv) {
@autoreleasepool {
require(argc == 3, "usage: playback-benchmark <loopback-base-url> <split|hls>");
require(argc == 3 || (argc == 4 && std::string(argv[3]) == "--seek-work"),
"usage: playback-benchmark <loopback-base-url> <split|hls> [--seek-work]");
const bool seekWork = argc == 4;
const std::string base = argv[1], mode = argv[2];
require(base.rfind("http://127.0.0.1:", 0) == 0 && base.size() > 17 && base.size() <= 22 &&
base.find_first_not_of("0123456789", 17) == std::string::npos &&
@@ -185,48 +187,57 @@ int main(int argc, char **argv) {
@"probe_media_seconds": @(state.position), @"render_width": @1280, @"render_height": @720});
constexpr double hitTarget = 20.5, missTarget = 100.5;
// Waiting for retained packet coverage is preparation, not seek latency.
until(player.get(), [](auto s) { return cached(s, hitTarget); }, "cached target coverage");
begin(@"cached_seek");
auto before = player_get_state(player.get());
require(cached(before, hitTarget), "cached seek target and GOP must remain inside retained coverage");
const auto hitStart = stamp();
state = seek(player.get(), hitTarget, "cached paused decoded seek");
[view draw];
const auto hitEnd = stamp();
finish(@"cached_seek", hitStart, hitEnd, state,
@{@"target_seconds": @(hitTarget), @"cache_before_start": @(before.buffered_start),
@"cache_before_end": @(before.buffered_end), @"cache_precondition": @"retained_packet_hit"});
auto before = state;
if (seekWork) {
// Preparation is outside the measured phase. Seek directly from the
// opening paused position, without the cached seek or paced playback.
until(player.get(), [](auto s) {
return pausedReady(s) && s.buffered_end >= 59;
}, "initial read-ahead coverage");
} else {
// Waiting for retained packet coverage is preparation, not seek latency.
until(player.get(), [](auto s) { return cached(s, hitTarget); }, "cached target coverage");
begin(@"cached_seek");
before = player_get_state(player.get());
require(cached(before, hitTarget), "cached seek target and GOP must remain inside retained coverage");
const auto hitStart = stamp();
state = seek(player.get(), hitTarget, "cached paused decoded seek");
[view draw];
const auto hitEnd = stamp();
finish(@"cached_seek", hitStart, hitEnd, state,
@{@"target_seconds": @(hitTarget), @"cache_before_start": @(before.buffered_start),
@"cache_before_end": @(before.buffered_end), @"cache_precondition": @"retained_packet_hit"});
// Sustained is real paced playback from the cached seek, including
// moving-frontier read-ahead, start and final pause acknowledgement.
// Position is the actual native audio-master clock. Only watchdogs impose
// elapsed-time limits; the completion condition is twelve media seconds.
begin(@"sustained");
before = player_get_state(player.get());
require(pausedReady(before) && std::abs(before.position - hitTarget) < .05 &&
before.duration > before.position + 12, "sustained starts at acknowledged cached target");
const auto sustainedStart = stamp();
auto previousPoll = sustainedStart.wall;
bool wasBuffering = true;
double bufferingMilliseconds = 0;
require(player_play(player.get()), "start paced sustained playback");
state = until(player.get(), [&](auto s) {
const auto now = Clock::now();
if (wasBuffering)
bufferingMilliseconds += std::chrono::duration<double, std::milli>(now - previousPoll).count();
previousPoll = now;
wasBuffering = s.buffering;
return s.status == PLAYER_PLAYING && !s.seek_pending && !s.buffering &&
s.position >= before.position + 12;
}, "twelve media seconds of native playback", view);
require(player_pause(player.get()), "pause sustained playback");
state = until(player.get(), pausedReady, "sustained pause acknowledgement", view);
const auto sustainedEnd = stamp();
finish(@"sustained", sustainedStart, sustainedEnd, state,
@{@"media_seconds": @(state.position - before.position), @"paced": @YES,
@"start_position_seconds": @(before.position), @"buffering_ms": @(bufferingMilliseconds),
@"buffering_measurement": @"sampled_at_approximately_1ms"});
// Sustained is real paced playback from the cached seek, including
// moving-frontier read-ahead, start and final pause acknowledgement.
// Position is the actual native audio-master clock. Only watchdogs impose
// elapsed-time limits; the completion condition is twelve media seconds.
begin(@"sustained");
before = player_get_state(player.get());
require(pausedReady(before) && std::abs(before.position - hitTarget) < .05 &&
before.duration > before.position + 12, "sustained starts at acknowledged cached target");
const auto sustainedStart = stamp();
auto previousPoll = sustainedStart.wall;
bool wasBuffering = true;
double bufferingMilliseconds = 0;
require(player_play(player.get()), "start paced sustained playback");
state = until(player.get(), [&](auto s) {
const auto now = Clock::now();
if (wasBuffering)
bufferingMilliseconds += std::chrono::duration<double, std::milli>(now - previousPoll).count();
previousPoll = now;
wasBuffering = s.buffering;
return s.status == PLAYER_PLAYING && !s.seek_pending && !s.buffering &&
s.position >= before.position + 12;
}, "twelve media seconds of native playback", view);
require(player_pause(player.get()), "pause sustained playback");
state = until(player.get(), pausedReady, "sustained pause acknowledgement", view);
const auto sustainedEnd = stamp();
finish(@"sustained", sustainedStart, sustainedEnd, state,
@{@"media_seconds": @(state.position - before.position), @"paced": @YES,
@"start_position_seconds": @(before.position), @"buffering_ms": @(bufferingMilliseconds),
@"buffering_measurement": @"sampled_at_approximately_1ms"});
}
begin(@"uncached_seek");
before = player_get_state(player.get());
+47 -4
View File
@@ -59,9 +59,11 @@ class BenchmarkServer(StreamingServer):
"connections": len(self.connections)}
def sample(binary: Path, cast: bool, source: str, server: BenchmarkServer):
def sample(binary: Path, cast: bool, source: str, server: BenchmarkServer, *, seek_work: bool = False):
command = [str(binary.resolve())] + (["--playback-benchmark"] if cast else [])
command += [server.base_url, source]
if seek_work:
command.append("--seek-work")
rows = []
# A process deadline is infrastructure protection, never a measured playback clock.
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
@@ -102,8 +104,10 @@ def sample(binary: Path, cast: bool, source: str, server: BenchmarkServer):
if process.poll() is None:
process.kill()
process.wait()
if sorted(row["phase"] for row in rows) != ["cached_seek", "cold_start", "sustained", "uncached_seek"]:
raise RuntimeError("benchmark must report all four phases exactly once")
expected_phases = (["cold_start", "uncached_seek"] if seek_work else
["cached_seek", "cold_start", "sustained", "uncached_seek"])
if sorted(row["phase"] for row in rows) != expected_phases:
raise RuntimeError(f"benchmark must report each expected phase exactly once: {expected_phases}")
return rows
@@ -114,13 +118,52 @@ def main():
parser.add_argument("fixtures", type=Path)
parser.add_argument("--runs", type=int, default=3)
parser.add_argument("--request-delay-ms", type=float, nargs="+", default=[0, 25])
parser.add_argument("--compare-native", type=Path, metavar="BASELINE_BINARY")
parser.add_argument("--seek-work", action="store_true")
args = parser.parse_args()
if args.runs < 1 or any(value < 0 for value in args.request_delay_ms):
parser.error("runs must be positive and delays nonnegative")
if args.seek_work and not args.compare_native:
parser.error("--seek-work requires --compare-native")
fixtures(args.fixtures)
print(json.dumps({"environment": platform.platform(), "machine": platform.machine(),
"runs": args.runs, "request_delay_ms": args.request_delay_ms}), flush=True)
"runs": args.runs, "request_delay_ms": args.request_delay_ms,
**({"baseline": str(args.compare_native), "candidate": str(args.native),
"seek_work": args.seek_work} if args.compare_native else {})}), flush=True)
results = []
if args.compare_native:
for delay in args.request_delay_ms:
for run in range(1, args.runs + 1):
variants = [("baseline", args.compare_native), ("candidate", args.native)]
if run % 2 == 0:
variants.reverse()
for source in ("split", "hls"):
for variant, binary in variants:
with BenchmarkServer(args.fixtures, delay) as server:
for row in sample(binary, False, source, server, seek_work=args.seek_work):
row.update(engine="native", variant=variant, source=source,
request_delay_ms=delay, run=run)
results.append(row)
print(json.dumps(row, sort_keys=True), flush=True)
print("\nMedians [min, max]; CPU is process CPU; peak RSS is process-lifetime high water; "
"upstream counts include concurrent read-ahead.")
print("| Delay ms | Variant | Source | Phase | Wall ms | CPU ms | Peak MiB | Requests | Upstream bytes |")
print("| --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: |")
for delay in args.request_delay_ms:
for variant in ("baseline", "candidate"):
for source in ("split", "hls"):
phases = dict.fromkeys(row["phase"] for row in results)
for phase in phases:
group = [row for row in results if
(row["request_delay_ms"], row["variant"], row["source"], row["phase"]) ==
(delay, variant, source, phase)]
def summary(key):
values = [row[key] for row in group]
return f"{statistics.median(values):.2f} [{min(values):.2f}, {max(values):.2f}]"
print(f"| {delay:g} | {variant} | {source} | {phase} | "
f"{summary('wall_ms')} | {summary('cpu_ms')} | {summary('peak_rss_mib')} | "
f"{summary('upstream_requests')} | {summary('upstream_bytes')} |")
return
for delay in args.request_delay_ms:
for run in range(1, args.runs + 1):
for source in ("split", "hls"):