From b34c0d31b14ffb025e2f6cdc3eb73ba7995e4e05 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Wed, 12 Aug 2026 08:09:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E5=AE=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web_server/CMakeLists.txt | 5 +- web_server/app/Gallery_Enum.h | 34 +++ web_server/app/Gallery_Observer_Adminive.h | 110 +++++++++ web_server/app/Gallery_Plot_Session.cpp | 157 +++--------- web_server/app/Gallery_Protocol.cpp | 267 +++++++++++++-------- web_server/app/Gallery_Renderables.h | 136 +---------- web_server/app/Web_Event_Adapter.cpp | 54 ++--- web_server/tests/Web_Bridge_Tests.cpp | 86 ++++--- 8 files changed, 422 insertions(+), 427 deletions(-) create mode 100644 web_server/app/Gallery_Enum.h create mode 100644 web_server/app/Gallery_Observer_Adminive.h diff --git a/web_server/CMakeLists.txt b/web_server/CMakeLists.txt index 1e3cf80..e80706b 100644 --- a/web_server/CMakeLists.txt +++ b/web_server/CMakeLists.txt @@ -1,4 +1,4 @@ -set(Renderive_Web_dependencies global::drogon global::spdlog) +set(Renderive_Web_dependencies global::drogon global::spdlog global::magic_enum) if (RENDERIVE_BUILD_TESTS) list(APPEND Renderive_Web_dependencies global::GTest) endif () @@ -16,6 +16,7 @@ function(renderive_find_web_dependencies) rcl_load_dependency_environment(${Renderive_Web_dependencies}) find_package(Drogon CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) + find_package(magic_enum CONFIG REQUIRED) if (RENDERIVE_BUILD_TESTS) find_package(GTest CONFIG REQUIRED) endif () @@ -45,7 +46,7 @@ target_include_directories(Renderive_Web PUBLIC target_compile_features(Renderive_Web PUBLIC cxx_std_20) target_compile_definitions(Renderive_Web PRIVATE NOMINMAX) target_link_libraries(Renderive_Web - PUBLIC Renderive_render_2D Drogon::Drogon + PUBLIC Renderive_render_2D Drogon::Drogon Adminive::MagicEnum magic_enum::magic_enum PRIVATE Adminive::Nlohmann spdlog::spdlog ) set(Renderive_Web_Server_source_dir "${CMAKE_CURRENT_LIST_DIR}/server") diff --git a/web_server/app/Gallery_Enum.h b/web_server/app/Gallery_Enum.h new file mode 100644 index 0000000..585bdf6 --- /dev/null +++ b/web_server/app/Gallery_Enum.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace renderive::web { + +template +requires std::is_enum_v +std::string gallery_enum_id(Enum value) { + std::string result(magic_enum::enum_name(value)); + std::ranges::transform(result, result.begin(), [](unsigned char character) { + return static_cast(std::tolower(character)); + }); + return result; +} + +template +requires std::is_enum_v +std::optional gallery_enum_cast(std::string_view identifier) { + for (const Enum value : magic_enum::enum_values()) { + if (gallery_enum_id(value) == identifier) + return value; + } + return std::nullopt; +} + +} // namespace renderive::web diff --git a/web_server/app/Gallery_Observer_Adminive.h b/web_server/app/Gallery_Observer_Adminive.h new file mode 100644 index 0000000..d2cd0c8 --- /dev/null +++ b/web_server/app/Gallery_Observer_Adminive.h @@ -0,0 +1,110 @@ +#pragma once + +#include "Gallery_Enum.h" +#include "render_2D/plot/Plot_Core.h" + +#include "adminive/adminive.hpp" +#include "adminive/adapters/magic_enum.hpp" + +#include +#include +#include + +namespace renderive::web { + +struct Gallery_Consumer_Feedback_Snapshot { + bool master_enabled{}; + bool pixel_enabled{}; + bool presentation_enabled{}; + bool manual_enabled{}; + double manual_fps{}; + std::string source; + std::uint64_t pixel_interval_ns{}; + std::uint64_t presentation_interval_ns{}; + std::uint64_t manual_interval_ns{}; +}; + +} // namespace renderive::web + +namespace adminive { + +template +struct Value_Adapter { + using value_type = std::string; + + static std::string read(renderive::Frame_Control_Mode value) { + return renderive::web::gallery_enum_id(value); + } + static void write(renderive::Frame_Control_Mode& target, std::string value) { + const auto parsed = + renderive::web::gallery_enum_cast(value); + if (!parsed) + throw std::invalid_argument("unknown frame control mode: " + value); + target = *parsed; + } +}; + +template <> +struct Type_Descriptor { + static auto get() { + using T = renderive::Frame_Observer_Snapshot; + return object("kernel_observer", + ADMINIVE_FIELD_LABEL(T, mode, "模式"), + ADMINIVE_FIELD_LABEL(T, last_event, "最近事件"), + ADMINIVE_FIELD_LABEL(T, limit_state, "当前瓶颈"), + ADMINIVE_FIELD_LABEL(T, frequency_hz, "配置频率"), + ADMINIVE_FIELD_LABEL(T, observation_count, "观察次数"), + ADMINIVE_FIELD_LABEL(T, produced_frame_count, "发布"), + ADMINIVE_FIELD_LABEL(T, consumed_frame_count, "完成"), + ADMINIVE_FIELD_LABEL(T, dropped_frame_count, "丢弃"), + ADMINIVE_FIELD_LABEL(T, failed_operation_count, "失败"), + ADMINIVE_FIELD_LABEL(T, pending_frame_count, "待处理"), + ADMINIVE_FIELD_LABEL(T, latest_sequence, "最新序号"), + ADMINIVE_FIELD_LABEL(T, paint_duration_ns, "PaintEvent"), + ADMINIVE_FIELD_LABEL(T, render_duration_ns, "后台渲染"), + ADMINIVE_FIELD_LABEL(T, target_interval_ns, "目标间隔"), + ADMINIVE_FIELD_LABEL(T, frequency_limit_enabled, "频率限制"), + ADMINIVE_FIELD_LABEL(T, consumer_feedback_enabled, "Kernel 反馈有效"), + ADMINIVE_FIELD_LABEL(T, bottleneck_duration_ns, "内部瓶颈"), + ADMINIVE_FIELD_LABEL(T, consumer_sample_interval_ns, "消费者原始采样"), + ADMINIVE_FIELD_LABEL(T, consumer_smoothed_interval_ns, "消费者平滑周期"), + ADMINIVE_FIELD_LABEL(T, consumer_variation_ns, "消费者抖动"), + ADMINIVE_FIELD_LABEL(T, consumer_safety_interval_ns, "消费者安全期限"), + ADMINIVE_FIELD_LABEL(T, consumer_interval_ns, "消费者限速周期"), + ADMINIVE_FIELD_LABEL(T, next_refresh_interval_ns, "下次刷新"), + ADMINIVE_FIELD_LABEL(T, paint_lease_wait_ns, "Painter Lease 等待"), + ADMINIVE_FIELD_LABEL(T, paint_state_wait_ns, "Paint State 等待"), + ADMINIVE_FIELD_LABEL(T, publish_state_wait_ns, "Publish State 等待"), + ADMINIVE_FIELD_LABEL(T, ready_wait_ns, "Ready 等待"), + ADMINIVE_FIELD_LABEL(T, frame_age_at_render_ns, "开始渲染时帧龄"), + ADMINIVE_FIELD_LABEL(T, render_lease_wait_ns, "Render Lease 等待"), + ADMINIVE_FIELD_LABEL(T, render_state_wait_ns, "Render State 等待"), + ADMINIVE_FIELD_LABEL(T, render_finish_state_wait_ns, "Render Finish 等待"), + ADMINIVE_FIELD_LABEL(T, queue_wait_ns, "回放队列等待"), + ADMINIVE_FIELD_LABEL(T, end_to_end_ns, "端到端延迟")) + .label("Kernel Observer"); + } +}; + +template <> +struct Type_Descriptor { + static auto get() { + using T = renderive::web::Gallery_Consumer_Feedback_Snapshot; + return object("consumer_feedback", + ADMINIVE_FIELD_LABEL(T, master_enabled, "消费者反馈总开关"), + ADMINIVE_FIELD_LABEL(T, pixel_enabled, "像素响应反馈"), + ADMINIVE_FIELD_LABEL(T, presentation_enabled, "浏览器呈现反馈"), + ADMINIVE_FIELD_LABEL(T, manual_enabled, "手动消费者反馈"), + ADMINIVE_FIELD_LABEL(T, manual_fps, "手动消费者 FPS"), + ADMINIVE_FIELD_LABEL(T, source, "生效反馈来源"), + ADMINIVE_FIELD_LABEL(T, pixel_interval_ns, "像素响应周期"), + ADMINIVE_FIELD_LABEL(T, presentation_interval_ns, "浏览器呈现周期"), + ADMINIVE_FIELD_LABEL(T, manual_interval_ns, "手动消费者周期")) + .label("Consumer Feedback"); + } +}; + +static_assert(Described_Type); +static_assert(Described_Type); + +} // namespace adminive diff --git a/web_server/app/Gallery_Plot_Session.cpp b/web_server/app/Gallery_Plot_Session.cpp index 002e187..db88fd3 100644 --- a/web_server/app/Gallery_Plot_Session.cpp +++ b/web_server/app/Gallery_Plot_Session.cpp @@ -1,5 +1,7 @@ #include "Gallery_Plot_Session.h" #include "Gallery_Controls.h" +#include "Gallery_Enum.h" +#include "Gallery_Observer_Adminive.h" #include "Gallery_Protocol.h" #include "Gallery_Session_Control_Adminive.h" #include "Pixel_Frame.h" @@ -75,26 +77,11 @@ std::optional action_number(const Gallery_Action_Request& request) { return std::get(*request.argument); } Frame_Control_Mode core_frame_mode(Gallery_Frame_Mode mode) { - switch (mode) { - case Gallery_Frame_Mode::Manual: - return Frame_Control_Mode::Manual; - case Gallery_Frame_Mode::Playback: - return Frame_Control_Mode::Playback; - case Gallery_Frame_Mode::Low_Latency: - return Frame_Control_Mode::Low_Latency; - } - return Frame_Control_Mode::Low_Latency; -} -std::string_view frame_mode_name(Gallery_Frame_Mode mode) { - switch (mode) { - case Gallery_Frame_Mode::Manual: - return "manual"; - case Gallery_Frame_Mode::Playback: - return "playback"; - case Gallery_Frame_Mode::Low_Latency: - return "low_latency"; - } - return "low_latency"; + const auto result = magic_enum::enum_cast( + magic_enum::enum_name(mode)); + if (!result) + throw std::logic_error("Gallery frame mode has no Kernel strategy"); + return *result; } } // namespace class Gallery_Scene final { @@ -118,7 +105,7 @@ public: session_control_ = std::make_unique( plot_, *primary_, feedback_policy_); register_controls(); - set_performance_plot_name(plot_, case_id_ + "/" + std::string(frame_mode_name(frame_mode_))); + set_performance_plot_name(plot_, case_id_ + "/" + gallery_enum_id(frame_mode_)); if (frame_mode_ == Gallery_Frame_Mode::Low_Latency) plot_.set_max_render_fps(30.0); plot_.activate_view(); @@ -434,9 +421,24 @@ public: const double pixel_fps = recent_pixel_rate(pixel_history_, telemetry_now); const double pixel_megabytes_per_second = recent_pixel_megabytes_per_second( pixel_history_, telemetry_now); + const bool low_latency = frame_mode_ == Gallery_Frame_Mode::Low_Latency; + const Gallery_Consumer_Feedback_Snapshot consumer_feedback{ + low_latency && feedback_policy_.enabled, + low_latency && feedback_policy_.pixel, + low_latency && feedback_policy_.presentation, + low_latency && feedback_policy_.manual, + low_latency ? feedback_policy_.manual_fps : 0.0, + consumer_feedback_source_, + consumer_pixel_interval_ns_, + consumer_presentation_interval_ns_, + consumer_manual_interval_ns_}; + nlohmann::json observer_data = + adminive::to_frontend_json(observer); + nlohmann::json consumer_feedback_data = + adminive::to_frontend_json(consumer_feedback); nlohmann::json telemetry{ {"case", case_id_}, - {"frame_mode", frame_mode_name(frame_mode_)}, + {"frame_mode", gallery_enum_id(frame_mode_)}, {"frame_index", frame_index_}, { "viewport", { @@ -467,75 +469,11 @@ public: { "automatic_low_latency_scheduler", automatic_low_latency_ && frame_mode_ == Gallery_Frame_Mode::Low_Latency - }, - {"kernel_paint_ms", static_cast(observer.paint_duration_ns) / 1e6}, - {"kernel_render_ms", static_cast(observer.render_duration_ns) / 1e6}, - {"kernel_bottleneck_ms", static_cast(observer.bottleneck_duration_ns) / 1e6}, - {"kernel_end_to_end_ms", static_cast(observer.end_to_end_ns) / 1e6} - } - }, - { - "kernel_observer", { - {"mode", frame_mode_name(frame_mode_)}, - {"last_event", observer.last_event}, - {"limit_state", observer.limit_state}, - {"configured_frequency_hz", observer.frequency_hz}, - {"observation_count", observer.observation_count}, - {"produced_frame_count", observer.produced_frame_count}, - {"consumed_frame_count", observer.consumed_frame_count}, - {"dropped_frame_count", observer.dropped_frame_count}, - {"failed_operation_count", observer.failed_operation_count}, - {"pending_frame_count", observer.pending_frame_count}, - {"latest_sequence", observer.latest_sequence}, - {"paint_duration_ns", observer.paint_duration_ns}, - {"render_duration_ns", observer.render_duration_ns}, - {"target_interval_ns", observer.target_interval_ns}, - {"frequency_limit_enabled", observer.frequency_limit_enabled}, - {"consumer_feedback_enabled", observer.consumer_feedback_enabled}, - { - "consumer_feedback_master_enabled", - frame_mode_ == Gallery_Frame_Mode::Low_Latency && - feedback_policy_.enabled - }, - { - "consumer_pixel_feedback_enabled", - frame_mode_ == Gallery_Frame_Mode::Low_Latency && - feedback_policy_.pixel - }, - { - "consumer_presentation_feedback_enabled", - frame_mode_ == Gallery_Frame_Mode::Low_Latency && - feedback_policy_.presentation - }, - { - "consumer_manual_feedback_enabled", - frame_mode_ == Gallery_Frame_Mode::Low_Latency && - feedback_policy_.manual - }, - {"consumer_manual_fps", frame_mode_ == Gallery_Frame_Mode::Low_Latency ? feedback_policy_.manual_fps : 0.0}, - {"consumer_feedback_source", consumer_feedback_source_}, - {"consumer_pixel_interval_ns", consumer_pixel_interval_ns_}, - {"consumer_presentation_interval_ns", consumer_presentation_interval_ns_}, - {"consumer_manual_interval_ns", consumer_manual_interval_ns_}, - {"bottleneck_duration_ns", observer.bottleneck_duration_ns}, - {"consumer_sample_interval_ns", observer.consumer_sample_interval_ns}, - {"consumer_smoothed_interval_ns", observer.consumer_smoothed_interval_ns}, - {"consumer_variation_ns", observer.consumer_variation_ns}, - {"consumer_safety_interval_ns", observer.consumer_safety_interval_ns}, - {"consumer_interval_ns", observer.consumer_interval_ns}, - {"next_refresh_interval_ns", observer.next_refresh_interval_ns}, - {"paint_lease_wait_ns", observer.paint_lease_wait_ns}, - {"paint_state_wait_ns", observer.paint_state_wait_ns}, - {"publish_state_wait_ns", observer.publish_state_wait_ns}, - {"ready_wait_ns", observer.ready_wait_ns}, - {"frame_age_at_render_ns", observer.frame_age_at_render_ns}, - {"render_lease_wait_ns", observer.render_lease_wait_ns}, - {"render_state_wait_ns", observer.render_state_wait_ns}, - {"render_finish_state_wait_ns", observer.render_finish_state_wait_ns}, - {"queue_wait_ns", observer.queue_wait_ns}, - {"end_to_end_ns", observer.end_to_end_ns} + } } }, + {"kernel_observer", std::move(observer_data)}, + {"consumer_feedback", std::move(consumer_feedback_data)}, { "client_performance", { {"transport_fps", client_transport_fps_}, @@ -556,49 +494,12 @@ public: }, {"last_action_result", last_action_result_} }; - if (frame_mode_ == Gallery_Frame_Mode::Low_Latency) { - telemetry["render_fps"] = plot_.max_render_fps(); - telemetry["refresh_feedback_hz"] = plot_.refresh_feedback_snapshot().frequency_hz; - const auto scheduler_interval_ns = kernel_refresh_interval_ns(); - telemetry["low_latency_limit"] = { - {"current", observer.limit_state}, - {"frequency_limited", observer.limit_state == "frequency_limited"}, - {"paint_limited", observer.limit_state == "paint_limited"}, - {"render_limited", observer.limit_state == "render_limited"}, - {"consumer_limited", observer.limit_state == "consumer_limited"}, - {"configured_frequency_hz", observer.frequency_hz}, - {"frequency_limit_enabled", observer.frequency_limit_enabled}, - {"consumer_feedback_enabled", observer.consumer_feedback_enabled}, - {"consumer_feedback_master_enabled", feedback_policy_.enabled}, - {"consumer_pixel_feedback_enabled", feedback_policy_.pixel}, - {"consumer_presentation_feedback_enabled", feedback_policy_.presentation}, - {"consumer_manual_feedback_enabled", feedback_policy_.manual}, - {"consumer_manual_fps", feedback_policy_.manual_fps}, - {"consumer_feedback_source", consumer_feedback_source_}, - {"consumer_pixel_interval_ns", consumer_pixel_interval_ns_}, - {"consumer_presentation_interval_ns", consumer_presentation_interval_ns_}, - {"consumer_manual_interval_ns", consumer_manual_interval_ns_}, - {"target_interval_ns", observer.target_interval_ns}, - {"paint_duration_ns", observer.paint_duration_ns}, - {"render_duration_ns", observer.render_duration_ns}, - {"bottleneck_duration_ns", observer.bottleneck_duration_ns}, - {"consumer_sample_interval_ns", observer.consumer_sample_interval_ns}, - {"consumer_smoothed_interval_ns", observer.consumer_smoothed_interval_ns}, - {"consumer_variation_ns", observer.consumer_variation_ns}, - {"consumer_safety_interval_ns", observer.consumer_safety_interval_ns}, - {"consumer_interval_ns", observer.consumer_interval_ns}, - {"scheduler_interval_ns", scheduler_interval_ns}, - {"scheduler_frequency_hz", scheduler_interval_ns == 0 ? 0.0 : 1'000'000'000.0 / static_cast(scheduler_interval_ns)} - }; - } if (primary_) { telemetry["renderable"] = { {"object_name", primary_->object_name()}, {"visible", primary_->is_visible()}, { - "cache_mode", primary_->get_cache_mode() == Renderable_Cache_Mode::Direct - ? "Direct" - : "Local_Pixel" + "cache_mode", magic_enum::enum_name(primary_->get_cache_mode()) } }; } @@ -887,7 +788,7 @@ private: {"unix_ms", unix_ms}, {"session_id", session_id_}, {"case", case_id_}, - {"frame_mode", frame_mode_name(frame_mode_)}, + {"frame_mode", gallery_enum_id(frame_mode_)}, {"configured_fps", low_latency ? plot_.max_render_fps() : 0.0}, {"automatic_scheduler_interval_ns", scheduler_interval_ns}, {"automatic_scheduler_fps", scheduler_interval_ns == 0 ? 0.0 : 1'000'000'000.0 / static_cast(scheduler_interval_ns)}, diff --git a/web_server/app/Gallery_Protocol.cpp b/web_server/app/Gallery_Protocol.cpp index ac01a17..90fa97f 100644 --- a/web_server/app/Gallery_Protocol.cpp +++ b/web_server/app/Gallery_Protocol.cpp @@ -1,4 +1,6 @@ #include "Gallery_Protocol.h" +#include "Gallery_Enum.h" +#include "Gallery_Observer_Adminive.h" #include "Gallery_Renderables.h" #include "Gallery_Session_Control_Adminive.h" @@ -148,28 +150,6 @@ Json case_contract(std::string_view case_id) { return item ? adminive::to_frontend_json(*item) : Json::object(); } -std::string_view frame_mode_id(Gallery_Frame_Mode mode) { - switch (mode) { - case Gallery_Frame_Mode::Manual: - return "manual"; - case Gallery_Frame_Mode::Low_Latency: - return "low_latency"; - case Gallery_Frame_Mode::Playback: - return "playback"; - } - return "low_latency"; -} - -std::optional parse_frame_mode(std::string_view value) { - if (value == "manual") - return Gallery_Frame_Mode::Manual; - if (value == "low_latency") - return Gallery_Frame_Mode::Low_Latency; - if (value == "playback") - return Gallery_Frame_Mode::Playback; - return std::nullopt; -} - Json dashboard_field(std::string_view label, std::string_view source, std::string_view format = "integer", int digits = -1) { Json result{{"label", label}, {"source", source}, {"format", format}}; @@ -178,10 +158,110 @@ Json dashboard_field(std::string_view label, std::string_view source, return result; } -Json mapped_dashboard_field(std::string_view label, std::string_view source, - std::string_view format, std::string_view value_map) { - Json result = dashboard_field(label, source, format); - result["value_map"] = value_map; +template +const Json& described_field(std::string_view name) { + static const Json descriptor = adminive::to_descriptor_json(); + const auto& fields = descriptor.at("fields"); + const auto found = std::find_if(fields.begin(), fields.end(), [name](const Json& field) { + return field.at("name").template get() == name; + }); + if (found == fields.end()) + throw std::logic_error("missing Adminive dashboard field: " + std::string(name)); + return *found; +} + +template +std::string described_source(std::string_view prefix, std::string_view name) { + static_cast(described_field(name)); + return std::string(prefix) + "." + std::string(name); +} + +template +Json described_dashboard_field(std::string_view prefix, std::string_view name) { + const Json& field = described_field(name); + const std::string value_type = field.at("value_type"); + std::string format = "integer"; + if (name.ends_with("_ns")) + format = "nanoseconds"; + else if (name.ends_with("_fps")) + format = "fps"; + else if (name.ends_with("frequency_hz")) + format = "frequency"; + else if (name == "source") + format = "flags"; + else if (value_type == "boolean" || name == "limit_state") + format = "enum"; + else if (value_type == "string" || value_type == "enum") + format = "text"; + Json result = dashboard_field( + field.at("presentation").at("label").template get(), + described_source(prefix, name), format, + name.ends_with("_fps") ? 2 : -1); + if (value_type == "boolean") + result["value_map"] = "enabled"; + else if (name == "limit_state") + result["value_map"] = "limit_state"; + else if (name == "source") + result["value_map"] = "consumer_feedback_source"; + return result; +} + +bool observer_header_field(std::string_view name) { + return name == "mode" || name == "last_event" || name == "limit_state"; +} + +bool observer_counter_field(std::string_view name) { + return name == "frequency_hz" || name == "latest_sequence" || + name.ends_with("_count"); +} + +bool observer_detail_field(std::string_view name) { + return name.ends_with("_wait_ns") || name == "frame_age_at_render_ns"; +} + +template +Json observer_fields(Predicate&& predicate) { + const Json descriptor = + adminive::to_descriptor_json(); + Json result = Json::array(); + for (const Json& field : descriptor.at("fields")) { + const std::string name = field.at("name"); + if (predicate(name)) { + Json field = described_dashboard_field( + "kernel_observer", name); + if (name == "frequency_hz") { + field["enabled_source"] = + described_source( + "kernel_observer", "frequency_limit_enabled"); + field["disabled_label"] = "已关闭"; + } else if (name == "end_to_end_ns") { + field["cell_class"] = "critical"; + } + result.push_back(std::move(field)); + } + } + return result; +} + +Json consumer_feedback_fields() { + const Json descriptor = + adminive::to_descriptor_json(); + Json result = Json::array(); + for (const Json& field : descriptor.at("fields")) { + const std::string name = field.at("name"); + result.push_back(described_dashboard_field( + "consumer_feedback", name)); + } + return result; +} + +Json observer_summary_fields() { + Json result = observer_fields([](std::string_view name) { + return !observer_header_field(name) && !observer_counter_field(name) && + !observer_detail_field(name); + }); + for (Json& field : consumer_feedback_fields()) + result.push_back(std::move(field)); return result; } @@ -189,18 +269,19 @@ Json dashboard_contract() { Json point_pair{{"label", "输入→绘制"}, {"format", "pair"}, {"sources", Json::array({"data_shape.input_points", "data_shape.rendered_elements"})}, {"separator", "→"}}; - Json bottleneck = mapped_dashboard_field("当前瓶颈", "low_latency_limit.current", - "duration_enum", "limit_state"); + Json bottleneck = described_dashboard_field( + "kernel_observer", "limit_state"); + bottleneck["format"] = "duration_enum"; bottleneck["duration_sources"] = { - {"frequency_limited", "kernel_observer.target_interval_ns"}, - {"paint_limited", "kernel_observer.paint_duration_ns"}, - {"render_limited", "kernel_observer.render_duration_ns"}, - {"consumer_limited", "kernel_observer.consumer_interval_ns"} + {"frequency_limited", described_source( + "kernel_observer", "target_interval_ns")}, + {"paint_limited", described_source( + "kernel_observer", "paint_duration_ns")}, + {"render_limited", described_source( + "kernel_observer", "render_duration_ns")}, + {"consumer_limited", described_source( + "kernel_observer", "consumer_interval_ns")} }; - Json configured_frequency = dashboard_field("配置频率", "kernel_observer.configured_frequency_hz", - "frequency"); - configured_frequency["enabled_source"] = "kernel_observer.frequency_limit_enabled"; - configured_frequency["disabled_label"] = "已关闭"; Json dashboard{ {"value_maps", { {"enabled", {{"true", "启用"}, {"false", "关闭"}}}, @@ -224,10 +305,13 @@ Json dashboard_contract() { dashboard_field("Core 渲染 ms", "performance.last_render_ms", "fixed", 2), dashboard_field("像素编码 ms", "performance.last_pixel_encode_ms", "fixed", 2), dashboard_field("响应负载 MB/s", "performance.pixel_payload_megabytes_per_second", "fixed", 1), - dashboard_field("Kernel 待处理", "kernel_observer.pending_frame_count"), - dashboard_field("Kernel 丢弃", "kernel_observer.dropped_frame_count"), + described_dashboard_field( + "kernel_observer", "pending_frame_count"), + described_dashboard_field( + "kernel_observer", "dropped_frame_count"), std::move(point_pair), std::move(bottleneck), - dashboard_field("观察事件", "kernel_observer.last_event", "text"), + described_dashboard_field( + "kernel_observer", "last_event"), dashboard_field("像素超时", "client_performance.frame_request_timeout_count"), dashboard_field("最近像素龄", "client_performance.last_pixel_receive_age_ms", "milliseconds", 0) })} @@ -238,66 +322,45 @@ Json dashboard_contract() { }}, {"limits", { {"aria_label", "低延迟限速来源"}, {"title", "限速来源"}, - {"current_source", "low_latency_limit.current"}, + {"current_source", described_source( + "kernel_observer", "limit_state")}, {"active_label", "当前瓶颈"}, {"inactive_label", "未受限"}, {"disabled_label", "已关闭"}, {"fields", Json::array({ {{"label", "Kernel 用户频率"}, {"active_value", "frequency_limited"}, - {"duration_source", "kernel_observer.target_interval_ns"}, - {"enabled_source", "kernel_observer.frequency_limit_enabled"}}, + {"duration_source", described_source( + "kernel_observer", "target_interval_ns")}, + {"enabled_source", described_source( + "kernel_observer", "frequency_limit_enabled")}}, {{"label", "Kernel PaintEvent"}, {"active_value", "paint_limited"}, - {"duration_source", "kernel_observer.paint_duration_ns"}}, + {"duration_source", described_source( + "kernel_observer", "paint_duration_ns")}}, {{"label", "Kernel 后台渲染"}, {"active_value", "render_limited"}, - {"duration_source", "kernel_observer.render_duration_ns"}}, + {"duration_source", described_source( + "kernel_observer", "render_duration_ns")}}, {{"label", "消费者反馈"}, {"active_value", "consumer_limited"}, - {"duration_source", "kernel_observer.consumer_interval_ns"}, - {"enabled_source", "kernel_observer.consumer_feedback_enabled"}} + {"duration_source", described_source( + "kernel_observer", "consumer_interval_ns")}, + {"enabled_source", described_source( + "kernel_observer", "consumer_feedback_enabled")}} })} }}, {"observer", { {"aria_label", "Kernel 低延迟全量统计"}, {"header", { {"prefix", "KERNEL"}, {"suffix", "OBSERVER"}, {"event_label", "事件"}, - {"mode", dashboard_field("", "kernel_observer.mode", "text")}, - {"limit", mapped_dashboard_field("", "kernel_observer.limit_state", "enum", "limit_state")}, - {"event", dashboard_field("", "kernel_observer.last_event", "text")} + {"mode", described_dashboard_field( + "kernel_observer", "mode")}, + {"limit", described_dashboard_field( + "kernel_observer", "limit_state")}, + {"event", described_dashboard_field( + "kernel_observer", "last_event")} }}, {"sections", Json::array({ - {{"class_name", "observer-counters"}, {"fields", Json::array({ - std::move(configured_frequency), - dashboard_field("观察次数", "kernel_observer.observation_count"), - dashboard_field("最新序号", "kernel_observer.latest_sequence"), - dashboard_field("发布", "kernel_observer.produced_frame_count"), - dashboard_field("完成", "kernel_observer.consumed_frame_count"), - dashboard_field("待处理", "kernel_observer.pending_frame_count"), - dashboard_field("丢弃", "kernel_observer.dropped_frame_count"), - dashboard_field("失败", "kernel_observer.failed_operation_count") - })}}, - {{"class_name", "latency-summary"}, {"fields", Json::array({ - dashboard_field("目标间隔", "kernel_observer.target_interval_ns", "nanoseconds"), - dashboard_field("PaintEvent", "kernel_observer.paint_duration_ns", "nanoseconds"), - dashboard_field("后台渲染", "kernel_observer.render_duration_ns", "nanoseconds"), - dashboard_field("内部瓶颈", "kernel_observer.bottleneck_duration_ns", "nanoseconds"), - mapped_dashboard_field("消费者总开关", "kernel_observer.consumer_feedback_master_enabled", "enum", "enabled"), - mapped_dashboard_field("Kernel 反馈有效", "kernel_observer.consumer_feedback_enabled", "enum", "enabled"), - mapped_dashboard_field("像素响应反馈", "kernel_observer.consumer_pixel_feedback_enabled", "enum", "enabled"), - mapped_dashboard_field("浏览器呈现反馈", "kernel_observer.consumer_presentation_feedback_enabled", "enum", "enabled"), - mapped_dashboard_field("手动消费者反馈", "kernel_observer.consumer_manual_feedback_enabled", "enum", "enabled"), - dashboard_field("手动消费者 FPS", "kernel_observer.consumer_manual_fps", "fps", 2), - mapped_dashboard_field("生效反馈来源", "kernel_observer.consumer_feedback_source", "flags", "consumer_feedback_source"), - dashboard_field("像素响应周期", "kernel_observer.consumer_pixel_interval_ns", "nanoseconds"), - dashboard_field("浏览器呈现周期", "kernel_observer.consumer_presentation_interval_ns", "nanoseconds"), - dashboard_field("手动消费者周期", "kernel_observer.consumer_manual_interval_ns", "nanoseconds"), - dashboard_field("消费者原始采样", "kernel_observer.consumer_sample_interval_ns", "nanoseconds"), - dashboard_field("消费者平滑周期", "kernel_observer.consumer_smoothed_interval_ns", "nanoseconds"), - dashboard_field("消费者抖动", "kernel_observer.consumer_variation_ns", "nanoseconds"), - dashboard_field("消费者安全期限", "kernel_observer.consumer_safety_interval_ns", "nanoseconds"), - dashboard_field("消费者限速周期", "kernel_observer.consumer_interval_ns", "nanoseconds"), - dashboard_field("消费者限速 FPS", "kernel_observer.consumer_interval_ns", "inverse_fps", 2), - dashboard_field("下次刷新", "kernel_observer.next_refresh_interval_ns", "nanoseconds"), - Json{{"label", "端到端延迟"}, {"source", "kernel_observer.end_to_end_ns"}, - {"format", "nanoseconds"}, {"cell_class", "critical"}} - })}}, + {{"class_name", "observer-counters"}, + {"fields", observer_fields(observer_counter_field)}}, + {{"class_name", "latency-summary"}, + {"fields", observer_summary_fields()}}, {{"class_name", "client-summary"}, {"aria_label", "浏览器消费者反馈全量统计"}, {"fields", Json::array({ dashboard_field("RAF 最新周期", "client_performance.display_interval_latest_ms", "milliseconds", 3), @@ -316,41 +379,35 @@ Json dashboard_contract() { dashboard_field("最近变化龄", "client_performance.last_pixel_change_age_ms", "milliseconds", 3) })}}, {{"class_name", "latency-details"}, {"aria_label", "Kernel 各阶段等待耗时"}, - {"fields", Json::array({ - dashboard_field("Painter Lease 等待", "kernel_observer.paint_lease_wait_ns", "nanoseconds"), - dashboard_field("Paint State 等待", "kernel_observer.paint_state_wait_ns", "nanoseconds"), - dashboard_field("Publish State 等待", "kernel_observer.publish_state_wait_ns", "nanoseconds"), - dashboard_field("Ready 等待", "kernel_observer.ready_wait_ns", "nanoseconds"), - dashboard_field("开始渲染时帧龄", "kernel_observer.frame_age_at_render_ns", "nanoseconds"), - dashboard_field("Render Lease 等待", "kernel_observer.render_lease_wait_ns", "nanoseconds"), - dashboard_field("Render State 等待", "kernel_observer.render_state_wait_ns", "nanoseconds"), - dashboard_field("Render Finish 等待", "kernel_observer.render_finish_state_wait_ns", "nanoseconds"), - dashboard_field("回放队列等待", "kernel_observer.queue_wait_ns", "nanoseconds") - })}} + {"fields", observer_fields(observer_detail_field)}} })} }} }; + dashboard["observer"]["descriptor"] = + adminive::to_descriptor_json(); + dashboard["observer"]["consumer_feedback_descriptor"] = + adminive::to_descriptor_json(); return dashboard; } Json frame_mode_contract(Gallery_Frame_Mode mode) { switch (mode) { case Gallery_Frame_Mode::Manual: - return {{"id", "manual"}, {"title", "手动刷新"}, + return {{"id", gallery_enum_id(mode)}, {"title", "手动刷新"}, {"strategy", "Manual_Refresh_Strategy"}, {"description", "显式准备、刷新和渲染;页面不会自动拉取像素。"}, {"automatic", false}, {"request_after_response", false}, {"request_on_animation_frame", false}, {"observer_visible", false}, {"frame_button_label", "手动刷新一帧"}, {"accent", "#ffd166aa"}, {"order", 10}}; case Gallery_Frame_Mode::Low_Latency: - return {{"id", "low_latency"}, {"title", "低延迟"}, + return {{"id", gallery_enum_id(mode)}, {"title", "低延迟"}, {"strategy", "Low_Latency_Strategy"}, {"description", "以最大 FPS 自动发布并消费最新帧,可观测丢帧与端到端延迟。"}, {"automatic", true}, {"request_after_response", true}, {"request_on_animation_frame", false}, {"observer_visible", true}, {"frame_button_label", "立即刷新"}, {"accent", "#45ddbeaa"}, {"order", 20}}; case Gallery_Frame_Mode::Playback: - return {{"id", "playback"}, {"title", "回放队列"}, + return {{"id", gallery_enum_id(mode)}, {"title", "回放队列"}, {"strategy", "Flow_Refresh_Strategy"}, {"description", "所有帧按队列顺序入队和消费,可观测队深与排队时间。"}, {"automatic", true}, {"request_after_response", true}, @@ -368,7 +425,8 @@ std::string Gallery_Protocol::catalog_json() { result["transport"] = {{"events", "websocket-text"}, {"pixels", "websocket-binary-rvp1"}, {"http_api", false}, {"socket_per_canvas", true}}; result["navigation"] = { - {"default_mode", "low_latency"}, {"all_categories_label", "全部"}, + {"default_mode", gallery_enum_id(Gallery_Frame_Mode::Low_Latency)}, + {"all_categories_label", "全部"}, {"catalog_loaded_text", "Kernel 策略目录已加载"}, {"hero_eyebrow", "REAL KERNEL STRATEGIES"}, {"hero_title", "对称策略页面,同一批控件,直接比较"} @@ -377,9 +435,7 @@ std::string Gallery_Protocol::catalog_json() { result["case_descriptor"] = adminive::to_descriptor_json(); result["frame_modes"] = Json::array(); - constexpr Gallery_Frame_Mode frame_modes[]{Gallery_Frame_Mode::Manual, - Gallery_Frame_Mode::Low_Latency, - Gallery_Frame_Mode::Playback}; + constexpr auto frame_modes = magic_enum::enum_values(); for (const auto mode : frame_modes) result["frame_modes"].push_back(frame_mode_contract(mode)); result["cases"] = Json::array(); @@ -392,7 +448,7 @@ std::string Gallery_Protocol::catalog_json() { const std::size_t controls = gallery_control_count() + gallery_renderable_control_count(item.id); for (const auto mode : frame_modes) { - const std::string key(frame_mode_id(mode)); + const std::string key = gallery_enum_id(mode); const std::size_t actions = gallery_detail::registered_actions(item.id, mode).size(); entry["control_count_by_mode"][key] = controls; @@ -412,7 +468,7 @@ std::string Gallery_Protocol::catalog_json() { {"manual_control_count", controls_total}, {"manual_action_count", actions_total}, {"frequency_modes", Json::array({"spectrum", "afterglow", "sweep_spectrum"})}, - {"image_interpolation_modes", Json::array({"Nearest", "Bilinear", "Bicubic"})}}; + {"image_interpolation_modes", magic_enum::enum_names()}}; return result.dump(); } @@ -493,7 +549,8 @@ std::optional Gallery_Protocol::open_request(std::string_v if (!is_case(value) || !request.contains("frame_mode") || !request.at("frame_mode").is_string()) return std::nullopt; - const auto mode = parse_frame_mode(request.at("frame_mode").get()); + const auto mode = gallery_enum_cast( + request.at("frame_mode").get()); return mode ? std::optional(Gallery_Open_Request{value, *mode}) : std::nullopt; } catch (const std::exception&) { diff --git a/web_server/app/Gallery_Renderables.h b/web_server/app/Gallery_Renderables.h index 5b89929..a6bb46f 100644 --- a/web_server/app/Gallery_Renderables.h +++ b/web_server/app/Gallery_Renderables.h @@ -4,6 +4,7 @@ #include "Gallery_Renderable_Types.h" #include "adminive/adminive.hpp" +#include "adminive/adapters/magic_enum.hpp" #include "adminive/adapters/nlohmann_json.hpp" #include @@ -11,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -22,86 +22,6 @@ namespace renderive::web::gallery_adminive { enum class Number_Locale_Choice { Point, Comma }; enum class Color_Map_Choice { Spectrum, Ember, Grayscale }; -template -struct Enum_Metadata; - -#define RENDERIVE_ENUM_METADATA(Type, ...) \ - template <> struct Enum_Metadata { static constexpr auto values = std::array{__VA_ARGS__}; } - -RENDERIVE_ENUM_METADATA(Orientation, - std::pair{Orientation::Horizontal, std::string_view{"Horizontal"}}, - std::pair{Orientation::Vertical, std::string_view{"Vertical"}}); -RENDERIVE_ENUM_METADATA(Renderable_Cache_Mode, - std::pair{Renderable_Cache_Mode::Direct, std::string_view{"Direct"}}, - std::pair{Renderable_Cache_Mode::Local_Pixel, std::string_view{"Local_Pixel"}}); -RENDERIVE_ENUM_METADATA(Line_Interpolation_Mode, - std::pair{Line_Interpolation_Mode::Nearest_Sample, std::string_view{"Nearest_Sample"}}, - std::pair{Line_Interpolation_Mode::Linear_Value, std::string_view{"Linear_Value"}}, - std::pair{Line_Interpolation_Mode::Linear_Power_Domain, std::string_view{"Linear_Power_Domain"}}, - std::pair{Line_Interpolation_Mode::Step_Left, std::string_view{"Step_Left"}}, - std::pair{Line_Interpolation_Mode::Step_Right, std::string_view{"Step_Right"}}, - std::pair{Line_Interpolation_Mode::Cubic_Value, std::string_view{"Cubic_Value"}}); -RENDERIVE_ENUM_METADATA(Image_Interpolation_Mode, - std::pair{Image_Interpolation_Mode::Nearest, std::string_view{"Nearest"}}, - std::pair{Image_Interpolation_Mode::Bilinear, std::string_view{"Bilinear"}}, - std::pair{Image_Interpolation_Mode::Bicubic, std::string_view{"Bicubic"}}); -RENDERIVE_ENUM_METADATA(Constellation_Diagram_Type, - std::pair{Constellation_Diagram_Type::Psk4, std::string_view{"PSK4"}}, - std::pair{Constellation_Diagram_Type::Psk8, std::string_view{"PSK8"}}, - std::pair{Constellation_Diagram_Type::Psk16, std::string_view{"PSK16"}}); -RENDERIVE_ENUM_METADATA(Line_Style, - std::pair{Line_Style::None, std::string_view{"None"}}, - std::pair{Line_Style::Solid, std::string_view{"Solid"}}, - std::pair{Line_Style::Dash, std::string_view{"Dash"}}, - std::pair{Line_Style::Dot, std::string_view{"Dot"}}); -RENDERIVE_ENUM_METADATA(Line_Cap, - std::pair{Line_Cap::Butt, std::string_view{"Butt"}}, - std::pair{Line_Cap::Square, std::string_view{"Square"}}, - std::pair{Line_Cap::Round, std::string_view{"Round"}}); -RENDERIVE_ENUM_METADATA(Line_Join, - std::pair{Line_Join::Miter, std::string_view{"Miter"}}, - std::pair{Line_Join::Bevel, std::string_view{"Bevel"}}, - std::pair{Line_Join::Round, std::string_view{"Round"}}); -RENDERIVE_ENUM_METADATA(Brush_Style, - std::pair{Brush_Style::None, std::string_view{"None"}}, - std::pair{Brush_Style::Solid, std::string_view{"Solid"}}); -RENDERIVE_ENUM_METADATA(Number_Locale_Choice, - std::pair{Number_Locale_Choice::Point, std::string_view{"."}}, - std::pair{Number_Locale_Choice::Comma, std::string_view{","}}); -RENDERIVE_ENUM_METADATA(Color_Map_Choice, - std::pair{Color_Map_Choice::Spectrum, std::string_view{"Spectrum"}}, - std::pair{Color_Map_Choice::Ember, std::string_view{"Ember"}}, - std::pair{Color_Map_Choice::Grayscale, std::string_view{"Grayscale"}}); - -#undef RENDERIVE_ENUM_METADATA - -template -struct Actual_Enum_Adapter { - static std::vector values() { - std::vector result; - result.reserve(Enum_Metadata::values.size()); - for (const auto& [value, name] : Enum_Metadata::values) - result.push_back(value); - return result; - } - - static std::string_view name(Enum value) { - for (const auto& [candidate, name] : Enum_Metadata::values) { - if (candidate == value) - return name; - } - return {}; - } - - static std::optional cast(std::string_view name) { - for (const auto& [value, candidate] : Enum_Metadata::values) { - if (candidate == name) - return value; - } - return std::nullopt; - } -}; - inline Color blend(Color first, Color second, double amount) { const auto channel = [amount](std::uint8_t a, std::uint8_t b) { return static_cast(std::clamp( @@ -111,15 +31,15 @@ inline Color blend(Color first, Color second, double amount) { channel(first.b, second.b), channel(first.a, second.a)}; } -inline Color_Map color_map(std::string_view palette) { +inline Color_Map color_map(Color_Map_Choice palette) { std::array stops{ Color{5, 10, 25, 255}, Color{20, 52, 112, 255}, Color{39, 196, 181, 255}, Color{242, 201, 76, 255}, Color{255, 76, 106, 255} }; - if (palette == "Ember") { + if (palette == Color_Map_Choice::Ember) { stops = {Color{10, 5, 8, 255}, Color{62, 18, 30, 255}, Color{153, 47, 39, 255}, Color{244, 132, 48, 255}, Color{255, 238, 166, 255}}; - } else if (palette == "Grayscale") { + } else if (palette == Color_Map_Choice::Grayscale) { stops = {Color{0, 0, 0, 255}, Color{52, 52, 52, 255}, Color{112, 112, 112, 255}, Color{188, 188, 188, 255}, Color{255, 255, 255, 255}}; } @@ -134,14 +54,12 @@ inline Color_Map color_map(std::string_view palette) { return Color_Map(std::move(colors)); } -inline std::string color_map_name(const Color_Map& value) { - for (const std::string_view name : {std::string_view{"Spectrum"}, - std::string_view{"Ember"}, - std::string_view{"Grayscale"}}) { - if (value.colors() == color_map(name).colors()) - return std::string(name); +inline Color_Map_Choice color_map_choice(const Color_Map& value) { + for (const Color_Map_Choice choice : magic_enum::enum_values()) { + if (value.colors() == color_map(choice).colors()) + return choice; } - return "Spectrum"; + return Color_Map_Choice::Spectrum; } template @@ -243,31 +161,6 @@ struct Value_Adapter<::Validated_Value, Json> static void write(Storage& target, T value) { target = std::move(value); } }; -#define RENDERIVE_ENUM_ADAPTER(Type) \ - template <> struct Enum_Adapter \ - : renderive::web::gallery_adminive::Actual_Enum_Adapter {} - -RENDERIVE_ENUM_ADAPTER(Orientation); -RENDERIVE_ENUM_ADAPTER(Renderable_Cache_Mode); -RENDERIVE_ENUM_ADAPTER(Line_Interpolation_Mode); -RENDERIVE_ENUM_ADAPTER(Image_Interpolation_Mode); -RENDERIVE_ENUM_ADAPTER(Constellation_Diagram_Type); -RENDERIVE_ENUM_ADAPTER(Line_Style); -RENDERIVE_ENUM_ADAPTER(Line_Cap); -RENDERIVE_ENUM_ADAPTER(Line_Join); -RENDERIVE_ENUM_ADAPTER(Brush_Style); -#undef RENDERIVE_ENUM_ADAPTER - -template <> -struct Enum_Adapter - : renderive::web::gallery_adminive::Actual_Enum_Adapter< - renderive::web::gallery_adminive::Number_Locale_Choice> {}; - -template <> -struct Enum_Adapter - : renderive::web::gallery_adminive::Actual_Enum_Adapter< - renderive::web::gallery_adminive::Color_Map_Choice> {}; - template struct Value_Adapter { using value_type = std::string; @@ -328,17 +221,10 @@ struct Value_Adapter { using Choice = renderive::web::gallery_adminive::Color_Map_Choice; using value_type = Choice; static Choice read(const renderive::Color_Map& value) { - const auto name = renderive::web::gallery_adminive::color_map_name(value); - if (name == "Ember") - return Choice::Ember; - if (name == "Grayscale") - return Choice::Grayscale; - return Choice::Spectrum; + return renderive::web::gallery_adminive::color_map_choice(value); } static void write(renderive::Color_Map& target, Choice value) { - const std::string_view name = value == Choice::Ember ? "Ember" : - value == Choice::Grayscale ? "Grayscale" : "Spectrum"; - target = renderive::web::gallery_adminive::color_map(name); + target = renderive::web::gallery_adminive::color_map(value); } }; diff --git a/web_server/app/Web_Event_Adapter.cpp b/web_server/app/Web_Event_Adapter.cpp index e78bb90..f2e51aa 100644 --- a/web_server/app/Web_Event_Adapter.cpp +++ b/web_server/app/Web_Event_Adapter.cpp @@ -1,4 +1,5 @@ #include "Web_Event_Adapter.h" +#include "Gallery_Enum.h" #include #include @@ -28,27 +29,13 @@ Keyboard_Modifier modifiers(const Json::Value& root) { Mouse_Button mouse_button(const Json::Value& root) { const std::string value = root["button"].isString() ? root["button"].asString() : "none"; - if (value == "left") - return Mouse_Button::Left; - if (value == "right") - return Mouse_Button::Right; - if (value == "middle") - return Mouse_Button::Middle; - return Mouse_Button::None; + return gallery_enum_cast(value).value_or(Mouse_Button::None); } Key key(const Json::Value& root) { const std::string value = root["key"].isString() ? root["key"].asString() : ""; - if (value == "Escape") - return Key::Escape; - if (value == "Enter") - return Key::Enter; - if (value == "Space") - return Key::Space; - if (value == "Delete") - return Key::Delete; - if (value == "Backspace") - return Key::Backspace; + if (const auto direct = magic_enum::enum_cast(value)) + return *direct; if (value == "ArrowLeft") return Key::Left; if (value == "ArrowRight") @@ -136,20 +123,14 @@ std::optional Web_Transport_Event_Decoder::decode( return Viewport_Resize{{static_cast(std::clamp(*width, 240.0, 1920.0)), static_cast(std::clamp(*height, 180.0, 1200.0))}}; } - Gallery_Request_Kind kind; - if (type == "gallery_catalog") - kind = Gallery_Request_Kind::Catalog; - else if (type == "gallery_open") - kind = Gallery_Request_Kind::Open; - else if (type == "gallery_patch") - kind = Gallery_Request_Kind::Patch; - else if (type == "gallery_action") - kind = Gallery_Request_Kind::Action; - else if (type == "gallery_observe") - kind = Gallery_Request_Kind::Observe; - else + constexpr std::string_view gallery_prefix = "gallery_"; + if (!std::string_view(type).starts_with(gallery_prefix)) return std::nullopt; - return Gallery_Request{kind, std::string(source)}; + const auto kind = gallery_enum_cast( + std::string_view(type).substr(gallery_prefix.size())); + return kind ? std::optional( + Gallery_Request{*kind, std::string(source)}) + : std::nullopt; } std::optional Render_2D_Web_Event_Decoder::decode( @@ -183,15 +164,10 @@ std::optional Web_Demo_Control_Decoder::decode( const std::string control = root["control"].asString(); if (control == "mode" && root["value"].isString()) { const std::string value = root["value"].asString(); - if (value == "AM") - return Set_Demo_Mode{Demo_Mode::Am}; - if (value == "FM") - return Set_Demo_Mode{Demo_Mode::Fm}; - if (value == "USB") - return Set_Demo_Mode{Demo_Mode::Usb}; - if (value == "LSB") - return Set_Demo_Mode{Demo_Mode::Lsb}; - return std::nullopt; + const auto mode = magic_enum::enum_cast( + value, magic_enum::case_insensitive); + return mode ? std::optional(Set_Demo_Mode{*mode}) + : std::nullopt; } if (control == "center_frequency_mhz") { const auto value = finite_number(root, "value"); diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index ce9d07c..75484c5 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -1,4 +1,5 @@ #include "../app/Gallery_Plot_Session.h" +#include "../app/Gallery_Observer_Adminive.h" #include "../app/Gallery_Protocol.h" #include "../app/Gallery_Renderables.h" #include "../app/Pixel_Frame.h" @@ -268,6 +269,30 @@ TEST(RenderiveWebGallery, CatalogOwnsDashboardFieldsAndDynamicBehavior) { EXPECT_TRUE(action->at("request_frame")); } +TEST(RenderiveWebGallery, ObserverTelemetryAndDashboardUseAdminiveDescriptors) { + const auto catalog = parse_json(Gallery_Protocol::catalog_json()); + const auto& observer_contract = catalog.at("dashboard").at("observer"); + const auto& observer_descriptor = observer_contract.at("descriptor"); + const auto& feedback_descriptor = + observer_contract.at("consumer_feedback_descriptor"); + + Gallery_Plot_Session session; + const auto opened = response_json(session.handle(gallery_request( + Gallery_Request_Kind::Open, open_message("spectrum", "manual")))); + const auto& telemetry = opened.at("telemetry"); + const auto& observer = telemetry.at("kernel_observer"); + const auto& feedback = telemetry.at("consumer_feedback"); + + EXPECT_EQ(observer.size(), observer_descriptor.at("fields").size()); + for (const auto& field : observer_descriptor.at("fields")) + EXPECT_TRUE(observer.contains(field.at("name").get())); + EXPECT_EQ(feedback.size(), feedback_descriptor.at("fields").size()); + for (const auto& field : feedback_descriptor.at("fields")) + EXPECT_TRUE(feedback.contains(field.at("name").get())); + EXPECT_EQ(observer.at("mode"), "manual"); + EXPECT_FALSE(telemetry.contains("low_latency_limit")); +} + TEST(RenderiveWebGallery, ActionsComeFromStructiveTypeMetadata) { using Spectrum_Schema = structive::type_descriptor_schema_t; using Time_Axis_Schema = structive::type_descriptor_schema_t; @@ -719,20 +744,21 @@ TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers ASSERT_TRUE(low_latency.handle(Frame_Request{})); const auto low_telemetry = observe(low_latency); EXPECT_EQ(low_telemetry.at("frame_mode"), "low_latency"); - EXPECT_GT(low_telemetry.at("kernel_observer").at("observation_count").get(), 0U); - EXPECT_TRUE(low_telemetry.contains("render_fps")); - ASSERT_TRUE(low_telemetry.contains("low_latency_limit")); - const auto& limit = low_telemetry.at("low_latency_limit"); - EXPECT_FALSE(limit.at("consumer_limited").get()); - EXPECT_FALSE(limit.at("frequency_limited").get()); - EXPECT_TRUE(limit.at("paint_limited").get() || limit.at("render_limited").get()); - EXPECT_EQ(limit.at("target_interval_ns"), 1); - EXPECT_EQ(limit.at("consumer_interval_ns"), 0); - EXPECT_GT(limit.at("bottleneck_duration_ns").get(), 1U); - EXPECT_TRUE(low_telemetry.at("kernel_observer").contains("paint_lease_wait_ns")); - EXPECT_TRUE(low_telemetry.at("kernel_observer").contains("render_finish_state_wait_ns")); + const auto& low_observer = low_telemetry.at("kernel_observer"); + EXPECT_GT(low_observer.at("observation_count").get(), 0U); + EXPECT_FALSE(low_telemetry.contains("render_fps")); + EXPECT_FALSE(low_telemetry.contains("low_latency_limit")); + EXPECT_NE(low_observer.at("limit_state"), "consumer_limited"); + EXPECT_NE(low_observer.at("limit_state"), "frequency_limited"); + EXPECT_TRUE(low_observer.at("limit_state") == "paint_limited" || + low_observer.at("limit_state") == "render_limited"); + EXPECT_EQ(low_observer.at("target_interval_ns"), 1); + EXPECT_EQ(low_observer.at("consumer_interval_ns"), 0); + EXPECT_GT(low_observer.at("bottleneck_duration_ns").get(), 1U); + EXPECT_TRUE(low_observer.contains("paint_lease_wait_ns")); + EXPECT_TRUE(low_observer.contains("render_finish_state_wait_ns")); constexpr std::array observer_fields{ - "mode", "last_event", "limit_state", "configured_frequency_hz", "observation_count", + "mode", "last_event", "limit_state", "frequency_hz", "observation_count", "produced_frame_count", "consumed_frame_count", "dropped_frame_count", "failed_operation_count", "pending_frame_count", "latest_sequence", "paint_duration_ns", "render_duration_ns", "target_interval_ns", "frequency_limit_enabled", @@ -744,7 +770,7 @@ TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers "render_finish_state_wait_ns", "queue_wait_ns", "end_to_end_ns" }; for (const auto field : observer_fields) - EXPECT_TRUE(low_telemetry.at("kernel_observer").contains(field)) << field; + EXPECT_TRUE(low_observer.contains(field)) << field; Gallery_Plot_Session playback; ASSERT_TRUE(playback.handle(gallery_request( @@ -838,7 +864,7 @@ TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggres const auto telemetry = observe_telemetry(session); EXPECT_LT(std::chrono::steady_clock::now() - started, std::chrono::milliseconds(250)) << iteration; - EXPECT_EQ(telemetry.at("kernel_observer").at("configured_frequency_hz"), 1000.0); + EXPECT_EQ(telemetry.at("kernel_observer").at("frequency_hz"), 1000.0); if (iteration == 0) first_pixels = frame->payload; last_pixels = frame->payload; @@ -949,7 +975,7 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback Gallery_Request_Kind::Observe, R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":50,"presentation_fps":50,"websocket_buffered_bytes":0,"changed_pixel_frames":10,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":80,"display_interval_ms":20,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); auto observer = observed.at("telemetry").at("kernel_observer"); - auto limit = observed.at("telemetry").at("low_latency_limit"); + auto feedback = observed.at("telemetry").at("consumer_feedback"); EXPECT_TRUE(observer.at("frequency_limit_enabled").get()); EXPECT_TRUE(observer.at("consumer_feedback_enabled").get()); EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 20'000'000); @@ -958,7 +984,7 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback EXPECT_EQ(observer.at("consumer_safety_interval_ns"), 20'000'000); EXPECT_EQ(observer.at("consumer_interval_ns"), 20'000'000); EXPECT_EQ(observer.at("limit_state"), "consumer_limited"); - EXPECT_EQ(limit.at("consumer_feedback_source"), "presentation"); + EXPECT_EQ(feedback.at("source"), "presentation"); observed = response_json(session.handle(gallery_request( Gallery_Request_Kind::Observe, R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":45,"presentation_fps":45,"websocket_buffered_bytes":0,"changed_pixel_frames":20,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":4,"display_interval_ms":22,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); @@ -999,31 +1025,35 @@ TEST(RenderiveWebGallery, ConsumerFeedbackSourcesCanBeSelectedIndependentlyAndMa Gallery_Request_Kind::Observe, R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":25,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":1,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":5,"display_interval_ms":16,"display_interval_latest_ms":16,"display_interval_p95_ms":17,"display_jitter_ms":1,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); auto observer = observed.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("consumer_feedback_source"), "pixel"); - EXPECT_EQ(observer.at("consumer_pixel_interval_ns"), 40'000'000); - EXPECT_EQ(observer.at("consumer_presentation_interval_ns"), 16'000'000); + auto feedback = observed.at("telemetry").at("consumer_feedback"); + EXPECT_EQ(feedback.at("source"), "pixel"); + EXPECT_EQ(feedback.at("pixel_interval_ns"), 40'000'000); + EXPECT_EQ(feedback.at("presentation_interval_ns"), 16'000'000); EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 40'000'000); observed = patch_controls(session, { {"feedback", {{"pixel", false}, {"presentation", true}}} }); observer = observed.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("consumer_feedback_source"), "presentation"); + feedback = observed.at("telemetry").at("consumer_feedback"); + EXPECT_EQ(feedback.at("source"), "presentation"); EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 16'000'000); observed = patch_controls(session, { {"feedback", {{"presentation", false}, {"manual", true}, {"manual_fps", 20.0}}} }); observer = observed.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("consumer_feedback_source"), "manual"); - EXPECT_EQ(observer.at("consumer_manual_interval_ns"), 50'000'000); + feedback = observed.at("telemetry").at("consumer_feedback"); + EXPECT_EQ(feedback.at("source"), "manual"); + EXPECT_EQ(feedback.at("manual_interval_ns"), 50'000'000); EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 50'000'000); observed = patch_controls(session, { {"feedback", {{"manual", false}}} }); observer = observed.at("telemetry").at("kernel_observer"); - EXPECT_EQ(observer.at("consumer_feedback_source"), "none"); + feedback = observed.at("telemetry").at("consumer_feedback"); + EXPECT_EQ(feedback.at("source"), "none"); EXPECT_FALSE(observer.at("consumer_feedback_enabled").get()); - EXPECT_TRUE(observer.at("consumer_feedback_master_enabled").get()); + EXPECT_TRUE(feedback.at("master_enabled").get()); EXPECT_EQ(observer.at("consumer_interval_ns"), 0); } @@ -1119,7 +1149,7 @@ TEST(RenderiveWebGallery, ProductionLowLatencySessionRendersWithoutPixelPulls) { EXPECT_TRUE(performance.at("automatic_low_latency_scheduler").get()); EXPECT_GE(performance.at("successful_render_count").get(), 4U); EXPECT_GT(performance.at("measured_fps").get(), 0.0); - EXPECT_DOUBLE_EQ(telemetry.at("kernel_observer").at("configured_frequency_hz"), 120.0); + EXPECT_DOUBLE_EQ(telemetry.at("kernel_observer").at("frequency_hz"), 120.0); EXPECT_EQ(telemetry.at("kernel_observer").at("target_interval_ns"), 8'333'333); EXPECT_TRUE(telemetry.at("kernel_observer").at("limit_state") == "frequency_limited" || telemetry.at("kernel_observer").at("limit_state") == "paint_limited" || @@ -1273,11 +1303,11 @@ TEST(RenderiveWebGallery, DirectPropertyChangesAndAllImageModesRender) { open_message("constellation", "low_latency")))); const auto patched = constellation.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","target":"constellation","patch":{"type":"PSK16","phase_offset_radians":0.75}})")); + R"({"category":"event","type":"gallery_patch","target":"constellation","patch":{"type":"Psk16","phase_offset_radians":0.75}})")); ASSERT_TRUE(patched.has_value()); const auto patched_json = parse_json(patched->payload); EXPECT_EQ(patched_json.at("controls").at("resources").at(3).at("data").at("type"), - "PSK16"); + "Psk16"); ASSERT_TRUE(constellation.handle(Frame_Request{}).has_value()); Gallery_Plot_Session waterfall;