653 lines
30 KiB
C++
653 lines
30 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 <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("3d")) {
|
||
configuration_error = "the Gallery has no 3D 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];
|
||
request.event.time_milliseconds = 1'000.0 +
|
||
static_cast<double>(sequence) *
|
||
(1'000.0 / static_cast<double>(configuration.input_rate_hz));
|
||
request.event.position = {
|
||
360.0 + static_cast<double>(
|
||
static_cast<std::int64_t>((sequence + plot_index * 7U) % 121U) - 60),
|
||
210.0 + static_cast<double>(
|
||
static_cast<std::int64_t>((sequence * 3U + plot_index * 11U) % 81U) - 40)};
|
||
request.event.global_position = request.event.position;
|
||
if (workload == Input_Workload::wheel ||
|
||
(workload == Input_Workload::mixed && (plot_index & 1U) != 0U)) {
|
||
request.event.type = Event_Type::wheel;
|
||
request.event.pixel_delta_y = (sequence & 1U) != 0U ? 120.0 : -120.0;
|
||
request.event.angle_delta_y = request.event.pixel_delta_y;
|
||
return request;
|
||
}
|
||
request.event.type = Event_Type::pointer_move;
|
||
request.event.button = Mouse_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;
|
||
plots = shared_plots;
|
||
streams = shared_streams;
|
||
return;
|
||
}
|
||
shared_service = Control_Service::create();
|
||
shared_workload = std::make_shared<Concurrent_Workload_State>();
|
||
shared_workload->completed.reserve(configuration.plot_ids.size());
|
||
shared_plots.reserve(configuration.plot_ids.size());
|
||
shared_streams.reserve(configuration.plot_ids.size());
|
||
for (std::size_t index = 0; index < configuration.plot_ids.size();
|
||
++index) {
|
||
auto plot = shared_service->find_plot(configuration.plot_ids[index]);
|
||
if (!plot)
|
||
throw std::logic_error("configured benchmark Plot is unavailable");
|
||
auto completed = std::make_shared<std::atomic_uint64_t>(0);
|
||
shared_workload->completed.push_back(completed);
|
||
const auto stream = plot->subscribe(
|
||
[completed = std::move(completed)](
|
||
std::shared_ptr<const web::Plot_Stream_Frame> frame) {
|
||
if (!frame || !frame->pixels) return;
|
||
completed->fetch_add(
|
||
1, std::memory_order_relaxed);
|
||
});
|
||
plot->configure_stream(
|
||
stream, configuration.width, configuration.height);
|
||
shared_plots.push_back(std::move(plot));
|
||
shared_streams.push_back(stream);
|
||
}
|
||
service = shared_service;
|
||
workload = shared_workload;
|
||
plots = shared_plots;
|
||
streams = shared_streams;
|
||
}
|
||
|
||
void TearDown(const benchmark::State&) override {}
|
||
|
||
static void shutdown() {
|
||
if (!shared_workload) return;
|
||
for (std::size_t index = 0; index < shared_plots.size(); ++index)
|
||
shared_plots[index]->unsubscribe(shared_streams[index]);
|
||
shared_plots.clear();
|
||
shared_streams.clear();
|
||
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 = *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& plot : plots) plot->reset_diagnostics();
|
||
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 < plots.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 = plots[index]->diagnostics();
|
||
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");
|
||
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 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;
|
||
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{};
|
||
inline static std::vector<std::shared_ptr<web::Plot>> shared_plots{};
|
||
inline static std::vector<web::Plot::Stream_Id> shared_streams{};
|
||
std::shared_ptr<Control_Service> service{};
|
||
std::shared_ptr<Concurrent_Workload_State> workload{};
|
||
std::vector<std::shared_ptr<web::Plot>> plots{};
|
||
std::vector<web::Plot::Stream_Id> streams{};
|
||
};
|
||
|
||
BENCHMARK_DEFINE_F(Configured_Plots, Run)(benchmark::State& state) {
|
||
run(state);
|
||
}
|
||
|
||
/* Fixture 静态持有所选 Plot;Google 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';
|
||
}
|
||
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") << "%\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;
|
||
}
|