Files
Aethera/mcp/tests/Control_Path_Benchmarks.cpp
T

832 lines
40 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 <render_common.hpp>
#include <algorithm>
#include <atomic>
#include <charconv>
#include <chrono>
#include <cstdint>
#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() {
if (!shared_workload) return;
shared_service.reset();
shared_workload.reset();
}
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);
for (const auto id : configuration.plot_ids) {
service->reset_plot_diagnostics(id);
service->request_plot_taskflow_trace(id, 1);
}
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 runtime_begin = service->call_tool(
"aethera_task_runtime", nlohmann::json::object());
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 + "requested"] =
policy.at("observation").at("request_count").get<double>();
state.counters[prefix + "submitted"] =
policy.at("observation").at("submitted_frame_count").get<double>();
state.counters[prefix + "completed"] =
policy.at("observation").at("completed_frame_count").get<double>();
state.counters[prefix + "active"] =
policy.at("observation").at("active_frame_count").get<double>();
state.counters[prefix + "scene_rejected"] =
policy.at("requests").at("scene_rejected").get<double>();
state.counters[prefix + "slot_backpressure"] =
policy.at("requests").at("frame_slot_backpressure").get<double>();
state.counters[prefix + "completion_ms"] =
policy.at("latency").at("average_completion_ms").get<double>();
state.counters[prefix + "completion_max_ms"] =
policy.at("latency").at("maximum_completion_ms").get<double>();
const auto& frame_statistics = diagnostics.at("frame_statistics");
const auto statistic_value = [&](std::string_view name,
std::string_view field) {
const auto found = frame_statistics.find(name);
return found == frame_statistics.end()
? 0.0 : found->at(field).get<double>();
};
state.counters[prefix + "backend_queue_ms"] =
statistic_value("backend_queue_ms", "average");
state.counters[prefix + "backend_queue_p95_ms"] =
statistic_value("backend_queue_ms", "p95");
state.counters[prefix + "backend_queue_max_ms"] =
statistic_value("backend_queue_ms", "maximum");
state.counters[prefix + "backend_apply_ms"] =
statistic_value("backend_apply_ms", "average");
state.counters[prefix + "backend_apply_p95_ms"] =
statistic_value("backend_apply_ms", "p95");
state.counters[prefix + "backend_apply_max_ms"] =
statistic_value("backend_apply_ms", "maximum");
state.counters[prefix + "backend_plan_ms"] =
statistic_value("backend_plan_ms", "average");
state.counters[prefix + "backend_plan_p95_ms"] =
statistic_value("backend_plan_ms", "p95");
state.counters[prefix + "backend_plan_max_ms"] =
statistic_value("backend_plan_ms", "maximum");
state.counters[prefix + "backend_execute_ms"] =
statistic_value("backend_execute_ms", "average");
state.counters[prefix + "backend_execute_p95_ms"] =
statistic_value("backend_execute_ms", "p95");
state.counters[prefix + "backend_execute_max_ms"] =
statistic_value("backend_execute_ms", "maximum");
constexpr std::array detailed_backend_statistics{
"backend_target_acquire_ms",
"backend_structure_check_ms",
"backend_query_ms",
"backend_runtime_plan_ms",
"backend_runtime_plan_cpu_ms",
"backend_runtime_execute_ms",
"backend_runtime_execute_cpu_ms",
"backend_mvp_update_ms",
"backend_frame_begin_ms",
"backend_frame_plan_ms",
"backend_frame_plan_cpu_ms",
"backend_external_register_ms",
"backend_frame_attach_ms",
"backend_frame_execute_ms",
"backend_frame_execute_cpu_ms",
"backend_frame_finish_ms"};
constexpr std::array detailed_emit_statistics{
"backend_emit_replay_dirty_ms",
"backend_emit_layout_ms",
"backend_emit_prepare_ms",
"backend_emit_plan_build_ms",
"backend_emit_contract_ms",
"backend_emit_stream_ms",
"backend_emit_stream_freeze_ms",
"backend_emit_commit_ms",
"backend_emit_plan_reset_ms",
"backend_emit_artifact_create_ms",
"backend_emit_artifact_freeze_ms",
"backend_emit_packet_encode_ms"};
constexpr std::array detailed_drp_statistics{
"backend_drp_validation_ms",
"backend_drp_state_ms",
"backend_drp_buffer_create_ms",
"backend_drp_texture_create_ms",
"backend_drp_shader_create_ms",
"backend_drp_shader_compile_ms",
"backend_drp_shader_module_create_ms",
"backend_drp_pipeline_create_ms",
"backend_drp_binding_create_ms",
"backend_drp_upload_ms",
"backend_drp_upload_decode_ms",
"backend_drp_upload_vulkan_allocate_ms",
"backend_drp_upload_host_copy_ms",
"backend_drp_upload_command_allocate_ms",
"backend_drp_upload_command_record_ms",
"backend_drp_upload_fence_create_ms",
"backend_drp_upload_submit_enqueue_ms",
"backend_drp_upload_submit_queue_wait_ms",
"backend_drp_upload_queue_submit_ms",
"backend_drp_upload_fence_wait_ms",
"backend_drp_upload_retire_ms",
"backend_drp_transfer_ms",
"backend_drp_record_ms"};
for (const std::string_view name : detailed_backend_statistics) {
state.counters[prefix + std::string{name} + "/p95"] =
statistic_value(name, "p95");
state.counters[prefix + std::string{name} + "/max"] =
statistic_value(name, "maximum");
}
for (const std::string_view name : detailed_emit_statistics) {
state.counters[prefix + std::string{name} + "/p95"] =
statistic_value(name, "p95");
state.counters[prefix + std::string{name} + "/max"] =
statistic_value(name, "maximum");
}
for (const std::string_view name : detailed_drp_statistics) {
state.counters[prefix + std::string{name} + "/p95"] =
statistic_value(name, "p95");
state.counters[prefix + std::string{name} + "/max"] =
statistic_value(name, "maximum");
}
state.counters[prefix + "backend_submit_queue_ms"] =
statistic_value("backend_submit_queue_ms", "average");
state.counters[prefix + "backend_submit_queue_p95_ms"] =
statistic_value("backend_submit_queue_ms", "p95");
state.counters[prefix + "backend_submit_queue_max_ms"] =
statistic_value("backend_submit_queue_ms", "maximum");
state.counters[prefix + "backend_submit_ms"] =
statistic_value("backend_submit_ms", "average");
state.counters[prefix + "backend_submit_p95_ms"] =
statistic_value("backend_submit_ms", "p95");
state.counters[prefix + "backend_submit_max_ms"] =
statistic_value("backend_submit_ms", "maximum");
state.counters[prefix + "gpu_total_ms"] =
statistic_value("gpu_total_ms", "average");
state.counters[prefix + "gpu_total_p95_ms"] =
statistic_value("gpu_total_ms", "p95");
state.counters[prefix + "readback_ms"] =
statistic_value("readback_ms", "average");
state.counters[prefix + "scene_render_ms"] =
statistic_value("scene_render_ms", "average");
state.counters[prefix + "event_dispatch_ms"] =
statistic_value("event_dispatch_ms", "average");
state.counters[prefix + "frame_interval_p95_ms"] =
statistic_value("frame_interval_ms", "p95");
}
const auto runtime_end = service->call_tool(
"aethera_task_runtime", nlohmann::json::object());
const auto runtime_delta = [&](std::string_view key) {
if (runtime_begin.result != Tool_Call_Result::ok ||
runtime_end.result != Tool_Call_Result::ok) return 0.0;
const auto begin = runtime_begin.content.at(key).get<std::uint64_t>();
const auto end = runtime_end.content.at(key).get<std::uint64_t>();
return static_cast<double>(end >= begin ? end - begin : 0U);
};
const double wall_ns = runtime_delta("observed_wall_time_ns");
const double busy_ns = runtime_delta("worker_busy_time_ns");
const double cpu_ns = runtime_delta("worker_cpu_time_ns");
const double cooperative_waits = runtime_delta(
"cooperative_wait_count");
const double cooperative_wait_ns = runtime_delta(
"cooperative_wait_time_ns");
const double worker_count = runtime_end.result == Tool_Call_Result::ok
? runtime_end.content.at("worker_count").get<double>() : 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["worker_busy_pct"] = wall_ns > 0.0 && worker_count > 0.0
? busy_ns / (wall_ns * worker_count) * 100.0 : 0.0;
state.counters["worker_cpu_pct"] = wall_ns > 0.0 && worker_count > 0.0
? cpu_ns / (wall_ns * worker_count) * 100.0 : 0.0;
double longest_sampled_busy_ms{};
std::string longest_sampled_busy_node;
std::string longest_sampled_busy_plot;
for (std::size_t plot_index = 0;
plot_index < configuration.plot_ids.size(); ++plot_index) {
const auto trace = service->plot_taskflow_trace(
configuration.plot_ids[plot_index]);
if (!trace.value("complete", false)) continue;
for (const auto& frame : trace.at("frames"))
for (const auto& execution : frame.at("executions")) {
const auto busy = std::max(
0.0, execution.value("duration_ms", 0.0) -
execution.value("cooperative_wait_ms", 0.0));
if (busy <= longest_sampled_busy_ms) continue;
longest_sampled_busy_ms = busy;
longest_sampled_busy_node = execution.value(
"node_id", std::string{});
longest_sampled_busy_plot =
std::string{configuration.plot_ids[plot_index]};
}
}
state.counters["sampled_longest_busy_ms"] = longest_sampled_busy_ms;
if (!longest_sampled_busy_node.empty())
state.SetLabel("busy=" + longest_sampled_busy_plot + ":" +
longest_sampled_busy_node);
state.counters["cooperative_yields"] = cooperative_waits;
state.counters["cooperative_wait_ms"] =
cooperative_wait_ns / 1'000'000.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) << "Done ms"
<< std::setw(11) << "Max ms"
<< std::setw(11) << "Scene ms"
<< std::setw(11) << "Event ms"
<< std::setw(11) << "Queue95"
<< std::setw(11) << "QueueMax"
<< std::setw(11) << "Apply95"
<< std::setw(11) << "ApplyMax"
<< std::setw(11) << "Plan95"
<< std::setw(11) << "PlanMax"
<< std::setw(11) << "Exec95"
<< std::setw(11) << "ExecMax"
<< std::setw(11) << "SubQ95"
<< std::setw(11) << "SubQMax"
<< std::setw(11) << "Submit95"
<< std::setw(11) << "SubmitMax"
<< std::setw(10) << "GPU ms"
<< std::setw(10) << "Read ms"
<< std::setw(10) << "Backpr."
<< std::setw(9) << "Reject" << '\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 + "event_dispatch_ms")
<< std::setw(11) << counter(prefix + "backend_queue_p95_ms")
<< std::setw(11) << counter(prefix + "backend_queue_max_ms")
<< std::setw(11) << counter(prefix + "backend_apply_p95_ms")
<< std::setw(11) << counter(prefix + "backend_apply_max_ms")
<< std::setw(11) << counter(prefix + "backend_plan_p95_ms")
<< std::setw(11) << counter(prefix + "backend_plan_max_ms")
<< std::setw(11) << counter(prefix + "backend_execute_p95_ms")
<< std::setw(11) << counter(prefix + "backend_execute_max_ms")
<< std::setw(11) << counter(prefix + "backend_submit_queue_p95_ms")
<< std::setw(11) << counter(prefix + "backend_submit_queue_max_ms")
<< std::setw(11) << counter(prefix + "backend_submit_p95_ms")
<< std::setw(11) << counter(prefix + "backend_submit_max_ms")
<< std::setw(10) << counter(prefix + "gpu_total_ms")
<< std::setw(10) << counter(prefix + "readback_ms")
<< std::setw(10) << counter(prefix + "slot_backpressure")
<< std::setw(9) << counter(prefix + "scene_rejected")
<< '\n';
constexpr std::array detailed_backend_statistics{
std::pair{"acquire", "backend_target_acquire_ms"},
std::pair{"structure", "backend_structure_check_ms"},
std::pair{"query", "backend_query_ms"},
std::pair{"runtime-plan", "backend_runtime_plan_ms"},
std::pair{"runtime-plan-cpu", "backend_runtime_plan_cpu_ms"},
std::pair{"runtime-exec", "backend_runtime_execute_ms"},
std::pair{"runtime-exec-cpu", "backend_runtime_execute_cpu_ms"},
std::pair{"mvp", "backend_mvp_update_ms"},
std::pair{"frame-begin", "backend_frame_begin_ms"},
std::pair{"frame-plan", "backend_frame_plan_ms"},
std::pair{"frame-plan-cpu", "backend_frame_plan_cpu_ms"},
std::pair{"external", "backend_external_register_ms"},
std::pair{"attach", "backend_frame_attach_ms"},
std::pair{"frame-exec", "backend_frame_execute_ms"},
std::pair{"frame-exec-cpu", "backend_frame_execute_cpu_ms"},
std::pair{"finish", "backend_frame_finish_ms"}};
output << " backend-detail " << id;
for (const auto& [label, statistic] :
detailed_backend_statistics)
output << " " << label << "95/max="
<< counter(prefix + statistic + "/p95") << "/"
<< counter(prefix + statistic + "/max");
output << '\n';
constexpr std::array detailed_emit_statistics{
std::pair{"replay", "backend_emit_replay_dirty_ms"},
std::pair{"layout", "backend_emit_layout_ms"},
std::pair{"prepare", "backend_emit_prepare_ms"},
std::pair{"build", "backend_emit_plan_build_ms"},
std::pair{"contract", "backend_emit_contract_ms"},
std::pair{"stream", "backend_emit_stream_ms"},
std::pair{"stream-freeze", "backend_emit_stream_freeze_ms"},
std::pair{"commit", "backend_emit_commit_ms"},
std::pair{"reset", "backend_emit_plan_reset_ms"},
std::pair{"artifact", "backend_emit_artifact_create_ms"},
std::pair{"artifact-freeze", "backend_emit_artifact_freeze_ms"},
std::pair{"packet", "backend_emit_packet_encode_ms"}};
output << " emit-detail " << id;
for (const auto& [label, statistic] : detailed_emit_statistics)
output << " " << label << "95/max="
<< counter(prefix + statistic + "/p95") << "/"
<< counter(prefix + statistic + "/max");
output << '\n';
constexpr std::array detailed_drp_statistics{
std::pair{"validate", "backend_drp_validation_ms"},
std::pair{"state", "backend_drp_state_ms"},
std::pair{"buffer", "backend_drp_buffer_create_ms"},
std::pair{"texture", "backend_drp_texture_create_ms"},
std::pair{"shader", "backend_drp_shader_create_ms"},
std::pair{"shader-compile", "backend_drp_shader_compile_ms"},
std::pair{"shader-module", "backend_drp_shader_module_create_ms"},
std::pair{"pipeline", "backend_drp_pipeline_create_ms"},
std::pair{"binding", "backend_drp_binding_create_ms"},
std::pair{"upload", "backend_drp_upload_ms"},
std::pair{"decode", "backend_drp_upload_decode_ms"},
std::pair{"vk-alloc", "backend_drp_upload_vulkan_allocate_ms"},
std::pair{"host-copy", "backend_drp_upload_host_copy_ms"},
std::pair{"cmd-alloc", "backend_drp_upload_command_allocate_ms"},
std::pair{"cmd-record", "backend_drp_upload_command_record_ms"},
std::pair{"fence-create", "backend_drp_upload_fence_create_ms"},
std::pair{"enqueue", "backend_drp_upload_submit_enqueue_ms"},
std::pair{"queue-wait", "backend_drp_upload_submit_queue_wait_ms"},
std::pair{"queue-submit", "backend_drp_upload_queue_submit_ms"},
std::pair{"fence-wait", "backend_drp_upload_fence_wait_ms"},
std::pair{"retire", "backend_drp_upload_retire_ms"},
std::pair{"transfer", "backend_drp_transfer_ms"},
std::pair{"record", "backend_drp_record_ms"}};
output << " drp-detail " << id;
for (const auto& [label, statistic] : detailed_drp_statistics)
output << " " << label << "95/max="
<< counter(prefix + statistic + "/p95") << "/"
<< counter(prefix + statistic + "/max");
output << '\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")
<< " worker_busy=" << counter("worker_busy_pct") << "%"
<< " worker_cpu=" << counter("worker_cpu_pct") << "%"
<< " sampled_busy_max="
<< counter("sampled_longest_busy_ms") << " ms"
<< " cooperative_yields=" << counter("cooperative_yields")
<< " cooperative_wait=" << counter("cooperative_wait_ms")
<< " ms\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() {
Configured_Plots::shutdown();
}
}
}
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;
}
aethera::initialize_runtime({});
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);
}
aethera::mcp::benchmarks::shutdown_configured_benchmark();
benchmark::Shutdown();
return 0;
}