Files
Aethera/mcp/tests/Event_Latency_Benchmarks.cpp
T

615 lines
27 KiB
C++

#include <mcp/core/Control_Requests.hpp>
#include <mcp/core/Control_Service.hpp>
#include <mcp/core/Protocol_Type.hpp>
#include <mcp/core/runtime/Gallery_Plots.hpp>
#include <benchmark/benchmark.h>
#include <magic_enum/magic_enum.hpp>
#include <render_common.hpp>
#include <algorithm>
#include <array>
#include <atomic>
#include <charconv>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace aethera::mcp::event_latency_benchmarks {
namespace {
using Clock = std::chrono::steady_clock;
inline constexpr auto event_types = magic_enum::enum_values<Event_Type>();
inline constexpr std::size_t event_count = event_types.size();
struct Configuration {
std::uint32_t width{720};
std::uint32_t height{420};
std::uint32_t samples{12};
bool three_d_only{};
bool manual_policy{true};
};
struct Summary {
double p50{};
double p95{};
double p99{};
double maximum{};
};
struct Distribution {
std::vector<double> samples{};
void add(double value) { samples.push_back(value); }
[[nodiscard]] Summary summarize() const {
if (samples.empty()) return {};
auto ordered = samples;
std::ranges::sort(ordered);
const auto percentile = [&](double probability) {
const auto rank = probability *
static_cast<double>(ordered.size() - 1U);
const auto lower = static_cast<std::size_t>(rank);
const auto upper = std::min(lower + 1U, ordered.size() - 1U);
const auto fraction = rank - static_cast<double>(lower);
return ordered[lower] +
(ordered[upper] - ordered[lower]) * fraction;
};
return {
percentile(0.50), percentile(0.95), percentile(0.99),
ordered.back()
};
}
};
struct Event_Result {
Distribution mcp_accept_ms{};
Distribution scene_queue_ms{};
Distribution dispatch_ms{};
Distribution scene_total_ms{};
Distribution accepted_to_frame_ms{};
Distribution end_to_end_ms{};
};
struct Plot_Result {
std::string id{};
web::Plot_Dimension dimension{web::Plot_Dimension::two_d};
std::array<Event_Result, event_count> events{};
};
struct Plot_Probe {
std::atomic_uint64_t expected_correlation{};
std::atomic_uint64_t completed_steady_ns{};
};
struct Active_Plot {
std::string id{};
std::shared_ptr<Plot_Probe> probe{};
};
struct Batch_Timing {
std::array<std::uint64_t, event_count> submitted_ns{};
std::array<std::uint64_t, event_count> accepted_ns{};
};
Configuration configuration{};
std::vector<Plot_Result> published_results{};
[[nodiscard]] std::uint64_t steady_time_ns() noexcept {
return static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
Clock::now().time_since_epoch()).count());
}
[[nodiscard]] double elapsed_ms(std::uint64_t begin,
std::uint64_t end) noexcept {
return end >= begin
? static_cast<double>(end - begin) / 1'000'000.0 : 0.0;
}
[[nodiscard]] nlohmann::json make_event(
Event_Type type, std::size_t plot_index, std::uint64_t batch) {
const nlohmann::json position{
{"x", 120.0 + static_cast<double>(
(batch * 13U + plot_index * 7U) % 400U)},
{"y", 80.0 + static_cast<double>(
(batch * 11U + plot_index * 5U) % 240U)}};
const auto wheel_delta = (batch & 1U) == 0U ? 120.0 : -120.0;
return {
{"type", magic_enum::enum_name(type)},
{"time_milliseconds",
static_cast<double>(steady_time_ns()) / 1'000'000.0},
{"position", position}, {"global_position", position},
{"button", "left"},
{"buttons", type == Event_Type::pointer_release ? 0U : 1U},
{"modifiers", static_cast<std::uint8_t>(Keyboard_Modifier::control)},
{"pixel_delta_x", 3.0}, {"pixel_delta_y", wheel_delta},
{"angle_delta_x", 3.0}, {"angle_delta_y", wheel_delta},
{"key", "space"}, {"native_key", 32U},
{"auto_repeat", (batch & 1U) != 0U}
};
}
[[nodiscard]] const nlohmann::json& event_statistic(
const nlohmann::json& diagnostics, Event_Type type,
std::string_view statistic) {
const auto type_name = std::string{magic_enum::enum_name(type)};
const auto& input = diagnostics.at("input_statistics");
const auto found_type = input.find(type_name);
if (found_type == input.end())
throw std::runtime_error(
"completed frame did not publish input statistics for " +
type_name);
const auto found_statistic = found_type->find(std::string{statistic});
if (found_statistic == found_type->end())
throw std::runtime_error(
"completed frame did not publish " + std::string{statistic} +
" for " + type_name);
return *found_statistic;
}
void configure_manual_backend_plot(
Control_Service& service, std::string_view id) {
const auto manual = service.write_plot_property(id,
"frame-analysis", "pacing_mode", "manual");
if (!manual.value("success", false))
throw std::runtime_error("failed to select manual frame pacing");
}
[[nodiscard]] std::uint64_t correlation_id(
std::uint64_t batch, std::size_t plot_index) noexcept {
return (batch + 1U) * 1'000U +
static_cast<std::uint64_t>(plot_index + 1U);
}
void add_distribution(Distribution& destination,
const Distribution& source) {
destination.samples.insert(destination.samples.end(),
source.samples.begin(), source.samples.end());
}
class Event_Latency_Reporter final : public benchmark::ConsoleReporter {
public:
bool ReportContext(const Context& context) override {
return ConsoleReporter::ReportContext(context);
}
void ReportRuns(const std::vector<Run>& reports) override {
auto& output = GetOutputStream();
for (const auto& report : reports) {
output << report.benchmark_name();
if (report.skipped != benchmark::internal::NotSkipped)
output << " ERROR: " << report.skip_message << '\n';
else
output << " elapsed=" << report.real_accumulated_time
<< " s\n";
}
if (published_results.empty()) return;
const auto flags = output.flags();
const auto precision = output.precision();
output << std::fixed << std::setprecision(3)
<< "\nPure backend event timeline (ms)\n"
<< "MCP = emit -> aethera_plot_input accepted; "
"Queue/Dispatch = authoritative Scene Event timing; "
"Frame = accepted -> correlated backend frame complete.\n\n";
output << std::left << std::setw(24) << "Plot"
<< std::setw(6) << "Dim"
<< std::setw(18) << "Worst event"
<< std::right << std::setw(11) << "MCP P95"
<< std::setw(11) << "Queue P95"
<< std::setw(11) << "Disp P95"
<< std::setw(11) << "Frame P95"
<< std::setw(11) << "E2E P95" << '\n';
for (const auto& plot : published_results) {
std::size_t worst_index{};
double worst_e2e{};
for (std::size_t index = 0; index < event_count; ++index) {
const auto p95 = plot.events[index].end_to_end_ms.summarize().p95;
if (p95 <= worst_e2e) continue;
worst_e2e = p95;
worst_index = index;
}
const auto& event = plot.events[worst_index];
output << std::left << std::setw(24) << plot.id
<< std::setw(6) << web::plot_dimension_name(plot.dimension)
<< std::setw(18)
<< magic_enum::enum_name(event_types[worst_index])
<< std::right
<< std::setw(11) << event.mcp_accept_ms.summarize().p95
<< std::setw(11) << event.scene_queue_ms.summarize().p95
<< std::setw(11) << event.dispatch_ms.summarize().p95
<< std::setw(11)
<< event.accepted_to_frame_ms.summarize().p95
<< std::setw(11) << worst_e2e << '\n';
}
output << "\nAll plots by event type\n"
<< std::left << std::setw(18) << "Event"
<< std::right << std::setw(11) << "MCP P50"
<< std::setw(11) << "MCP P95"
<< std::setw(11) << "Queue P95"
<< std::setw(11) << "Disp P95"
<< std::setw(11) << "Scene P95"
<< std::setw(11) << "Frame P95"
<< std::setw(11) << "E2E P50"
<< std::setw(11) << "E2E P95"
<< std::setw(11) << "E2E P99" << '\n';
for (std::size_t event_index = 0; event_index < event_count;
++event_index) {
Event_Result aggregate{};
for (const auto& plot : published_results) {
const auto& event = plot.events[event_index];
add_distribution(aggregate.mcp_accept_ms, event.mcp_accept_ms);
add_distribution(aggregate.scene_queue_ms, event.scene_queue_ms);
add_distribution(aggregate.dispatch_ms, event.dispatch_ms);
add_distribution(aggregate.scene_total_ms, event.scene_total_ms);
add_distribution(
aggregate.accepted_to_frame_ms,
event.accepted_to_frame_ms);
add_distribution(aggregate.end_to_end_ms, event.end_to_end_ms);
}
const auto mcp = aggregate.mcp_accept_ms.summarize();
const auto queue = aggregate.scene_queue_ms.summarize();
const auto dispatch = aggregate.dispatch_ms.summarize();
const auto scene = aggregate.scene_total_ms.summarize();
const auto frame = aggregate.accepted_to_frame_ms.summarize();
const auto end_to_end = aggregate.end_to_end_ms.summarize();
output << std::left << std::setw(18)
<< magic_enum::enum_name(event_types[event_index])
<< std::right << std::setw(11) << mcp.p50
<< std::setw(11) << mcp.p95
<< std::setw(11) << queue.p95
<< std::setw(11) << dispatch.p95
<< std::setw(11) << scene.p95
<< std::setw(11) << frame.p95
<< std::setw(11) << end_to_end.p50
<< std::setw(11) << end_to_end.p95
<< std::setw(11) << end_to_end.p99 << '\n';
}
output.flags(flags);
output.precision(precision);
}
};
void run_event_latency(benchmark::State& state) {
published_results.clear();
const auto definitions = web::gallery_plot_definitions();
std::vector<Active_Plot> active;
active.reserve(definitions.size());
published_results.reserve(definitions.size());
try {
for (const auto& definition : definitions) {
if (configuration.three_d_only &&
definition.dimension != web::Plot_Dimension::three_d)
continue;
auto probe = std::make_shared<Plot_Probe>();
active.push_back({std::string{definition.id}, std::move(probe)});
published_results.push_back({
std::string{definition.id}, definition.dimension, {}});
}
auto service = Control_Service::create(Gallery_Output{
.width = configuration.width,
.height = configuration.height,
.publish = [&active](
std::string_view id,
std::shared_ptr<const web::Gallery_Frame> frame) {
const auto found = std::ranges::find(active, id,
&Active_Plot::id);
if (found == active.end() || !frame || !frame->pixels)
return web::Gallery_Frame_Publication::ignored;
const auto expected = found->probe->expected_correlation.load(
std::memory_order_acquire);
if (expected != 0 &&
frame->pixels->correlation_id == expected) {
std::uint64_t incomplete{};
static_cast<void>(found->probe->completed_steady_ns.
compare_exchange_strong(
incomplete, steady_time_ns(),
std::memory_order_release,
std::memory_order_relaxed));
}
return web::Gallery_Frame_Publication::completed;
}});
if (configuration.manual_policy)
for (const auto& item : active)
configure_manual_backend_plot(*service, item.id);
std::string configuration_failure;
if (configuration.manual_policy) {
Task_Graph configuration_barrier{"mcp.event-latency.configure"};
configuration_barrier.add("await.manual-policy", [&] {
const auto deadline = Clock::now() + std::chrono::seconds{10};
Task_Graph::corun_until([&] {
const bool ready = std::ranges::all_of(
active, [&service](const Active_Plot& item) {
const auto diagnostics =
service->plot_diagnostics(item.id);
if (!diagnostics.contains("frame_policy"))
return false;
const auto& policy = diagnostics.at("frame_policy").
at("configuration");
if (policy.at("mode") == "manual" &&
policy.at("pixel_delivery_enabled").get<bool>()) {
static_cast<void>(service->write_plot_property(
item.id, "frame-analysis",
"pixel_delivery_enabled", false));
return false;
}
return policy.at("mode") == "manual" &&
!policy.at("pixel_delivery_enabled").get<bool>();
});
if (ready) return true;
if (Clock::now() < deadline) return false;
configuration_failure =
"timed out applying manual backend frame policy";
for (std::size_t index = 0; index < active.size(); ++index) {
const auto& item = active[index];
const auto diagnostics =
service->plot_diagnostics(item.id);
const auto& policy = diagnostics.at("frame_policy");
const auto& policy_configuration =
policy.at("configuration");
if (policy_configuration.at("mode") == "manual")
continue;
const auto& lifecycle = policy.at("lifecycle");
configuration_failure += "\n " +
published_results[index].id +
": mode=" +
policy_configuration.at("mode").get<std::string>() +
", lifecycle=" +
lifecycle.at("state").get<std::string>() +
", active=" + std::to_string(
lifecycle.at("active_frame_count").
get<std::size_t>());
}
return true;
});
});
aethera::detail::run_taskflow(configuration_barrier);
}
if (!configuration_failure.empty())
throw std::runtime_error(configuration_failure);
for (auto& item : active)
service->reset_plot_diagnostics(item.id);
std::vector<Batch_Timing> batch_timing(active.size());
std::string failure;
Task_Graph coordinator{"mcp.event-latency"};
coordinator.add("measure.all-plots", [&] {
for (std::uint64_t batch = 0; batch < configuration.samples;
++batch) {
const auto deadline = Clock::now() + std::chrono::seconds{30};
for (std::size_t plot_index = 0;
plot_index < active.size(); ++plot_index) {
auto& item = active[plot_index];
auto& timing = batch_timing[plot_index];
const auto correlation = correlation_id(batch, plot_index);
item.probe->completed_steady_ns.store(
0, std::memory_order_relaxed);
item.probe->expected_correlation.store(
correlation, std::memory_order_release);
for (std::size_t event_index = 0;
event_index < event_count; ++event_index) {
auto request = Plot_Input_Request{
.plot = published_results[plot_index].id,
.event = make_event(
event_types[event_index], plot_index, batch)
};
timing.submitted_ns[event_index] = steady_time_ns();
const auto result = service->call_tool(
"aethera_plot_input",
encode_protocol_value(request));
timing.accepted_ns[event_index] = steady_time_ns();
if (result.result != Tool_Call_Result::ok) {
failure = "aethera_plot_input rejected " +
published_results[plot_index].id + "/" +
std::string{magic_enum::enum_name(
event_types[event_index])} + ": " +
result.message;
return;
}
}
service->request_frame(item.id, aethera::Frame_Request{
.issued_at = Clock::now(),
.sequence = correlation,
.time_milliseconds =
static_cast<double>(steady_time_ns()) / 1'000'000.0,
.width = configuration.width,
.height = configuration.height,
.source = Frame_Request_Source::immediate
});
}
Task_Graph::corun_until([&] {
const bool complete = std::ranges::all_of(
active, [](const Active_Plot& item) {
return item.probe->completed_steady_ns.load(
std::memory_order_acquire) != 0;
});
if (complete) return true;
if (Clock::now() < deadline) return false;
failure = "timed out waiting for a correlated backend frame";
return true;
});
if (!failure.empty()) return;
for (std::size_t plot_index = 0;
plot_index < active.size(); ++plot_index) {
const auto completed = active[plot_index].probe->
completed_steady_ns.load(std::memory_order_acquire);
const auto diagnostics = service->plot_diagnostics(
active[plot_index].id);
for (std::size_t event_index = 0;
event_index < event_count; ++event_index) {
auto& result = published_results[plot_index].
events[event_index];
const auto& timing = batch_timing[plot_index];
result.mcp_accept_ms.add(elapsed_ms(
timing.submitted_ns[event_index],
timing.accepted_ns[event_index]));
result.accepted_to_frame_ms.add(elapsed_ms(
timing.accepted_ns[event_index], completed));
result.end_to_end_ms.add(elapsed_ms(
timing.submitted_ns[event_index], completed));
const auto type = event_types[event_index];
result.scene_queue_ms.add(event_statistic(
diagnostics, type, "queue_wait_ms").at("latest").
get<double>());
result.dispatch_ms.add(event_statistic(
diagnostics, type, "dispatch_ms").at("latest").
get<double>());
result.scene_total_ms.add(event_statistic(
diagnostics, type, "total_ms").at("latest").
get<double>());
}
active[plot_index].probe->expected_correlation.store(
0, std::memory_order_release);
}
}
});
for ([[maybe_unused]] auto iteration : state)
aethera::detail::run_taskflow(coordinator);
if (!failure.empty()) state.SkipWithError(failure);
Distribution all_end_to_end;
for (const auto& plot : published_results) {
Distribution plot_end_to_end;
for (std::size_t event_index = 0;
event_index < event_count; ++event_index) {
const auto event_name = std::string{
magic_enum::enum_name(event_types[event_index])};
const auto& event = plot.events[event_index];
add_distribution(plot_end_to_end, event.end_to_end_ms);
add_distribution(all_end_to_end, event.end_to_end_ms);
const auto prefix = plot.id + "/" + event_name + "/";
state.counters[prefix + "queue_p95_ms"] =
event.scene_queue_ms.summarize().p95;
state.counters[prefix + "dispatch_p95_ms"] =
event.dispatch_ms.summarize().p95;
state.counters[prefix + "e2e_p95_ms"] =
event.end_to_end_ms.summarize().p95;
}
state.counters[plot.id + "/e2e_p95_ms"] =
plot_end_to_end.summarize().p95;
}
const auto end_to_end = all_end_to_end.summarize();
state.counters["all/e2e_p50_ms"] = end_to_end.p50;
state.counters["all/e2e_p95_ms"] = end_to_end.p95;
state.counters["all/e2e_p99_ms"] = end_to_end.p99;
state.SetItemsProcessed(static_cast<std::int64_t>(
configuration.samples * active.size() * event_count));
}
catch (const std::exception& error) {
state.SkipWithError(error.what());
}
}
BENCHMARK(run_event_latency)->Iterations(1)->UseRealTime();
[[nodiscard]] bool parse_unsigned(std::string_view value,
std::uint32_t& output) {
const auto* begin = value.data();
const auto* end = begin + value.size();
const auto parsed = std::from_chars(begin, end, output);
return parsed.ec == std::errc{} && parsed.ptr == end;
}
[[nodiscard]] bool configure(int& argc, char** argv) {
int retained{1};
for (int index = 1; index < argc; ++index) {
const std::string_view argument{argv[index]};
constexpr std::string_view samples_prefix{
"--aethera_event_samples="};
constexpr std::string_view size_prefix{"--aethera_size="};
constexpr std::string_view plots_prefix{"--aethera_plots="};
constexpr std::string_view policy_prefix{"--aethera_policy="};
if (argument.starts_with(samples_prefix)) {
if (!parse_unsigned(argument.substr(samples_prefix.size()),
configuration.samples) ||
configuration.samples == 0 || configuration.samples > 600) {
std::cerr << "--aethera_event_samples must be in [1, 600]\n";
return false;
}
continue;
}
if (argument.starts_with(size_prefix)) {
const auto value = argument.substr(size_prefix.size());
const auto separator = value.find('x');
if (separator == std::string_view::npos ||
!parse_unsigned(value.substr(0, separator),
configuration.width) ||
!parse_unsigned(value.substr(separator + 1U),
configuration.height) ||
configuration.width == 0 || configuration.height == 0) {
std::cerr << "--aethera_size requires WIDTHxHEIGHT\n";
return false;
}
continue;
}
if (argument.starts_with(plots_prefix)) {
const auto value = argument.substr(plots_prefix.size());
if (value == "all") configuration.three_d_only = false;
else if (value == "3d") configuration.three_d_only = true;
else {
std::cerr << "--aethera_plots requires all or 3d\n";
return false;
}
continue;
}
if (argument.starts_with(policy_prefix)) {
const auto value = argument.substr(policy_prefix.size());
if (value == "manual") configuration.manual_policy = true;
else if (value == "current") configuration.manual_policy = false;
else {
std::cerr << "--aethera_policy requires manual or current\n";
return false;
}
continue;
}
argv[retained++] = argv[index];
}
argc = retained;
return true;
}
} // namespace
} // namespace aethera::mcp::event_latency_benchmarks
int main(int argc, char** argv) {
if (!aethera::mcp::event_latency_benchmarks::configure(argc, argv))
return 2;
aethera::initialize_runtime({});
benchmark::Initialize(&argc, argv);
if (benchmark::ReportUnrecognizedArguments(argc, argv)) return 2;
benchmark::AddCustomContext(
"aethera_plots",
aethera::mcp::event_latency_benchmarks::configuration.three_d_only
? "3d" : "all");
benchmark::AddCustomContext(
"aethera_policy",
aethera::mcp::event_latency_benchmarks::configuration.manual_policy
? "manual" : "current");
benchmark::AddCustomContext(
"aethera_event_samples",
std::to_string(
aethera::mcp::event_latency_benchmarks::configuration.samples));
benchmark::AddCustomContext(
"aethera_size",
std::to_string(
aethera::mcp::event_latency_benchmarks::configuration.width) +
"x" + std::to_string(
aethera::mcp::event_latency_benchmarks::configuration.height));
aethera::mcp::event_latency_benchmarks::Event_Latency_Reporter reporter;
benchmark::RunSpecifiedBenchmarks(&reporter);
benchmark::Shutdown();
return 0;
}