This commit is contained in:
2026-08-12 08:09:16 +08:00
parent b51833499f
commit b34c0d31b1
8 changed files with 422 additions and 427 deletions
+3 -2
View File
@@ -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")
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <magic_enum/magic_enum.hpp>
#include <algorithm>
#include <cctype>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
namespace renderive::web {
template <class Enum>
requires std::is_enum_v<Enum>
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<char>(std::tolower(character));
});
return result;
}
template <class Enum>
requires std::is_enum_v<Enum>
std::optional<Enum> gallery_enum_cast(std::string_view identifier) {
for (const Enum value : magic_enum::enum_values<Enum>()) {
if (gallery_enum_id(value) == identifier)
return value;
}
return std::nullopt;
}
} // namespace renderive::web
+110
View File
@@ -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 <cstdint>
#include <stdexcept>
#include <string>
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 <class Json>
struct Value_Adapter<renderive::Frame_Control_Mode, Json> {
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<renderive::Frame_Control_Mode>(value);
if (!parsed)
throw std::invalid_argument("unknown frame control mode: " + value);
target = *parsed;
}
};
template <>
struct Type_Descriptor<renderive::Frame_Observer_Snapshot> {
static auto get() {
using T = renderive::Frame_Observer_Snapshot;
return object<T>("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<renderive::web::Gallery_Consumer_Feedback_Snapshot> {
static auto get() {
using T = renderive::web::Gallery_Consumer_Feedback_Snapshot;
return object<T>("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<renderive::Frame_Observer_Snapshot>);
static_assert(Described_Type<renderive::web::Gallery_Consumer_Feedback_Snapshot>);
} // namespace adminive
+29 -128
View File
@@ -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<double> action_number(const Gallery_Action_Request& request) {
return std::get<double>(*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<Frame_Control_Mode>(
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<Gallery_Session_Control>(
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<nlohmann::json>(observer);
nlohmann::json consumer_feedback_data =
adminive::to_frontend_json<nlohmann::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<double>(observer.paint_duration_ns) / 1e6},
{"kernel_render_ms", static_cast<double>(observer.render_duration_ns) / 1e6},
{"kernel_bottleneck_ms", static_cast<double>(observer.bottleneck_duration_ns) / 1e6},
{"kernel_end_to_end_ms", static_cast<double>(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<double>(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<double>(scheduler_interval_ns)},
+162 -105
View File
@@ -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<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<Gallery_Frame_Mode> 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 <class Object>
const Json& described_field(std::string_view name) {
static const Json descriptor = adminive::to_descriptor_json<Json, Object>();
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<std::string>() == name;
});
if (found == fields.end())
throw std::logic_error("missing Adminive dashboard field: " + std::string(name));
return *found;
}
template <class Object>
std::string described_source(std::string_view prefix, std::string_view name) {
static_cast<void>(described_field<Object>(name));
return std::string(prefix) + "." + std::string(name);
}
template <class Object>
Json described_dashboard_field(std::string_view prefix, std::string_view name) {
const Json& field = described_field<Object>(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<std::string>(),
described_source<Object>(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 <class Predicate>
Json observer_fields(Predicate&& predicate) {
const Json descriptor =
adminive::to_descriptor_json<Json, renderive::Frame_Observer_Snapshot>();
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<renderive::Frame_Observer_Snapshot>(
"kernel_observer", name);
if (name == "frequency_hz") {
field["enabled_source"] =
described_source<renderive::Frame_Observer_Snapshot>(
"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, Gallery_Consumer_Feedback_Snapshot>();
Json result = Json::array();
for (const Json& field : descriptor.at("fields")) {
const std::string name = field.at("name");
result.push_back(described_dashboard_field<Gallery_Consumer_Feedback_Snapshot>(
"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<renderive::Frame_Observer_Snapshot>(
"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<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "target_interval_ns")},
{"paint_limited", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "paint_duration_ns")},
{"render_limited", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "render_duration_ns")},
{"consumer_limited", described_source<renderive::Frame_Observer_Snapshot>(
"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<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "pending_frame_count"),
described_dashboard_field<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "dropped_frame_count"),
std::move(point_pair), std::move(bottleneck),
dashboard_field("观察事件", "kernel_observer.last_event", "text"),
described_dashboard_field<renderive::Frame_Observer_Snapshot>(
"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<renderive::Frame_Observer_Snapshot>(
"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<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "target_interval_ns")},
{"enabled_source", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "frequency_limit_enabled")}},
{{"label", "Kernel PaintEvent"}, {"active_value", "paint_limited"},
{"duration_source", "kernel_observer.paint_duration_ns"}},
{"duration_source", described_source<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "paint_duration_ns")}},
{{"label", "Kernel 后台渲染"}, {"active_value", "render_limited"},
{"duration_source", "kernel_observer.render_duration_ns"}},
{"duration_source", described_source<renderive::Frame_Observer_Snapshot>(
"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<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "consumer_interval_ns")},
{"enabled_source", described_source<renderive::Frame_Observer_Snapshot>(
"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<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "mode")},
{"limit", described_dashboard_field<renderive::Frame_Observer_Snapshot>(
"kernel_observer", "limit_state")},
{"event", described_dashboard_field<renderive::Frame_Observer_Snapshot>(
"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<Json, renderive::Frame_Observer_Snapshot>();
dashboard["observer"]["consumer_feedback_descriptor"] =
adminive::to_descriptor_json<Json, Gallery_Consumer_Feedback_Snapshot>();
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<Json, gallery_detail::Case_Model>();
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<Gallery_Frame_Mode>();
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_Session_Control>() +
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<Image_Interpolation_Mode>()}};
return result.dump();
}
@@ -493,7 +549,8 @@ std::optional<Gallery_Open_Request> 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<std::string>());
const auto mode = gallery_enum_cast<Gallery_Frame_Mode>(
request.at("frame_mode").get<std::string>());
return mode ? std::optional<Gallery_Open_Request>(Gallery_Open_Request{value, *mode})
: std::nullopt;
} catch (const std::exception&) {
+11 -125
View File
@@ -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 <algorithm>
@@ -11,7 +12,6 @@
#include <cctype>
#include <cmath>
#include <cstdio>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
@@ -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 <class Enum>
struct Enum_Metadata;
#define RENDERIVE_ENUM_METADATA(Type, ...) \
template <> struct Enum_Metadata<Type> { 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 <class Enum>
struct Actual_Enum_Adapter {
static std::vector<Enum> values() {
std::vector<Enum> result;
result.reserve(Enum_Metadata<Enum>::values.size());
for (const auto& [value, name] : Enum_Metadata<Enum>::values)
result.push_back(value);
return result;
}
static std::string_view name(Enum value) {
for (const auto& [candidate, name] : Enum_Metadata<Enum>::values) {
if (candidate == value)
return name;
}
return {};
}
static std::optional<Enum> cast(std::string_view name) {
for (const auto& [value, candidate] : Enum_Metadata<Enum>::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::uint8_t>(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<Color, 5> 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<Color_Map_Choice>()) {
if (value.colors() == color_map(choice).colors())
return choice;
}
return "Spectrum";
return Color_Map_Choice::Spectrum;
}
template <auto Member>
@@ -243,31 +161,6 @@ struct Value_Adapter<::Validated_Value<T, Validator>, Json>
static void write(Storage& target, T value) { target = std::move(value); }
};
#define RENDERIVE_ENUM_ADAPTER(Type) \
template <> struct Enum_Adapter<renderive::Type> \
: renderive::web::gallery_adminive::Actual_Enum_Adapter<renderive::Type> {}
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::Number_Locale_Choice>
: renderive::web::gallery_adminive::Actual_Enum_Adapter<
renderive::web::gallery_adminive::Number_Locale_Choice> {};
template <>
struct Enum_Adapter<renderive::web::gallery_adminive::Color_Map_Choice>
: renderive::web::gallery_adminive::Actual_Enum_Adapter<
renderive::web::gallery_adminive::Color_Map_Choice> {};
template <class Json>
struct Value_Adapter<renderive::Color, Json> {
using value_type = std::string;
@@ -328,17 +221,10 @@ struct Value_Adapter<renderive::Color_Map, Json> {
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);
}
};
+15 -39
View File
@@ -1,4 +1,5 @@
#include "Web_Event_Adapter.h"
#include "Gallery_Enum.h"
#include <algorithm>
#include <cmath>
@@ -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<Mouse_Button>(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<Key>(value))
return *direct;
if (value == "ArrowLeft")
return Key::Left;
if (value == "ArrowRight")
@@ -136,20 +123,14 @@ std::optional<Web_Transport_Events> Web_Transport_Event_Decoder::decode(
return Viewport_Resize{{static_cast<int>(std::clamp(*width, 240.0, 1920.0)),
static_cast<int>(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<Gallery_Request_Kind>(
std::string_view(type).substr(gallery_prefix.size()));
return kind ? std::optional<Web_Transport_Events>(
Gallery_Request{*kind, std::string(source)})
: std::nullopt;
}
std::optional<Render_2D_Web_Events> Render_2D_Web_Event_Decoder::decode(
@@ -183,15 +164,10 @@ std::optional<Web_Demo_Control_Events> 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<Demo_Mode>(
value, magic_enum::case_insensitive);
return mode ? std::optional<Web_Demo_Control_Events>(Set_Demo_Mode{*mode})
: std::nullopt;
}
if (control == "center_frequency_mhz") {
const auto value = finite_number(root, "value");
+58 -28
View File
@@ -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<std::string>()));
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<std::string>()));
EXPECT_EQ(observer.at("mode"), "manual");
EXPECT_FALSE(telemetry.contains("low_latency_limit"));
}
TEST(RenderiveWebGallery, ActionsComeFromStructiveTypeMetadata) {
using Spectrum_Schema = structive::type_descriptor_schema_t<Gallery_Spectrum>;
using Time_Axis_Schema = structive::type_descriptor_schema_t<Gallery_Time_Axis>;
@@ -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<std::uint64_t>(), 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<bool>());
EXPECT_FALSE(limit.at("frequency_limited").get<bool>());
EXPECT_TRUE(limit.at("paint_limited").get<bool>() || limit.at("render_limited").get<bool>());
EXPECT_EQ(limit.at("target_interval_ns"), 1);
EXPECT_EQ(limit.at("consumer_interval_ns"), 0);
EXPECT_GT(limit.at("bottleneck_duration_ns").get<std::uint64_t>(), 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<std::uint64_t>(), 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<std::uint64_t>(), 1U);
EXPECT_TRUE(low_observer.contains("paint_lease_wait_ns"));
EXPECT_TRUE(low_observer.contains("render_finish_state_wait_ns"));
constexpr std::array<std::string_view, 33> 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<bool>());
EXPECT_TRUE(observer.at("consumer_feedback_enabled").get<bool>());
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<bool>());
EXPECT_TRUE(observer.at("consumer_feedback_master_enabled").get<bool>());
EXPECT_TRUE(feedback.at("master_enabled").get<bool>());
EXPECT_EQ(observer.at("consumer_interval_ns"), 0);
}
@@ -1119,7 +1149,7 @@ TEST(RenderiveWebGallery, ProductionLowLatencySessionRendersWithoutPixelPulls) {
EXPECT_TRUE(performance.at("automatic_low_latency_scheduler").get<bool>());
EXPECT_GE(performance.at("successful_render_count").get<std::uint64_t>(), 4U);
EXPECT_GT(performance.at("measured_fps").get<double>(), 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;