732 lines
36 KiB
C++
732 lines
36 KiB
C++
#include "Gallery_Protocol.h"
|
|
#include "Gallery_Actions.h"
|
|
#include "Gallery_Enum.h"
|
|
#include "Gallery_Observer_Adminive.h"
|
|
#include "Gallery_Renderables.h"
|
|
#include "Gallery_Session_Control_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;
|
|
};
|
|
|
|
using Action_Model = Gallery_Action_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},
|
|
{"datoviz_3d", "Datoviz 3D", "Mesh / Texture / MSDF Text", "3D",
|
|
"Render3D 通过 Kernel 帧策略驱动迁移后的 Datoviz Vulkan 后端;3D 的已发布帧禁止自动覆盖。", 90}
|
|
};
|
|
return value;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void append_session_actions(Gallery_Frame_Mode frame_mode,
|
|
std::vector<Action_Model>& result) {
|
|
switch (frame_mode) {
|
|
case Gallery_Frame_Mode::Manual:
|
|
append_gallery_actions<Gallery_Session_Control<Manual_Scene2D>>(result);
|
|
return;
|
|
case Gallery_Frame_Mode::Low_Latency:
|
|
append_gallery_actions<Gallery_Session_Control<Scene2D>>(result);
|
|
return;
|
|
case Gallery_Frame_Mode::Playback:
|
|
append_gallery_actions<Gallery_Session_Control<Playback_Scene2D>>(result);
|
|
return;
|
|
}
|
|
throw std::invalid_argument("unknown gallery frame mode");
|
|
}
|
|
std::size_t session_control_count(std::string_view case_id, Gallery_Frame_Mode frame_mode) {
|
|
if (case_id == "datoviz_3d")
|
|
return 0;
|
|
switch (frame_mode) {
|
|
case Gallery_Frame_Mode::Manual:
|
|
return gallery_control_count<Gallery_Session_Control<Manual_Scene2D>>();
|
|
case Gallery_Frame_Mode::Low_Latency:
|
|
return gallery_control_count<Gallery_Session_Control<Scene2D>>();
|
|
case Gallery_Frame_Mode::Playback:
|
|
return gallery_control_count<Gallery_Session_Control<Playback_Scene2D>>();
|
|
}
|
|
throw std::invalid_argument("unknown gallery frame mode");
|
|
}
|
|
void append_3d_actions(Gallery_Frame_Mode frame_mode, std::vector<Action_Model>& result) {
|
|
const auto append = [&result](const Gallery_Action_Attribute& action) {
|
|
result.push_back(gallery_action_model(action));
|
|
};
|
|
switch (frame_mode) {
|
|
case Gallery_Frame_Mode::Manual:
|
|
append(gallery_action("mode_prepare", "准备 3D 帧", "Manual_Scene3D::prepare_frame", "3D 帧策略"));
|
|
append(gallery_action("mode_refresh", "提交手动刷新", "Manual_Scene3D::refresh", "3D 帧策略"));
|
|
append(gallery_action("mode_render", "渲染已刷新帧", "Manual_Scene3D::render_frame", "3D 帧策略", {}, {}, {}, 0.0, true));
|
|
break;
|
|
case Gallery_Frame_Mode::Low_Latency:
|
|
append(gallery_action("mode_prepare", "发布 3D 帧", "Scene3D::prepare_frame", "3D 帧策略"));
|
|
append(gallery_action("mode_render", "消费 3D 帧", "Scene3D::render_frame", "3D 帧策略", {}, {}, {}, 0.0, true));
|
|
break;
|
|
case Gallery_Frame_Mode::Playback:
|
|
append(gallery_action("mode_enqueue", "压入 3D 帧", "Playback_Scene3D::prepare_frame", "3D 帧策略"));
|
|
append(gallery_action("mode_dequeue", "消费队首 3D 帧", "Playback_Scene3D::render_frame", "3D 帧策略", {}, {}, {}, 0.0, true));
|
|
break;
|
|
}
|
|
append(gallery_action("move_mesh", "移动蓝色对象", "Mesh::place", "3D 场景"));
|
|
append(gallery_action("add_mesh", "添加黄色对象", "Scene_Edit::attach", "3D 场景"));
|
|
append(gallery_action("remove_mesh", "移除黄色对象", "Scene_Edit::detach", "3D 场景"));
|
|
append(gallery_action("mesh_churn", "Attach/Detach 验证", "Scene_Edit::attach/detach", "3D 场景"));
|
|
append(gallery_action("reset_camera", "重置相机", "Camera::look_at/use_perspective", "3D 相机"));
|
|
append(gallery_action("toggle_projection", "切换投影", "Camera::use_perspective/use_orthographic", "3D 相机"));
|
|
append(gallery_action("toggle_texture", "切换纹理", "Material::use_texture", "3D 材质"));
|
|
append(gallery_action("change_text", "修改 MSDF 文本", "Text::set_string", "3D 文本"));
|
|
append(gallery_action("reset", "恢复 3D 场景", "Gallery_Scene3D factory", "3D 场景"));
|
|
}
|
|
std::vector<Action_Model> registered_actions(std::string_view case_id, Gallery_Frame_Mode frame_mode) {
|
|
std::vector<Action_Model> result;
|
|
if (case_id == "datoviz_3d") {
|
|
append_3d_actions(frame_mode, result);
|
|
return result;
|
|
}
|
|
append_session_actions(frame_mode, result);
|
|
for (auto& action : gallery_frame_actions(frame_mode))
|
|
result.push_back(std::move(action));
|
|
append_gallery_renderable_actions(case_id, result);
|
|
return result;
|
|
}
|
|
|
|
Json parse_request(std::string_view message) {
|
|
return Json::parse(message.begin(), message.end());
|
|
}
|
|
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, "Renderive 组件"),
|
|
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, "Renderive 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>("Renderive 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();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
Json dashboard_contract() {
|
|
Json point_pair{{"label", "输入→绘制"}, {"format", "pair"},
|
|
{"sources", Json::array({"data_shape.input_points", "data_shape.rendered_elements"})},
|
|
{"separator", "→"}};
|
|
Json bottleneck = described_dashboard_field<renderive::Frame_Observer_Snapshot>(
|
|
"kernel_observer", "limit_state");
|
|
bottleneck["format"] = "duration_enum";
|
|
bottleneck["duration_sources"] = {
|
|
{"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 dashboard{
|
|
{"value_maps", {
|
|
{"enabled", {{"true", "启用"}, {"false", "关闭"}}},
|
|
{"consumer_feedback_source", {
|
|
{"disabled", "总开关关闭"}, {"none", "无"}, {"pixel", "像素响应"},
|
|
{"presentation", "浏览器呈现"}, {"manual", "手动"}
|
|
}},
|
|
{"limit_state", {
|
|
{"frequency_limited", "内核频率受限"}, {"paint_limited", "绘制事件受限"},
|
|
{"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("Core 滑动平均 ms", "performance.average_render_ms", "fixed", 2),
|
|
dashboard_field("Core P95 ms", "performance.render_p95_ms", "fixed", 2),
|
|
dashboard_field("Core P99 ms", "performance.render_p99_ms", "fixed", 2),
|
|
dashboard_field("像素编码 ms", "performance.last_pixel_encode_ms", "fixed", 2),
|
|
dashboard_field("编码 P95 ms", "performance.pixel_encode_p95_ms", "fixed", 2),
|
|
dashboard_field("编码 P99 ms", "performance.pixel_encode_p99_ms", "fixed", 2),
|
|
dashboard_field("WS 平均 ms", "client_performance.frame_round_trip_average_ms", "fixed", 2),
|
|
dashboard_field("WS P95 ms", "client_performance.frame_round_trip_p95_ms", "fixed", 2),
|
|
dashboard_field("WS P99 ms", "client_performance.frame_round_trip_p99_ms", "fixed", 2),
|
|
dashboard_field("呈现平均 ms", "client_performance.display_interval_average_ms", "fixed", 2),
|
|
dashboard_field("呈现 P95 ms", "client_performance.display_interval_p95_ms", "fixed", 2),
|
|
dashboard_field("呈现 P99 ms", "client_performance.display_interval_p99_ms", "fixed", 2),
|
|
dashboard_field("响应负载 MB/s", "performance.pixel_payload_megabytes_per_second", "fixed", 1),
|
|
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),
|
|
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)
|
|
})}
|
|
}},
|
|
{"menu_views", {
|
|
{"observer", {
|
|
{"kernel", {
|
|
{"title", "内核帧观察器"},
|
|
{"source", "kernel_observer"},
|
|
{"descriptor", adminive::to_descriptor_json<
|
|
Json, renderive::Frame_Observer_Snapshot>()}
|
|
}},
|
|
{"renderables_source", "renderable_observers"},
|
|
{"renderable_fields", Json::array({
|
|
"event", "event_time_ns", "cache_update_count", "publish_count"
|
|
})}
|
|
}},
|
|
{"performance", {
|
|
{"renderables_source", "renderable_observers"},
|
|
{"renderable_fields", Json::array({
|
|
"event", "event_time_ns", "cache_update_count", "publish_count"
|
|
})},
|
|
{"resources", Json::array({
|
|
{
|
|
{"title", "渲染性能"},
|
|
{"source", "performance"},
|
|
{"descriptor", adminive::to_descriptor_json<
|
|
Json, Gallery_Render_Performance>()}
|
|
},
|
|
{
|
|
{"title", "浏览器性能"},
|
|
{"source", "client_performance"},
|
|
{"descriptor", adminive::to_descriptor_json<
|
|
Json, Gallery_Client_Performance>()}
|
|
}
|
|
})}
|
|
}}
|
|
}},
|
|
{"limits", {
|
|
{"aria_label", "低延迟限速来源"}, {"title", "限速来源"},
|
|
{"current_source", described_source<renderive::Frame_Observer_Snapshot>(
|
|
"kernel_observer", "limit_state")},
|
|
{"active_label", "当前瓶颈"}, {"inactive_label", "未受限"},
|
|
{"disabled_label", "已关闭"},
|
|
{"fields", Json::array({
|
|
{{"label", "内核用户频率"}, {"active_value", "frequency_limited"},
|
|
{"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", "内核绘制事件"}, {"active_value", "paint_limited"},
|
|
{"duration_source", described_source<renderive::Frame_Observer_Snapshot>(
|
|
"kernel_observer", "paint_duration_ns")}},
|
|
{{"label", "内核后台渲染"}, {"active_value", "render_limited"},
|
|
{"duration_source", described_source<renderive::Frame_Observer_Snapshot>(
|
|
"kernel_observer", "render_duration_ns")}},
|
|
{{"label", "消费者反馈"}, {"active_value", "consumer_limited"},
|
|
{"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", "内核低延迟全量统计"},
|
|
{"header", {
|
|
{"prefix", "内核"}, {"suffix", "观察器"}, {"event_label", "事件"},
|
|
{"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", 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),
|
|
dashboard_field("RAF 中位周期", "client_performance.display_interval_ms", "milliseconds", 3),
|
|
dashboard_field("RAF 平均周期", "client_performance.display_interval_average_ms", "milliseconds", 3),
|
|
dashboard_field("RAF P95 周期", "client_performance.display_interval_p95_ms", "milliseconds", 3),
|
|
dashboard_field("RAF P99 周期", "client_performance.display_interval_p99_ms", "milliseconds", 3),
|
|
dashboard_field("RAF 周期标准差", "client_performance.display_interval_deviation_ms", "milliseconds", 3),
|
|
dashboard_field("WS 往返", "client_performance.frame_round_trip_ms", "milliseconds", 3),
|
|
dashboard_field("WS 往返平均", "client_performance.frame_round_trip_average_ms", "milliseconds", 3),
|
|
dashboard_field("WS 往返 P95", "client_performance.frame_round_trip_p95_ms", "milliseconds", 3),
|
|
dashboard_field("WS 往返 P99", "client_performance.frame_round_trip_p99_ms", "milliseconds", 3),
|
|
dashboard_field("WS 往返标准差", "client_performance.frame_round_trip_deviation_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", "内核各阶段等待耗时"},
|
|
{"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", 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", 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", gallery_enum_id(mode)}, {"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
|
|
|
|
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", gallery_enum_id(Gallery_Frame_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 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();
|
|
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();
|
|
const std::size_t renderable_controls = item.id == "datoviz_3d" ? 0 : gallery_renderable_control_count(item.id);
|
|
for (const auto mode : frame_modes) {
|
|
const std::string key = gallery_enum_id(mode);
|
|
const std::size_t controls =
|
|
gallery_detail::session_control_count(item.id, mode) + renderable_controls;
|
|
const std::size_t actions =
|
|
gallery_detail::registered_actions(item.id, mode).size();
|
|
entry["control_count_by_mode"][key] = controls;
|
|
entry["action_count_by_mode"][key] = actions;
|
|
controls_total += controls;
|
|
actions_total += actions;
|
|
}
|
|
entry["control_count"] =
|
|
gallery_detail::session_control_count(item.id, Gallery_Frame_Mode::Low_Latency) +
|
|
renderable_controls;
|
|
entry["action_count"] =
|
|
gallery_detail::registered_actions(
|
|
item.id, Gallery_Frame_Mode::Low_Latency).size();
|
|
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", magic_enum::enum_names<Image_Interpolation_Mode>()}};
|
|
return result.dump();
|
|
}
|
|
|
|
bool Gallery_Protocol::is_case(std::string_view case_id) {
|
|
return gallery_detail::find_case(case_id) != nullptr;
|
|
}
|
|
|
|
std::string Gallery_Protocol::case_json_from_controls(
|
|
std::string_view case_id,
|
|
std::string_view controls_json,
|
|
std::string_view telemetry_json,
|
|
std::string_view notice,
|
|
Gallery_Frame_Mode frame_mode,
|
|
bool manual_refresh) {
|
|
Json result = protocol_base();
|
|
result["type"] = manual_refresh ? "refresh_state" : "case_state";
|
|
result["case"] = case_contract(case_id);
|
|
result["frame_mode"] = frame_mode_contract(frame_mode);
|
|
try {
|
|
result["controls"] = Json::parse(controls_json.begin(), controls_json.end());
|
|
} catch (const std::exception&) {
|
|
result["controls"] = {
|
|
{"resources", Json::array()},
|
|
{"observers", Json::array()},
|
|
{"render_plan", Json::object()}
|
|
};
|
|
}
|
|
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& action : gallery_detail::registered_actions(case_id, frame_mode))
|
|
result["actions"]["data"].push_back(
|
|
adminive::to_frontend_json<Json>(action));
|
|
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::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();
|
|
}
|
|
|
|
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 = 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&) {
|
|
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::optional<Gallery_Control_Patch_Request> Gallery_Protocol::control_patch_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_patch" ||
|
|
!request.contains("target") || !request.at("target").is_string() ||
|
|
!request.contains("patch") || !request.at("patch").is_object())
|
|
return std::nullopt;
|
|
return Gallery_Control_Patch_Request{
|
|
request.at("target").get<std::string>(), request.at("patch").dump()};
|
|
} catch (const std::exception&) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
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::registered_actions(case_id, frame_mode);
|
|
return std::any_of(available.begin(), available.end(),
|
|
[action_id](const gallery_detail::Action_Model& item) {
|
|
return item.id == action_id;
|
|
});
|
|
}
|
|
|
|
} // namespace renderive::web
|