perf: benchmark native and Cast startup, seeks, and sustained playback
This commit is contained in:
@@ -31,6 +31,7 @@ extern "C" {
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
extern char **environ;
|
||||
@@ -830,7 +831,154 @@ static void decodeWork(const std::filesystem::path &directory, const std::string
|
||||
decodeUsage(beforeDecode);
|
||||
std::cout << "\ncast-media: codec decode, independent IDRs, AAC continuity, sequential and random seeks passed\n";
|
||||
}
|
||||
|
||||
// External fixtures keep upstream serving CPU/RSS out of the relay process.
|
||||
// Cache seek means retained generated TS, not a packet-cache seek. All output
|
||||
// decoding happens after the final snapshot, so validation cannot inflate any
|
||||
// phase's CPU or process high-water RSS. Only endpoint responses are retained.
|
||||
static void playbackBenchmark(std::string origin, const std::string &kind) {
|
||||
if (origin.ends_with('/')) origin.pop_back();
|
||||
const std::string prefix = "http://127.0.0.1:";
|
||||
assert(origin.starts_with(prefix));
|
||||
assert(origin.size() > prefix.size() && origin.find_first_not_of("0123456789", prefix.size()) == std::string::npos);
|
||||
const int port = std::stoi(origin.substr(prefix.size())); assert(port > 0 && port <= 65535);
|
||||
assert(kind == "split" || kind == "hls");
|
||||
const bool split = kind == "split";
|
||||
const std::string video = origin + (split ? "/media/bench-video.mp4" : "/media/bench.m3u8");
|
||||
const std::string audio = origin + "/media/bench-audio.m4a";
|
||||
const CastMediaSource source{video.c_str(), split ? "X-Terebi-Fixture: video\r\n" : "X-Terebi-Fixture: combined\r\n",
|
||||
split ? audio.c_str() : nullptr, split ? "X-Terebi-Fixture: audio\r\n" : nullptr, false, !split, false, 120};
|
||||
auto acknowledge = [] {
|
||||
std::string line; assert(bool(std::getline(std::cin, line)));
|
||||
};
|
||||
auto snapshot = [] {
|
||||
rusage usage{}; assert(getrusage(RUSAGE_SELF, &usage) == 0);
|
||||
return std::pair{std::chrono::steady_clock::now(), usage};
|
||||
};
|
||||
auto begin = [&](const char *phase) {
|
||||
std::cout << "{\"event\":\"begin\",\"phase\":\"" << phase << "\"}" << std::endl;
|
||||
acknowledge();
|
||||
return snapshot();
|
||||
};
|
||||
auto cpuMS = [](const rusage &usage) {
|
||||
return 1000.0 * (usage.ru_utime.tv_sec + usage.ru_stime.tv_sec)
|
||||
+ (usage.ru_utime.tv_usec + usage.ru_stime.tv_usec) / 1000.0;
|
||||
};
|
||||
auto wallMS = [](auto from, auto to) {
|
||||
return std::chrono::duration<double, std::milli>(to - from).count();
|
||||
};
|
||||
auto finish = [&](const char *phase, const auto &before, const auto &after, double seconds, size_t bytes, const std::string &extra = "") {
|
||||
#ifdef __APPLE__
|
||||
const double rssMiB = double(after.second.ru_maxrss) / (1024 * 1024);
|
||||
#else
|
||||
const double rssMiB = double(after.second.ru_maxrss) / 1024;
|
||||
#endif
|
||||
std::cout << "{\"phase\":\"" << phase << "\",\"wall_ms\":" << wallMS(before.first, after.first)
|
||||
<< ",\"cpu_ms\":" << cpuMS(after.second) - cpuMS(before.second)
|
||||
<< ",\"peak_rss_mib\":" << rssMiB << ",\"media_seconds\":" << seconds
|
||||
<< ",\"output_bytes\":" << bytes << extra << "}" << std::endl;
|
||||
acknowledge();
|
||||
};
|
||||
std::map<int, HTTP> outputs;
|
||||
std::string url; double duration = 0;
|
||||
const auto cold = begin("cold_start");
|
||||
auto *media = start(source, url, duration);
|
||||
const auto prepared = std::chrono::steady_clock::now();
|
||||
const auto manifest = request(url);
|
||||
const auto base = url.substr(0, url.rfind('/') + 1);
|
||||
const auto firstBegan = std::chrono::steady_clock::now();
|
||||
outputs.emplace(0, request(base + "segment/0.ts"));
|
||||
const auto coldEnd = snapshot();
|
||||
finish("cold_start", cold, coldEnd, 0, outputs.at(0).bytes.size(),
|
||||
",\"prepare_ms\":" + std::to_string(wallMS(cold.first, prepared))
|
||||
+ ",\"manifest_ms\":" + std::to_string(wallMS(prepared, firstBegan))
|
||||
+ ",\"first_segment_ms\":" + std::to_string(wallMS(firstBegan, coldEnd.first)));
|
||||
assert(duration >= 120 && duration < 121 && manifest.status == 200);
|
||||
const std::string playlist(manifest.bytes.begin(), manifest.bytes.end());
|
||||
assert(playlist.find("#EXT-X-ENDLIST") != std::string::npos);
|
||||
std::vector<double> boundaries{0};
|
||||
for (size_t at = 0; (at = playlist.find("#EXTINF:", at)) != std::string::npos; ++at)
|
||||
boundaries.push_back(boundaries.back() + std::stod(playlist.substr(at + 8)));
|
||||
assert(boundaries.size() >= 18);
|
||||
for (int id = 0; id <= 17; ++id) assert(std::abs(boundaries[id] - id * 6) < .00002);
|
||||
|
||||
// No intervening generation: segment zero is in the eight-entry TS cache.
|
||||
{
|
||||
const auto cached = begin("cached_seek");
|
||||
const auto repeated = request(base + "segment/0.ts");
|
||||
const auto cachedEnd = snapshot();
|
||||
finish("cached_seek", cached, cachedEnd, 0, repeated.bytes.size(), ",\"segment_id\":0");
|
||||
assert(repeated.status == 200 && repeated.bytes == outputs.at(0).bytes);
|
||||
}
|
||||
outputs.clear();
|
||||
|
||||
// Sequential generation stays on the warm pipeline. The initialization and
|
||||
// first six seconds above are deliberately excluded; there is no pacing.
|
||||
size_t producedBytes = 0;
|
||||
const auto sustained = begin("sustained");
|
||||
for (int id = 1; id <= 10; ++id) {
|
||||
auto response = request(base + "segment/" + std::to_string(id) + ".ts");
|
||||
producedBytes += response.bytes.size();
|
||||
assert(response.status == 200);
|
||||
if (id == 10) outputs.emplace(id, std::move(response));
|
||||
}
|
||||
const auto sustainedEnd = snapshot();
|
||||
finish("sustained", sustained, sustainedEnd, boundaries[11] - boundaries[1], producedBytes,
|
||||
",\"unpaced\":true,\"first_segment_excluded\":true,\"first_segment_id\":1,\"segment_count\":10");
|
||||
|
||||
// Generating through 66s evicts segment zero from the eight-entry TS cache
|
||||
// and its packets from the thirty-second rewind history: a miss in both.
|
||||
const auto uncached = begin("uncached_seek");
|
||||
outputs.emplace(0, request(base + "segment/0.ts"));
|
||||
const auto uncachedEnd = snapshot();
|
||||
finish("uncached_seek", uncached, uncachedEnd, 0, outputs.at(0).bytes.size(), ",\"segment_id\":0");
|
||||
cast_media_destroy(media);
|
||||
|
||||
char temp[] = "/tmp/terebi-cast-benchmark.XXXXXX"; assert(mkdtemp(temp));
|
||||
const auto output = (std::filesystem::path(temp) / "segment.ts").string();
|
||||
for (const auto &[id, response] : outputs) {
|
||||
assert(response.status == 200);
|
||||
writeFile(output, response.bytes);
|
||||
// samples requires an IDR entry, strict independent decode, both A/V,
|
||||
// monotonic DTS and one decoded frame for every compressed sample.
|
||||
const auto rows = samples(output, 1);
|
||||
int videos = 0, audios = 0;
|
||||
int64_t firstAudio = 0, firstVideo = 0;
|
||||
for (const auto &[key, value] : rows) {
|
||||
assert(key.second >= id * 6 * 90000 && key.second < (id + 1) * 6 * 90000);
|
||||
if (key.first) {
|
||||
if (!videos) {
|
||||
firstVideo = key.second;
|
||||
// The parent's HLS TS starts AAC 21.334ms before video;
|
||||
// split MP4 starts both at zero. Allow TS rescale rounding.
|
||||
const int64_t expected = id * 6 * 90000 + (split ? 0 : 1920);
|
||||
assert(std::abs(firstVideo - expected) <= 2);
|
||||
}
|
||||
assert(key.second == firstVideo + videos * 3000);
|
||||
++videos;
|
||||
} else {
|
||||
if (!audios) {
|
||||
firstAudio = key.second;
|
||||
assert(firstAudio < id * 6 * 90000 + 1922);
|
||||
}
|
||||
assert(key.second == firstAudio + audios * 1920);
|
||||
++audios;
|
||||
}
|
||||
}
|
||||
assert(videos == 180 && audios >= 281 && audios <= 282);
|
||||
}
|
||||
std::filesystem::remove_all(temp);
|
||||
std::cerr << "cast playback benchmark: independently decodable IDRs, 30fps timestamps/frame counts and AAC verified\n";
|
||||
}
|
||||
int main(int argc, char **argv) {
|
||||
if (argc > 1 && std::string(argv[1]) == "--playback-benchmark") {
|
||||
if (argc != 4 || (std::string(argv[3]) != "split" && std::string(argv[3]) != "hls")) {
|
||||
std::cerr << "usage: " << argv[0] << " --playback-benchmark <base_url> <split|hls>\n";
|
||||
return 2;
|
||||
}
|
||||
playbackBenchmark(argv[2], argv[3]);
|
||||
return 0;
|
||||
}
|
||||
bool liveCheck = false, seekCheck = false, hlsCheck = false, remuxCheck = false;
|
||||
for (int argument = 1; argument < argc; ++argument) {
|
||||
if (std::string(argv[argument]) == "--live") liveCheck = true;
|
||||
|
||||
Reference in New Issue
Block a user