Files
Aethera/mcp/tests/Control_Path_Benchmarks.cpp
T
2026-09-03 15:31:39 +08:00

554 lines
25 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 <task_flow/src/Task_Runtime.hpp>
#include <algorithm>
#include <atomic>
#include <charconv>
#include <chrono>
#include <cstdint>
#include <functional>
#include <iomanip>
#include <iostream>
#include <limits>
#include <memory>
#include <optional>
#include <ranges>
#include <string>
#include <string_view>
#include <vector>
namespace aethera::mcp::benchmarks {
namespace {
enum struct Input_Workload : std::uint8_t {
steady,
drag,
wheel,
mixed
};
struct Benchmark_Configuration {
std::vector<std::string_view> plot_ids{}; /* 本进程并发打开的 Gallery Plot ID,直接引用 Gallery 权威定义。 */
Input_Workload input{Input_Workload::mixed}; /* 本进程向所选图提交的交互负载。 */
std::uint32_t width{720}; /* 每个输出流的像素宽度。 */
std::uint32_t height{420}; /* 每个输出流的像素高度。 */
std::uint32_t input_rate_hz{120}; /* 输入批次目标频率;零表示 steady。 */
double duration_seconds{10.0}; /* 单次测量的明确墙钟时长。 */
};
enum struct Parse_Benchmark_Arguments_Result : std::uint8_t {
configured,
help_requested,
invalid_argument
};
Benchmark_Configuration configuration{};
std::string configuration_error{};
[[nodiscard]] bool append_plot_id(std::string_view id) {
if (std::ranges::find(configuration.plot_ids, id) !=
configuration.plot_ids.end()) return true;
if (!web::find_gallery_plot_definition(id)) return false;
configuration.plot_ids.push_back(id);
return true;
}
void append_dimension(web::Plot_Dimension dimension) {
for (const auto& definition : web::gallery_plot_definitions())
if (definition.dimension == dimension)
static_cast<void>(append_plot_id(definition.id));
}
[[nodiscard]] bool select_plots(std::string_view specification) {
configuration.plot_ids.clear();
while (!specification.empty()) {
const auto separator = specification.find(',');
const auto token = specification.substr(0, separator);
if (token == "2d") append_dimension(web::Plot_Dimension::two_d);
else if (token == "3d") append_dimension(web::Plot_Dimension::three_d);
else if (token == "all") {
append_dimension(web::Plot_Dimension::two_d);
append_dimension(web::Plot_Dimension::three_d);
}
else if (token.empty() || !append_plot_id(token)) return false;
if (separator == std::string_view::npos) break;
specification.remove_prefix(separator + 1U);
}
return !configuration.plot_ids.empty();
}
[[nodiscard]] bool parse_unsigned(std::string_view text,
std::uint32_t& value) {
const auto* begin = text.data();
const auto* end = begin + text.size();
const auto result = std::from_chars(begin, end, value);
return result.ec == std::errc{} && result.ptr == end;
}
[[nodiscard]] bool parse_duration(std::string_view text, double& value) {
const auto* begin = text.data();
const auto* end = begin + text.size();
const auto result = std::from_chars(begin, end, value);
return result.ec == std::errc{} && result.ptr == end &&
value > 0.0 && value <= 3'600.0;
}
[[nodiscard]] Parse_Benchmark_Arguments_Result parse_arguments(
int& argc, char** argv) {
configuration = {};
if (!select_plots("all")) {
configuration_error = "the Gallery has no plots";
return Parse_Benchmark_Arguments_Result::invalid_argument;
}
int retained_count{1};
for (int index = 1; index < argc; ++index) {
const std::string_view argument{argv[index]};
if (argument == "--aethera_help")
return Parse_Benchmark_Arguments_Result::help_requested;
const auto value_after = [&](std::string_view prefix)
-> std::optional<std::string_view> {
if (!argument.starts_with(prefix)) return std::nullopt;
return argument.substr(prefix.size());
};
if (const auto value = value_after("--aethera_plots=")) {
if (!select_plots(*value)) {
configuration_error = "invalid --aethera_plots selection: " +
std::string{*value};
return Parse_Benchmark_Arguments_Result::invalid_argument;
}
continue;
}
if (const auto value = value_after("--aethera_input=")) {
if (*value == "steady") configuration.input = Input_Workload::steady;
else if (*value == "drag") configuration.input = Input_Workload::drag;
else if (*value == "wheel") configuration.input = Input_Workload::wheel;
else if (*value == "mixed") configuration.input = Input_Workload::mixed;
else {
configuration_error = "invalid --aethera_input value: " +
std::string{*value};
return Parse_Benchmark_Arguments_Result::invalid_argument;
}
continue;
}
if (const auto value = value_after("--aethera_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) {
configuration_error = "invalid --aethera_size value: " +
std::string{*value};
return Parse_Benchmark_Arguments_Result::invalid_argument;
}
continue;
}
if (const auto value = value_after("--aethera_input_rate=")) {
if (!parse_unsigned(*value, configuration.input_rate_hz) ||
configuration.input_rate_hz == 0) {
configuration_error = "invalid --aethera_input_rate value: " +
std::string{*value};
return Parse_Benchmark_Arguments_Result::invalid_argument;
}
continue;
}
if (const auto value = value_after("--aethera_duration=")) {
if (!parse_duration(*value, configuration.duration_seconds)) {
configuration_error = "invalid --aethera_duration value: " +
std::string{*value};
return Parse_Benchmark_Arguments_Result::invalid_argument;
}
continue;
}
argv[retained_count++] = argv[index];
}
argc = retained_count;
return Parse_Benchmark_Arguments_Result::configured;
}
struct Concurrent_Workload_State {
std::vector<std::shared_ptr<std::atomic_uint64_t>> completed{}; /* 所选 Plot 各自的发布计数器;回调共享稳定所有权。 */
};
[[nodiscard]] Plot_Input_Request concurrent_input_request(
Input_Workload workload, std::size_t plot_index,
std::uint64_t sequence) {
Plot_Input_Request request;
request.plot = configuration.plot_ids[plot_index];
const nlohmann::json position{
{"x", 360.0 + static_cast<double>(
static_cast<std::int64_t>((sequence + plot_index * 7U) % 121U) - 60)},
{"y", 210.0 + static_cast<double>(
static_cast<std::int64_t>((sequence * 3U + plot_index * 11U) % 81U) - 40)}};
request.event = {
{"time_milliseconds", 1'000.0 + static_cast<double>(sequence) *
(1'000.0 / static_cast<double>(configuration.input_rate_hz))},
{"position", position}, {"global_position", position}
};
if (workload == Input_Workload::wheel ||
(workload == Input_Workload::mixed && (plot_index & 1U) != 0U)) {
const auto delta = (sequence & 1U) != 0U ? 120.0 : -120.0;
request.event["type"] = "wheel";
request.event["pixel_delta_y"] = delta;
request.event["angle_delta_y"] = delta;
return request;
}
request.event["type"] = "pointer_move";
request.event["button"] = "left";
request.event["buttons"] = 1;
return request;
}
class Configured_Plots : public benchmark::Fixture {
public:
void SetUp(const benchmark::State&) override {
if (shared_service) {
service = shared_service;
workload = shared_workload;
return;
}
shared_workload = std::make_shared<Concurrent_Workload_State>();
shared_workload->completed.reserve(configuration.plot_ids.size());
for (std::size_t index = 0; index < configuration.plot_ids.size();
++index) {
auto completed = std::make_shared<std::atomic_uint64_t>(0);
shared_workload->completed.push_back(completed);
}
shared_service = Control_Service::create(Gallery_Output{
.width = configuration.width,
.height = configuration.height,
.publish = [workload = shared_workload](
std::string_view id,
std::shared_ptr<const web::Gallery_Frame> frame) {
const auto found = std::ranges::find(
configuration.plot_ids, id);
if (found == configuration.plot_ids.end() || !frame ||
!frame->pixels)
return web::Gallery_Frame_Publication::ignored;
const auto index = static_cast<std::size_t>(
found - configuration.plot_ids.begin());
workload->completed[index]->fetch_add(
1, std::memory_order_relaxed);
return web::Gallery_Frame_Publication::completed;
}});
service = shared_service;
workload = shared_workload;
}
void TearDown(const benchmark::State&) override {}
static void shutdown(std::function<void()> completion) {
if (!completion) std::terminate();
if (!shared_service) {
shared_workload.reset();
completion();
return;
}
auto retiring = std::move(shared_service);
shared_workload.reset();
retiring->stop([retiring, completion = std::move(completion)] { completion(); });
}
protected:
[[nodiscard]] bool submit_input_batch(
Input_Workload input, std::uint64_t sequence,
std::optional<Event_Type> pointer_type = std::nullopt) {
for (std::size_t plot_index = 0;
plot_index < configuration.plot_ids.size(); ++plot_index) {
const bool wheel = input == Input_Workload::wheel ||
(input == Input_Workload::mixed && (plot_index & 1U) != 0U);
if (pointer_type && wheel) continue;
auto request = concurrent_input_request(
input, plot_index, sequence);
if (pointer_type) {
request.event["type"] = magic_enum::enum_name(*pointer_type);
request.event["buttons"] = *pointer_type == Event_Type::pointer_release
? 0 : 1;
}
const auto result = service->call_tool(
"aethera_plot_input", encode_protocol_value(request));
if (result.result != Tool_Call_Result::ok) return false;
}
return true;
}
void run(benchmark::State& state) {
const auto input = configuration.input;
for (auto& count : workload->completed)
count->store(0, std::memory_order_relaxed);
if (input == Input_Workload::drag || input == Input_Workload::mixed) {
if (!submit_input_batch(
input, 0, Event_Type::pointer_press)) {
state.SkipWithError("configured Plot pointer press batch was rejected");
return;
}
}
const auto started = std::chrono::steady_clock::now();
const auto deadline = started + std::chrono::duration_cast<
std::chrono::steady_clock::duration>(
std::chrono::duration<double>{configuration.duration_seconds});
const auto input_period = std::chrono::nanoseconds{
1'000'000'000 / configuration.input_rate_hz};
auto next_input = started + input_period;
std::uint64_t input_batches{};
std::uint64_t input_requests{};
std::uint32_t input_poll{};
for ([[maybe_unused]] auto iteration : state) {
while (std::chrono::steady_clock::now() < deadline) {
benchmark::DoNotOptimize(
workload->completed.front()->load(
std::memory_order_relaxed));
benchmark::ClobberMemory();
if ((++input_poll & 0x3FFU) != 0U) continue;
const auto now = std::chrono::steady_clock::now();
if (input == Input_Workload::steady || now < next_input)
continue;
++input_batches;
if (!submit_input_batch(input, input_batches)) {
state.SkipWithError(
"configured Plot interaction batch was rejected");
break;
}
input_requests += configuration.plot_ids.size();
do next_input += input_period; while (next_input <= now);
}
}
const auto finished = std::chrono::steady_clock::now();
if (input == Input_Workload::drag || input == Input_Workload::mixed) {
static_cast<void>(submit_input_batch(
input, input_batches + 1U, Event_Type::pointer_release));
}
const double elapsed_seconds = std::chrono::duration<double>(
finished - started).count();
std::uint64_t total_completed{};
double minimum_fps = std::numeric_limits<double>::max();
double maximum_fps{};
for (std::size_t index = 0;
index < configuration.plot_ids.size(); ++index) {
const auto count = workload->completed[index]->load(
std::memory_order_relaxed);
total_completed += count;
const double fps = elapsed_seconds > 0.0
? static_cast<double>(count) / elapsed_seconds : 0.0;
minimum_fps = std::min(minimum_fps, fps);
maximum_fps = std::max(maximum_fps, fps);
const auto diagnostics = service->plot_diagnostics(
configuration.plot_ids[index]);
const auto& policy = diagnostics.at("frame_policy");
const auto prefix = std::string{configuration.plot_ids[index]} + "/";
state.counters[prefix + "fps"] = fps;
state.counters[prefix + "timer_ticks"] = policy.value("timer_ticks", 0.0);
state.counters[prefix + "dropped_timer_ticks"] = policy.value("dropped_timer_ticks", 0.0);
state.counters[prefix + "completed"] = policy.value("completed_frames", 0.0);
state.counters[prefix + "effective_fps"] = policy.value("effective_frames_per_second", 0.0);
const auto statistic_milliseconds = [&](std::string_view name, std::string_view field) {
const auto found = policy.find(std::string{name});
return found == policy.end() ? 0.0 : found->value(std::string{field}, 0.0) / 1'000'000.0;
};
state.counters[prefix + "completion_ms"] = statistic_milliseconds("end_to_end_time", "average_ns");
state.counters[prefix + "completion_max_ms"] = statistic_milliseconds("end_to_end_time", "p95_ns");
state.counters[prefix + "scene_render_ms"] = statistic_milliseconds("render_time", "average_ns");
if (const auto found = diagnostics.find("datoviz"); found != diagnostics.end()) {
const auto timings = found->value("timings_ms", nlohmann::json::object());
state.counters[prefix + "backend_queue_p95_ms"] = timings.value("queue_submit_wait", 0.0);
state.counters[prefix + "backend_apply_p95_ms"] = timings.value("apply", 0.0);
state.counters[prefix + "backend_plan_p95_ms"] = timings.value("runtime_plan", 0.0);
state.counters[prefix + "backend_execute_p95_ms"] = timings.value("runtime_execute", 0.0);
state.counters[prefix + "backend_submit_p95_ms"] = timings.value("submit", 0.0);
state.counters[prefix + "readback_ms"] = timings.value("readback", 0.0);
if (const auto gpu = found->find("gpu_ms"); gpu != found->end()) state.counters[prefix + "gpu_total_ms"] = gpu->value("total", 0.0);
}
}
state.counters["aggregate_fps"] = elapsed_seconds > 0.0
? static_cast<double>(total_completed) / elapsed_seconds : 0.0;
state.counters["minimum_plot_fps"] = minimum_fps;
state.counters["maximum_plot_fps"] = maximum_fps;
state.counters["fairness_pct"] = maximum_fps > 0.0
? minimum_fps / maximum_fps * 100.0 : 0.0;
state.counters["input_batches"] = static_cast<double>(input_batches);
state.counters["input_requests"] = static_cast<double>(input_requests);
state.counters["input_request_rate"] = elapsed_seconds > 0.0
? static_cast<double>(input_requests) / elapsed_seconds : 0.0;
state.SetItemsProcessed(static_cast<std::int64_t>(total_completed));
}
private:
inline static std::shared_ptr<Control_Service> shared_service{};
inline static std::shared_ptr<Concurrent_Workload_State> shared_workload{};
std::shared_ptr<Control_Service> service{};
std::shared_ptr<Concurrent_Workload_State> workload{};
};
BENCHMARK_DEFINE_F(Configured_Plots, Run)(benchmark::State& state) {
run(state);
}
/* Fixture 静态持有所选 PlotGoogle Benchmark 校准不会反复重建其渲染资源。 */
BENCHMARK_REGISTER_F(Configured_Plots, Run)
->Iterations(1)
->UseRealTime();
class Plot_Console_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();
const auto flags = output.flags();
const auto precision = output.precision();
output << std::fixed << std::setprecision(2);
for (const auto& report : reports) {
if (report.skipped != benchmark::internal::NotSkipped) {
output << report.benchmark_name() << ": "
<< report.skip_message << '\n';
continue;
}
const auto counter = [&](std::string_view name) {
const auto found = report.counters.find(std::string{name});
return found == report.counters.end() ? 0.0 : found->second.value;
};
output << "\n" << report.benchmark_name()
<< " elapsed=" << report.real_accumulated_time
<< " s frames="
<< static_cast<std::uint64_t>(counter("aggregate_fps") *
report.real_accumulated_time)
<< "\n";
output << std::left << std::setw(23) << "Plot" << std::right << std::setw(9) << "FPS" << std::setw(11) << "E2E avg" << std::setw(11) << "E2E p95" << std::setw(11) << "Render" << std::setw(11) << "Effective" << std::setw(11) << "Dropped" << std::setw(11) << "Queue" << std::setw(11) << "Apply" << std::setw(11) << "Plan" << std::setw(11) << "Execute" << std::setw(11) << "Submit" << std::setw(10) << "GPU" << std::setw(10) << "Read" << '\n';
for (const auto id : configuration.plot_ids) {
const auto prefix = std::string{id} + "/";
output << std::left << std::setw(23) << id << std::right
<< std::setw(9) << counter(prefix + "fps")
<< std::setw(11) << counter(prefix + "completion_ms")
<< std::setw(11) << counter(prefix + "completion_max_ms")
<< std::setw(11) << counter(prefix + "scene_render_ms")
<< std::setw(11) << counter(prefix + "effective_fps")
<< std::setw(11) << counter(prefix + "dropped_timer_ticks")
<< std::setw(11) << counter(prefix + "backend_queue_p95_ms")
<< std::setw(11) << counter(prefix + "backend_apply_p95_ms")
<< std::setw(11) << counter(prefix + "backend_plan_p95_ms")
<< std::setw(11) << counter(prefix + "backend_execute_p95_ms")
<< std::setw(11) << counter(prefix + "backend_submit_p95_ms")
<< std::setw(10) << counter(prefix + "gpu_total_ms")
<< std::setw(10) << counter(prefix + "readback_ms")
<< '\n';
}
output << "aggregate_fps=" << counter("aggregate_fps")
<< " min_fps=" << counter("minimum_plot_fps")
<< " max_fps=" << counter("maximum_plot_fps")
<< " fairness=" << counter("fairness_pct") << "%"
<< " input_batches=" << counter("input_batches")
<< " input_requests/s=" << counter("input_request_rate")
<< '\n';
if (!report.report_label.empty())
output << report.report_label << '\n';
}
output.flags(flags);
output.precision(precision);
}
};
void print_help() {
std::cout <<
"Aethera concurrent Plot benchmark options:\n"
" --aethera_plots=3d|2d|all|id[,id...]\n"
" Groups and IDs may be mixed, for example: 2d,datoviz_mesh\n"
" --aethera_input=steady|drag|wheel|mixed\n"
" --aethera_size=WIDTHxHEIGHT\n"
" --aethera_input_rate=HZ\n"
" --aethera_duration=SECONDS (default: 10, maximum: 3600)\n"
" --aethera_help\n"
"Google Benchmark output options remain available, for example:\n"
" --benchmark_format=json\n\n"
"Available Gallery Plot IDs:\n";
for (const auto& definition : web::gallery_plot_definitions())
std::cout << " " << definition.id << " ("
<< web::plot_dimension_name(definition.dimension) << ")\n";
}
[[nodiscard]] Parse_Benchmark_Arguments_Result configure_benchmark(
int& argc, char** argv) {
return parse_arguments(argc, argv);
}
[[nodiscard]] const std::string& benchmark_configuration_error() {
return configuration_error;
}
void describe_configuration() {
std::string plots;
for (const auto id : configuration.plot_ids) {
if (!plots.empty()) plots += ',';
plots += id;
}
const auto input = [&] {
switch (configuration.input) {
case Input_Workload::steady: return "steady";
case Input_Workload::drag: return "drag";
case Input_Workload::wheel: return "wheel";
case Input_Workload::mixed: return "mixed";
}
return "unknown";
}();
benchmark::AddCustomContext("aethera_plots", std::move(plots));
benchmark::AddCustomContext("aethera_input", input);
benchmark::AddCustomContext(
"aethera_size", std::to_string(configuration.width) + "x" +
std::to_string(configuration.height));
benchmark::AddCustomContext(
"aethera_input_rate_hz", std::to_string(configuration.input_rate_hz));
benchmark::AddCustomContext(
"aethera_duration_seconds",
std::to_string(configuration.duration_seconds));
}
void shutdown_configured_benchmark(std::function<void()> completion) {
Configured_Plots::shutdown(std::move(completion));
}
}
}
int main(int argc, char** argv) {
bool structured_display{};
for (int index = 1; index < argc; ++index) {
const std::string_view argument{argv[index]};
constexpr std::string_view format_prefix{"--benchmark_format="};
if (argument.starts_with(format_prefix) &&
argument.substr(format_prefix.size()) != "console")
structured_display = true;
}
const auto configured =
aethera::mcp::benchmarks::configure_benchmark(argc, argv);
if (configured == aethera::mcp::benchmarks::
Parse_Benchmark_Arguments_Result::help_requested) {
aethera::mcp::benchmarks::print_help();
return 0;
}
if (configured == aethera::mcp::benchmarks::
Parse_Benchmark_Arguments_Result::invalid_argument) {
std::cerr << aethera::mcp::benchmarks::benchmark_configuration_error()
<< "\nUse --aethera_help to list valid selections.\n";
return 2;
}
if (aethera::initialize_task_runtime() != aethera::Initialize_Task_Runtime_Result::initialized) return 2;
benchmark::Initialize(&argc, argv);
if (benchmark::ReportUnrecognizedArguments(argc, argv)) return 2;
aethera::mcp::benchmarks::describe_configuration();
if (structured_display) benchmark::RunSpecifiedBenchmarks();
else {
aethera::mcp::benchmarks::Plot_Console_Reporter reporter;
benchmark::RunSpecifiedBenchmarks(&reporter);
}
std::atomic_bool stopped{};
aethera::mcp::benchmarks::shutdown_configured_benchmark([&] { stopped.store(true, std::memory_order_release); stopped.notify_one(); });
/* Google Benchmark's process boundary must not return while asynchronous plot retirement still owns worker callbacks. This wait exists only in the benchmark main thread. */
stopped.wait(false, std::memory_order_acquire);
benchmark::Shutdown();
return 0;
}