788 lines
43 KiB
C++
788 lines
43 KiB
C++
#include "Gallery_Protocol.h"
|
||
#include "Gallery_Properties_Adminive.h"
|
||
|
||
#include "adminive/adminive.hpp"
|
||
#include "adminive/adapters/nlohmann_json.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <array>
|
||
#include <cctype>
|
||
#include <cmath>
|
||
#include <limits>
|
||
#include <stdexcept>
|
||
#include <tuple>
|
||
#include <type_traits>
|
||
#include <utility>
|
||
#include <vector>
|
||
|
||
namespace renderive::web::gallery_detail {
|
||
|
||
using Json = nlohmann::json;
|
||
|
||
struct Case_Model {
|
||
std::string id;
|
||
std::string title;
|
||
std::string component;
|
||
std::string category;
|
||
std::string description;
|
||
int order{};
|
||
int preferred_width = 560;
|
||
int preferred_height = 320;
|
||
};
|
||
|
||
struct Action_Model {
|
||
std::string id;
|
||
std::string label;
|
||
std::string api;
|
||
std::string description;
|
||
std::string group;
|
||
std::string argument_input;
|
||
std::string argument_label;
|
||
double argument_default{};
|
||
bool request_frame{};
|
||
};
|
||
|
||
struct Action_Definition {
|
||
Action_Model model;
|
||
};
|
||
|
||
const std::vector<Case_Model>& cases() {
|
||
static const std::vector<Case_Model> value{
|
||
{"axis_lab", "坐标轴实验室", "Axis / Frequency_Axis / Time_Axis", "基础控件",
|
||
"普通轴、频率轴和时间轴的布局、格式化、滚轮与拖拽 API。", 10},
|
||
{"spectrum", "实时频谱", "Spectrum", "三种频率图",
|
||
"频率控制模式 1:曲线、保持线、扫频区、峰值和自定义 Marker。", 20},
|
||
{"afterglow", "余辉频谱", "Afterglow", "三种频率图",
|
||
"频率控制模式 2:二维功率密度、衰减和功率 bin 插值。", 30},
|
||
{"sweep_spectrum", "扫频频谱", "Sweep_Spectrum", "三种频率图",
|
||
"频率控制模式 3:分块扫频、当前频率游标和六种线插值。", 40},
|
||
{"waterfall", "瀑布图", "Waterfall", "热力图控件",
|
||
"逐行时频热力图;Nearest、Bilinear、Bicubic 三种图像模式均可切换。", 50},
|
||
{"frequency_trace", "时间轨迹", "Frequency_Trace", "曲线控件",
|
||
"Time_Axis 驱动的连续轨迹,覆盖时间格式和属性化笔刷 API。", 60},
|
||
{"selection_overlay", "框选叠层", "Selection_Rectangle_Overlay", "交互控件",
|
||
"鼠标框选、多区域保留、标注样式和清空。", 70},
|
||
{"constellation", "星座图", "Constellation_Diagram", "点图控件",
|
||
"PSK4/PSK8/PSK16 模式、相位、寿命、坐标范围和方形拟合。", 80}
|
||
};
|
||
return value;
|
||
}
|
||
|
||
Action_Definition action(std::string id, std::string label, std::string api,
|
||
std::string group, std::string description = {},
|
||
std::string argument_input = {}, std::string argument_label = {},
|
||
double argument_default = 0) {
|
||
return {{std::move(id), std::move(label), std::move(api), std::move(description),
|
||
std::move(group), std::move(argument_input), std::move(argument_label),
|
||
argument_default}};
|
||
}
|
||
|
||
Action_Definition frame_action(std::string id, std::string label, std::string api,
|
||
std::string group) {
|
||
auto result = action(std::move(id), std::move(label), std::move(api), std::move(group));
|
||
result.model.request_frame = true;
|
||
return result;
|
||
}
|
||
|
||
std::vector<Action_Definition> actions(std::string_view case_id,
|
||
Gallery_Frame_Mode frame_mode) {
|
||
std::vector<Action_Definition> result{
|
||
action("reset", "恢复本图默认值", "Gallery_Scene::rebuild", "会话"),
|
||
action("rebuild_renderable", "移除并重建控件", "Plot_Core::remove_renderable / Builder::build", "会话"),
|
||
action("toggle_view", "切换 View 生命周期", "Plot_Core::activate_view / deactivate_view", "会话"),
|
||
action("axis_set_start", "写入坐标范围起点", "Axis::set<&Axis_Properties::coordinates>", "Axis"),
|
||
action("axis_set_length", "写入坐标范围跨度", "Axis::set<&Axis_Properties::coordinates>", "Axis"),
|
||
action("axis_probe", "读取坐标映射与刻度", "coord_to_pixel / pixel_to_coord / tick_step / sub_tick_count / tick_label", "Axis")
|
||
};
|
||
if (frame_mode == Gallery_Frame_Mode::Manual) {
|
||
result.push_back(action("mode_prepare", "准备帧", "Manual_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame",
|
||
"Manual_Refresh_Strategy"));
|
||
result.push_back(action("mode_refresh", "提交手动刷新", "Manual_Refresh_Strategy::refresh / Plot_Core::refresh_manual_frame",
|
||
"Manual_Refresh_Strategy"));
|
||
result.push_back(frame_action("mode_render", "渲染已刷新帧", "Manual_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame",
|
||
"Manual_Refresh_Strategy"));
|
||
result.push_back(action("mode_discard", "丢弃待刷新帧", "Plot_Core::discard_pending_frame",
|
||
"Manual_Refresh_Strategy"));
|
||
result.push_back(frame_action("mode_cycle", "执行完整手动帧周期", "Plot_Core::render_frame",
|
||
"Manual_Refresh_Strategy"));
|
||
} else if (frame_mode == Gallery_Frame_Mode::Low_Latency) {
|
||
result.push_back(action("mode_prepare", "发布低延迟帧", "Low_Latency_Strategy::acquire_painter / Plot_Core::prepare_frame",
|
||
"Low_Latency_Strategy"));
|
||
result.push_back(frame_action("mode_render", "消费最新帧", "Low_Latency_Strategy::acquire_renderer / Plot_Core::render_prepared_frame",
|
||
"Low_Latency_Strategy"));
|
||
result.push_back(action("mode_discard", "丢弃陈旧帧", "Plot_Core::discard_pending_frame",
|
||
"Low_Latency_Strategy"));
|
||
result.push_back(frame_action("mode_cycle", "执行低延迟帧周期", "Plot_Core::render_frame",
|
||
"Low_Latency_Strategy"));
|
||
} else {
|
||
result.push_back(action("mode_enqueue", "压入一帧", "Flow_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame",
|
||
"Flow_Refresh_Strategy"));
|
||
result.push_back(action("mode_enqueue_burst", "批量压入回放队列", "Flow_Refresh_Strategy::acquire_painter / Plot_Core::prepare_frame × N",
|
||
"Flow_Refresh_Strategy", {}, "number", "帧数", 8));
|
||
result.push_back(frame_action("mode_dequeue", "消费队首帧", "Flow_Refresh_Strategy::acquire_renderer / Plot_Core::render_prepared_frame",
|
||
"Flow_Refresh_Strategy"));
|
||
result.push_back(frame_action("mode_cycle", "执行回放帧周期", "Plot_Core::render_frame",
|
||
"Flow_Refresh_Strategy"));
|
||
}
|
||
if (case_id == "axis_lab") {
|
||
result.push_back(action("append_time", "追加时间点", "Time_Axis::append_time / tick_to_time", "Time_Axis"));
|
||
result.push_back(action("read_data_shape", "读取轴采样点状态", "Abs_Axis::transform / Time_Axis::time_point_count", "观察者"));
|
||
} else if (case_id == "spectrum" || case_id == "selection_overlay") {
|
||
result.push_back(action("push_samples", "推送一帧样本", "Spectrum::update_samples", "Spectrum"));
|
||
result.push_back(action("power_at", "查询指定频率功率", "Spectrum::power_at", "Spectrum", {}, "number", "频率", 98e6));
|
||
result.push_back(action("add_marker", "添加点 Marker", "Spectrum::add_custom_marker", "Marker", {}, "number", "频率", 96e6));
|
||
result.push_back(action("add_line_marker", "添加线 Marker", "Spectrum::add_custom_line_marker", "Marker", {}, "number", "频率", 100e6));
|
||
result.push_back(action("remove_marker", "按频率删除 Marker", "Spectrum::remove_custom_marker", "Marker", {}, "number", "频率", 96e6));
|
||
result.push_back(action("remove_selected_marker", "删除选中 Marker", "Spectrum::remove_selected_marker", "Marker"));
|
||
result.push_back(action("clear_markers", "清空 Marker", "Spectrum::clear_custom_markers", "Marker"));
|
||
result.push_back(action("select_marker", "选择 Marker 索引", "Spectrum::set_selected_marker_index", "Marker", {}, "number", "索引", 0));
|
||
result.push_back(action("select_next_marker", "选择下一个 Marker", "Spectrum::select_next_marker", "Marker"));
|
||
result.push_back(action("select_previous_marker", "选择上一个 Marker", "Spectrum::select_previous_marker", "Marker"));
|
||
result.push_back(action("clear_marker_selection", "清除 Marker 选择", "Spectrum::clear_marker_selection", "Marker"));
|
||
result.push_back(action("set_marker_frequency", "修改选中 Marker 频率",
|
||
"Spectrum::set_marker_frequency / set_current_marker_frequency",
|
||
"Marker", {}, "number", "频率", 99e6));
|
||
result.push_back(action("read_data_shape", "读取输入与绘制点数", "Spectrum::sample_count / rendered_point_count", "观察者"));
|
||
if (case_id == "selection_overlay") {
|
||
result.push_back(action("clear_selection", "清空框选区域", "Selection_Rectangle_Overlay::clear_selected_regions", "Selection Overlay"));
|
||
}
|
||
} else if (case_id == "waterfall") {
|
||
result.push_back(action("append_row", "追加一行", "Waterfall::append_row", "Waterfall"));
|
||
result.push_back(action("append_tick_row", "按 tick 追加一行", "Time_Axis::append_time / Waterfall::append_row(int, span)", "Waterfall"));
|
||
result.push_back(action("read_data_shape", "读取行、点与渲染单元", "Waterfall::row_count / stored_point_count / rendered_cell_count", "观察者"));
|
||
} else if (case_id == "afterglow") {
|
||
result.push_back(action("append_spectrum", "追加一帧频谱", "Afterglow::append_spectrum", "Afterglow"));
|
||
result.push_back(action("read_data_shape", "读取历史帧与渲染网格", "Afterglow::history_count / latest_spectrum_point_count / rendered_cell_count", "观察者"));
|
||
} else if (case_id == "sweep_spectrum") {
|
||
result.push_back(action("append_block", "追加一个扫频块", "Sweep_Spectrum::append_block", "Sweep Spectrum"));
|
||
result.push_back(action("read_data_shape", "读取块与绘制点数", "Sweep_Spectrum::stored_block_count / stored_point_count / rendered_point_count", "观察者"));
|
||
} else if (case_id == "frequency_trace") {
|
||
result.push_back(action("append_sample", "追加一个样本", "Frequency_Trace::append_sample", "Frequency Trace"));
|
||
result.push_back(action("append_tick_sample", "按 tick 追加样本", "Time_Axis::append_time / Frequency_Trace::append_sample(int, double)", "Frequency Trace"));
|
||
result.push_back(action("read_data_shape", "读取轨迹点数", "Frequency_Trace::sample_count / rendered_point_count / Time_Axis::time_point_count", "观察者"));
|
||
} else if (case_id == "constellation") {
|
||
result.push_back(action("append_points", "追加一组 IQ 点", "Constellation_Diagram::append_point", "Constellation"));
|
||
result.push_back(action("fit_square", "按轴拟合正方形", "Constellation_Diagram::fit_square_to_axes", "Constellation"));
|
||
result.push_back(action("read_data_shape", "读取星座点与锚点数", "Constellation_Diagram::point_count", "观察者"));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
const Case_Model* find_case(std::string_view id) {
|
||
const auto& all = cases();
|
||
const auto found = std::find_if(all.begin(), all.end(),
|
||
[id](const Case_Model& item) { return item.id == id; });
|
||
return found == all.end() ? nullptr : &*found;
|
||
}
|
||
|
||
Json parse_request(std::string_view message) {
|
||
return Json::parse(message.begin(), message.end());
|
||
}
|
||
template <class Properties>
|
||
Gallery_Property_Model make_property_model(Gallery_Frame_Mode frame_mode) {
|
||
if(frame_mode == Gallery_Frame_Mode::Low_Latency) {
|
||
using T = Gallery_Low_Latency_Case_Properties<Properties>;
|
||
return Gallery_Property_Model{std::in_place_type<T>};
|
||
}
|
||
return Gallery_Property_Model{std::in_place_type<Properties>};
|
||
}
|
||
Gallery_Property_Model make_property_model(std::string_view case_id, Gallery_Frame_Mode frame_mode) {
|
||
if(case_id == "spectrum")
|
||
return make_property_model<Gallery_Spectrum_Properties>(frame_mode);
|
||
if(case_id == "afterglow")
|
||
return make_property_model<Gallery_Afterglow_Properties>(frame_mode);
|
||
if(case_id == "sweep_spectrum")
|
||
return make_property_model<Gallery_Sweep_Spectrum_Properties>(frame_mode);
|
||
if(case_id == "waterfall")
|
||
return make_property_model<Gallery_Waterfall_Properties>(frame_mode);
|
||
if(case_id == "frequency_trace")
|
||
return make_property_model<Gallery_Frequency_Trace_Properties>(frame_mode);
|
||
if(case_id == "selection_overlay")
|
||
return make_property_model<Gallery_Selection_Overlay_Properties>(frame_mode);
|
||
if(case_id == "constellation")
|
||
return make_property_model<Gallery_Constellation_Properties>(frame_mode);
|
||
return make_property_model<Gallery_Axis_Lab_Properties>(frame_mode);
|
||
}
|
||
Gallery_Value gallery_value(const Json& value) {
|
||
if(value.is_boolean())
|
||
return value.get<bool>();
|
||
if(value.is_number())
|
||
return value.get<double>();
|
||
return value.get<std::string>();
|
||
}
|
||
template <class T>
|
||
std::optional<Gallery_Value> property_value(const T& value, std::string_view id) {
|
||
std::optional<Gallery_Value> result;
|
||
const auto descriptor = adminive::describe<T>();
|
||
std::apply([&](const auto&... field) {
|
||
([&] {
|
||
if(!result && field.name() == id)
|
||
result = gallery_value(adminive::encode_json_value<Json>(field.get(value)));
|
||
}(), ...);
|
||
}, descriptor.fields());
|
||
return result;
|
||
}
|
||
template <class T>
|
||
std::size_t property_count(const T&) {
|
||
return std::tuple_size_v<adminive::Descriptor_Fields<T>>;
|
||
}
|
||
template <class T>
|
||
Json property_resource(const T& value) {
|
||
return {
|
||
{"descriptor", adminive::to_descriptor_json<Json, T>()},
|
||
{"view", adminive::to_view_json<Json, T>(adminive::describe_edit_view<T>().titled("控件属性").submit("应用"))},
|
||
{"data", adminive::to_frontend_json<Json>(value)}
|
||
};
|
||
}
|
||
template <class T>
|
||
adminive::Update_Result apply_property_patch(T& value, const Json& patch) {
|
||
return adminive::apply_frontend_patch<Json>(value, patch);
|
||
}
|
||
|
||
adminive::Table_View action_view();
|
||
|
||
} // namespace renderive::web::gallery_detail
|
||
|
||
namespace adminive {
|
||
|
||
template <>
|
||
struct Type_Descriptor<renderive::web::gallery_detail::Case_Model> {
|
||
static auto get() {
|
||
using T = renderive::web::gallery_detail::Case_Model;
|
||
return object<T>("renderive_gallery_case",
|
||
ADMINIVE_FIELD_LABEL(T, id, "标识"),
|
||
ADMINIVE_FIELD_LABEL(T, title, "标题"),
|
||
ADMINIVE_FIELD_LABEL(T, component, "Core2 控件"),
|
||
ADMINIVE_FIELD_LABEL(T, category, "分类"),
|
||
ADMINIVE_FIELD_LABEL(T, description, "说明"),
|
||
ADMINIVE_FIELD_LABEL(T, order, "顺序"),
|
||
ADMINIVE_FIELD_LABEL(T, preferred_width, "建议宽度"),
|
||
ADMINIVE_FIELD_LABEL(T, preferred_height, "建议高度"))
|
||
.label("Renderive 控件用例");
|
||
}
|
||
};
|
||
|
||
template <>
|
||
struct Type_Descriptor<renderive::web::gallery_detail::Action_Model> {
|
||
static auto get() {
|
||
using T = renderive::web::gallery_detail::Action_Model;
|
||
return object<T>("renderive_gallery_action",
|
||
ADMINIVE_FIELD_LABEL(T, id, "动作"),
|
||
ADMINIVE_FIELD_LABEL(T, label, "名称"),
|
||
ADMINIVE_FIELD_LABEL(T, api, "Core2 API"),
|
||
ADMINIVE_FIELD_LABEL(T, description, "说明"),
|
||
ADMINIVE_FIELD_LABEL(T, group, "分组"),
|
||
ADMINIVE_FIELD_LABEL(T, argument_input, "参数类型"),
|
||
ADMINIVE_FIELD_LABEL(T, argument_label, "参数名称"),
|
||
ADMINIVE_FIELD_LABEL(T, argument_default, "参数默认值"),
|
||
ADMINIVE_FIELD_LABEL(T, request_frame, "执行后请求像素帧"))
|
||
.label("后端动作菜单");
|
||
}
|
||
};
|
||
|
||
} // namespace adminive
|
||
|
||
namespace renderive::web::gallery_detail {
|
||
|
||
adminive::Table_View action_view() {
|
||
using T = Action_Model;
|
||
return adminive::table_view<T>(
|
||
adminive::column<&T::label>("动作"),
|
||
adminive::column<&T::group>("分组"),
|
||
adminive::column<&T::api>("Core2 API"))
|
||
.titled("保留 API 动作");
|
||
}
|
||
|
||
} // namespace renderive::web::gallery_detail
|
||
|
||
namespace renderive::web {
|
||
namespace {
|
||
|
||
using gallery_detail::Json;
|
||
|
||
Json protocol_base() {
|
||
return {{"category", "gallery"},
|
||
{"protocol", "renderive.control-gallery"},
|
||
{"protocol_version", 4}};
|
||
}
|
||
|
||
Json case_contract(std::string_view case_id) {
|
||
const auto* item = gallery_detail::find_case(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}};
|
||
if (digits >= 0)
|
||
result["digits"] = digits;
|
||
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;
|
||
return result;
|
||
}
|
||
|
||
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");
|
||
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"}
|
||
};
|
||
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", "关闭"}}},
|
||
{"consumer_feedback_source", {
|
||
{"disabled", "总开关关闭"}, {"none", "无"}, {"pixel", "像素响应"},
|
||
{"presentation", "浏览器呈现"}, {"manual", "手动"}
|
||
}},
|
||
{"limit_state", {
|
||
{"frequency_limited", "Kernel 频率受限"}, {"paint_limited", "PaintEvent 受限"},
|
||
{"render_limited", "后台渲染受限"}, {"consumer_limited", "消费者反馈受限"},
|
||
{"unlimited", "无限制"}, {"not_applicable", "N/A"}
|
||
}}
|
||
}},
|
||
{"performance", {
|
||
{"fields", Json::array({
|
||
dashboard_field("后端渲染 FPS", "performance.measured_fps", "fixed", 1),
|
||
dashboard_field("像素响应 FPS", "performance.pixel_response_fps", "fixed", 1),
|
||
dashboard_field("浏览器呈现 FPS", "client_performance.presentation_fps", "fixed", 1),
|
||
dashboard_field("WS 往返 ms", "client_performance.frame_round_trip_ms", "fixed", 2),
|
||
dashboard_field("未呈现覆盖", "client_performance.overwritten_pixel_frames"),
|
||
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"),
|
||
std::move(point_pair), std::move(bottleneck),
|
||
dashboard_field("观察事件", "kernel_observer.last_event", "text"),
|
||
dashboard_field("像素超时", "client_performance.frame_request_timeout_count"),
|
||
dashboard_field("最近像素龄", "client_performance.last_pixel_receive_age_ms", "milliseconds", 0)
|
||
})}
|
||
}},
|
||
{"menu_views", {
|
||
{"observer", {{"source", "kernel_observer"}}},
|
||
{"performance", Json::object()}
|
||
}},
|
||
{"limits", {
|
||
{"aria_label", "低延迟限速来源"}, {"title", "限速来源"},
|
||
{"current_source", "low_latency_limit.current"},
|
||
{"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"}},
|
||
{{"label", "Kernel PaintEvent"}, {"active_value", "paint_limited"},
|
||
{"duration_source", "kernel_observer.paint_duration_ns"}},
|
||
{{"label", "Kernel 后台渲染"}, {"active_value", "render_limited"},
|
||
{"duration_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"}}
|
||
})}
|
||
}},
|
||
{"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")}
|
||
}},
|
||
{"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", "client-summary"}, {"aria_label", "浏览器消费者反馈全量统计"},
|
||
{"fields", Json::array({
|
||
dashboard_field("RAF 最新周期", "client_performance.display_interval_latest_ms", "milliseconds", 3),
|
||
dashboard_field("RAF 中位周期", "client_performance.display_interval_ms", "milliseconds", 3),
|
||
dashboard_field("RAF P95 周期", "client_performance.display_interval_p95_ms", "milliseconds", 3),
|
||
dashboard_field("RAF P95-P50 抖动", "client_performance.display_jitter_ms", "milliseconds", 3),
|
||
dashboard_field("WS 往返", "client_performance.frame_round_trip_ms", "milliseconds", 3),
|
||
dashboard_field("像素响应 FPS", "client_performance.transport_fps", "fps", 2),
|
||
dashboard_field("浏览器呈现 FPS", "client_performance.presentation_fps", "fps", 2),
|
||
dashboard_field("WS 缓冲", "client_performance.websocket_buffered_bytes", "bytes"),
|
||
dashboard_field("未呈现覆盖", "client_performance.overwritten_pixel_frames"),
|
||
dashboard_field("变化像素帧", "client_performance.changed_pixel_frames"),
|
||
dashboard_field("重复像素帧", "client_performance.duplicate_pixel_frames"),
|
||
dashboard_field("像素请求超时", "client_performance.frame_request_timeout_count"),
|
||
dashboard_field("最近像素龄", "client_performance.last_pixel_receive_age_ms", "milliseconds", 3),
|
||
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")
|
||
})}}
|
||
})}
|
||
}}
|
||
};
|
||
return dashboard;
|
||
}
|
||
|
||
Json frame_mode_contract(Gallery_Frame_Mode mode) {
|
||
switch (mode) {
|
||
case Gallery_Frame_Mode::Manual:
|
||
return {{"id", "manual"}, {"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", "低延迟"},
|
||
{"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", "回放队列"},
|
||
{"strategy", "Flow_Refresh_Strategy"},
|
||
{"description", "所有帧按队列顺序入队和消费,可观测队深与排队时间。"},
|
||
{"automatic", true}, {"request_after_response", true},
|
||
{"request_on_animation_frame", true}, {"observer_visible", false},
|
||
{"frame_button_label", "消费下一帧"}, {"accent", "#6aa9ffaa"}, {"order", 30}};
|
||
}
|
||
return Json::object();
|
||
}
|
||
|
||
} // namespace
|
||
|
||
Gallery_Value gallery_property_value(const Gallery_State& state, std::string_view id) {
|
||
const auto result = std::visit([id](const auto& properties) {
|
||
return gallery_detail::property_value(properties, id);
|
||
}, state.properties);
|
||
if(!result)
|
||
throw std::out_of_range("gallery property does not exist: " + std::string(id));
|
||
return *result;
|
||
}
|
||
|
||
bool gallery_has_property(const Gallery_State& state, std::string_view id) {
|
||
return std::visit([id](const auto& properties) {
|
||
return gallery_detail::property_value(properties, id).has_value();
|
||
}, state.properties);
|
||
}
|
||
|
||
std::size_t gallery_property_count(const Gallery_State& state) {
|
||
return std::visit([](const auto& properties) {
|
||
return gallery_detail::property_count(properties);
|
||
}, state.properties);
|
||
}
|
||
|
||
std::string Gallery_Protocol::catalog_json() {
|
||
Json result = protocol_base();
|
||
result["type"] = "catalog";
|
||
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", "全部"},
|
||
{"catalog_loaded_text", "Kernel 策略目录已加载"},
|
||
{"hero_eyebrow", "REAL KERNEL STRATEGIES"},
|
||
{"hero_title", "对称策略页面,同一批控件,直接比较"}
|
||
};
|
||
result["dashboard"] = dashboard_contract();
|
||
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};
|
||
for (const auto mode : frame_modes)
|
||
result["frame_modes"].push_back(frame_mode_contract(mode));
|
||
result["cases"] = Json::array();
|
||
std::size_t controls_total{};
|
||
std::size_t actions_total{};
|
||
for (const auto& item : gallery_detail::cases()) {
|
||
Json entry = adminive::to_frontend_json<Json>(item);
|
||
entry["control_count_by_mode"] = Json::object();
|
||
entry["action_count_by_mode"] = Json::object();
|
||
for (const auto mode : frame_modes) {
|
||
const std::string key(frame_mode_id(mode));
|
||
const std::size_t mode_controls = control_count(item.id, mode);
|
||
const std::size_t mode_actions = action_count(item.id, mode);
|
||
entry["control_count_by_mode"][key] = mode_controls;
|
||
entry["action_count_by_mode"][key] = mode_actions;
|
||
controls_total += mode_controls;
|
||
actions_total += mode_actions;
|
||
}
|
||
entry["control_count"] = control_count(item.id, Gallery_Frame_Mode::Low_Latency);
|
||
entry["action_count"] = action_count(item.id, Gallery_Frame_Mode::Low_Latency);
|
||
result["cases"].push_back(std::move(entry));
|
||
}
|
||
result["coverage"] = {{"case_count", gallery_detail::cases().size()},
|
||
{"page_count", std::size(frame_modes)},
|
||
{"canvas_count", gallery_detail::cases().size() * std::size(frame_modes)},
|
||
{"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"})}};
|
||
return result.dump();
|
||
}
|
||
|
||
bool Gallery_Protocol::is_case(std::string_view case_id) {
|
||
return gallery_detail::find_case(case_id) != nullptr;
|
||
}
|
||
|
||
Gallery_State Gallery_Protocol::default_state(std::string_view case_id,
|
||
Gallery_Frame_Mode frame_mode) {
|
||
return {gallery_detail::make_property_model(case_id, frame_mode)};
|
||
}
|
||
|
||
Gallery_State Gallery_Protocol::default_state(std::string_view case_id) {
|
||
return default_state(case_id, Gallery_Frame_Mode::Low_Latency);
|
||
}
|
||
|
||
std::string Gallery_Protocol::case_json(std::string_view case_id,
|
||
const Gallery_State& state,
|
||
std::string_view telemetry_json,
|
||
std::string_view notice,
|
||
Gallery_Frame_Mode frame_mode) {
|
||
Json result = protocol_base();
|
||
result["type"] = "case_state";
|
||
result["case"] = case_contract(case_id);
|
||
result["frame_mode"] = frame_mode_contract(frame_mode);
|
||
result["controls"] = std::visit([](const auto& properties) {
|
||
return gallery_detail::property_resource(properties);
|
||
}, state.properties);
|
||
result["actions"] = {
|
||
{"descriptor", adminive::to_descriptor_json<Json, gallery_detail::Action_Model>()},
|
||
{"view", adminive::to_view_json<Json, gallery_detail::Action_Model>(
|
||
gallery_detail::action_view())},
|
||
{"data", Json::array()}
|
||
};
|
||
for (const auto& definition : gallery_detail::actions(case_id, frame_mode))
|
||
result["actions"]["data"].push_back(
|
||
adminive::to_frontend_json<Json>(definition.model));
|
||
try {
|
||
result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end());
|
||
} catch (const std::exception&) {
|
||
result["telemetry"] = Json::object();
|
||
}
|
||
if (!notice.empty())
|
||
result["notice"] = notice;
|
||
return result.dump();
|
||
}
|
||
|
||
std::string Gallery_Protocol::case_json(std::string_view case_id,
|
||
const Gallery_State& state,
|
||
std::string_view telemetry_json,
|
||
std::string_view notice) {
|
||
return case_json(case_id, state, telemetry_json, notice,
|
||
Gallery_Frame_Mode::Low_Latency);
|
||
}
|
||
|
||
std::string Gallery_Protocol::error_json(std::string_view message,
|
||
std::string_view field) {
|
||
Json result = protocol_base();
|
||
result["type"] = "error";
|
||
result["message"] = message;
|
||
result["field_errors"] = Json::object();
|
||
if (!field.empty())
|
||
result["field_errors"][std::string(field)] = message;
|
||
return result.dump();
|
||
}
|
||
|
||
std::string Gallery_Protocol::observer_json(std::string_view case_id,
|
||
Gallery_Frame_Mode frame_mode,
|
||
std::string_view telemetry_json) {
|
||
Json result = protocol_base();
|
||
result["type"] = "observer_state";
|
||
result["case_id"] = case_id;
|
||
result["frame_mode"] = frame_mode_contract(frame_mode);
|
||
try {
|
||
result["telemetry"] = Json::parse(telemetry_json.begin(), telemetry_json.end());
|
||
} catch (const std::exception&) {
|
||
result["telemetry"] = Json::object();
|
||
}
|
||
return result.dump();
|
||
}
|
||
|
||
Gallery_Patch_Result Gallery_Protocol::apply_patch(std::string_view case_id,
|
||
const Gallery_State& current,
|
||
std::string_view message,
|
||
Gallery_Frame_Mode frame_mode) {
|
||
try {
|
||
const Json request = gallery_detail::parse_request(message);
|
||
if (!request.is_object() || request.value("category", "") != "event" ||
|
||
request.value("type", "") != "gallery_patch" ||
|
||
!request.contains("patch") || !request.at("patch").is_object()) {
|
||
return {std::nullopt, error_json("gallery_patch 格式无效")};
|
||
}
|
||
Gallery_State candidate = current;
|
||
const auto update = std::visit([&request](auto& properties) {
|
||
return gallery_detail::apply_property_patch(properties, request.at("patch"));
|
||
}, candidate.properties);
|
||
if(!update.success) {
|
||
Json response = protocol_base();
|
||
response["type"] = "error";
|
||
response["message"] = update.message;
|
||
response["field_errors"] = update.field_errors;
|
||
return {std::nullopt, response.dump()};
|
||
}
|
||
return {std::move(candidate), {}};
|
||
} catch (const std::exception& error) {
|
||
return {std::nullopt, error_json(error.what())};
|
||
}
|
||
}
|
||
|
||
Gallery_Patch_Result Gallery_Protocol::apply_patch(std::string_view case_id,
|
||
const Gallery_State& current,
|
||
std::string_view message) {
|
||
return apply_patch(case_id, current, message, Gallery_Frame_Mode::Low_Latency);
|
||
}
|
||
|
||
std::optional<Gallery_Open_Request> Gallery_Protocol::open_request(std::string_view message) {
|
||
try {
|
||
const Json request = gallery_detail::parse_request(message);
|
||
if (!request.is_object() || request.value("category", "") != "event" ||
|
||
request.value("type", "") != "gallery_open" ||
|
||
!request.contains("case") || !request.at("case").is_string())
|
||
return std::nullopt;
|
||
const std::string value = request.at("case").get<std::string>();
|
||
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>());
|
||
return mode ? std::optional<Gallery_Open_Request>(Gallery_Open_Request{value, *mode})
|
||
: std::nullopt;
|
||
} catch (const std::exception&) {
|
||
return std::nullopt;
|
||
}
|
||
}
|
||
|
||
std::optional<Gallery_Action_Request> Gallery_Protocol::action_request(
|
||
std::string_view message) {
|
||
try {
|
||
const Json request = gallery_detail::parse_request(message);
|
||
if (!request.is_object() || request.value("category", "") != "event" ||
|
||
request.value("type", "") != "gallery_action" ||
|
||
!request.contains("action") || !request.at("action").is_string())
|
||
return std::nullopt;
|
||
Gallery_Action_Request result{request.at("action").get<std::string>()};
|
||
if (request.contains("argument")) {
|
||
const Json& argument = request.at("argument");
|
||
if (argument.is_boolean())
|
||
result.argument = argument.get<bool>();
|
||
else if (argument.is_number()) {
|
||
const double value = argument.get<double>();
|
||
if (!std::isfinite(value))
|
||
return std::nullopt;
|
||
result.argument = value;
|
||
} else if (argument.is_string())
|
||
result.argument = argument.get<std::string>();
|
||
else
|
||
return std::nullopt;
|
||
}
|
||
return result;
|
||
} catch (const std::exception&) {
|
||
return std::nullopt;
|
||
}
|
||
}
|
||
|
||
std::size_t Gallery_Protocol::control_count(std::string_view case_id,
|
||
Gallery_Frame_Mode frame_mode) {
|
||
return gallery_property_count(default_state(case_id, frame_mode));
|
||
}
|
||
|
||
std::size_t Gallery_Protocol::control_count(std::string_view case_id) {
|
||
return control_count(case_id, Gallery_Frame_Mode::Low_Latency);
|
||
}
|
||
|
||
std::size_t Gallery_Protocol::action_count(std::string_view case_id,
|
||
Gallery_Frame_Mode frame_mode) {
|
||
return gallery_detail::actions(case_id, frame_mode).size();
|
||
}
|
||
|
||
std::size_t Gallery_Protocol::action_count(std::string_view case_id) {
|
||
return action_count(case_id, Gallery_Frame_Mode::Low_Latency);
|
||
}
|
||
|
||
bool Gallery_Protocol::action_available(std::string_view case_id,
|
||
Gallery_Frame_Mode frame_mode,
|
||
std::string_view action_id) {
|
||
const auto available = gallery_detail::actions(case_id, frame_mode);
|
||
return std::any_of(available.begin(), available.end(),
|
||
[action_id](const gallery_detail::Action_Definition& item) {
|
||
return item.model.id == action_id;
|
||
});
|
||
}
|
||
|
||
} // namespace renderive::web
|