恢复成自定义协议了

This commit is contained in:
2026-08-11 16:10:02 +08:00
parent 274dd4c044
commit f2c1374a7f
27 changed files with 820 additions and 1526 deletions
+32 -33
View File
@@ -1,77 +1,76 @@
set(Renderive_Web_dependencies global::drogon global::spdlog)
if(RENDERIVE_BUILD_TESTS)
if (RENDERIVE_BUILD_TESTS)
list(APPEND Renderive_Web_dependencies global::GTest)
endif()
endif ()
rcl_add_dependency_action_targets(Renderive_Web_env ${Renderive_Web_dependencies})
set_target_properties(Renderive_Web_env PROPERTIES FOLDER Renderive_Web)
library_is_installed_with_rely(Renderive_Web_dependencies_installed ${Renderive_Web_dependencies})
if(NOT Renderive_Web_dependencies_installed)
if (NOT Renderive_Web_dependencies_installed)
rcl_log_append("[FATAL_ERROR] Renderive_Web dependencies are not installed. Build Renderive_Web_env first")
return()
endif()
endif ()
function(renderive_find_web_dependencies)
if(POLICY CMP0144)
if (POLICY CMP0144)
cmake_policy(SET CMP0144 NEW)
endif()
endif ()
rcl_load_dependency_environment(${Renderive_Web_dependencies})
find_package(Drogon CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
if(RENDERIVE_BUILD_TESTS)
if (RENDERIVE_BUILD_TESTS)
find_package(GTest CONFIG REQUIRED)
endif()
endif ()
endfunction()
renderive_find_web_dependencies()
if(NOT TARGET Renderive_render_2D)
if (NOT TARGET Renderive_render_2D)
rcl_log_append("[FATAL_ERROR] Renderive_Web requires Renderive_render_2D")
return()
endif()
if(NOT TARGET Adminive::Nlohmann)
endif ()
if (NOT TARGET Adminive::Nlohmann)
set(Renderive_Web_saved_build_testing "${BUILD_TESTING}")
set(BUILD_TESTING OFF)
add_subdirectory(
"${CMAKE_CURRENT_LIST_DIR}/../third_party/Adminive"
"${CMAKE_CURRENT_BINARY_DIR}/third_party/Adminive"
EXCLUDE_FROM_ALL
"${CMAKE_CURRENT_LIST_DIR}/../third_party/Adminive"
"${CMAKE_CURRENT_BINARY_DIR}/third_party/Adminive"
EXCLUDE_FROM_ALL
)
set(BUILD_TESTING "${Renderive_Web_saved_build_testing}")
unset(Renderive_Web_saved_build_testing)
endif()
set(Renderive_Web_source_dir "${CMAKE_CURRENT_LIST_DIR}")
endif ()
set(Renderive_Web_source_dir "${CMAKE_CURRENT_LIST_DIR}/app")
append_glob_source(Renderive_Web_sources "${Renderive_Web_source_dir}")
list(FILTER Renderive_Web_sources EXCLUDE REGEX "/(app|tests)/")
add_library(Renderive_Web STATIC ${Renderive_Web_sources})
target_include_directories(Renderive_Web PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/..>"
"$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/..>"
)
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
PRIVATE Adminive::Nlohmann spdlog::spdlog
PUBLIC Renderive_render_2D Drogon::Drogon
PRIVATE Adminive::Nlohmann spdlog::spdlog
)
set(Renderive_Web_Server_source_dir "${CMAKE_CURRENT_LIST_DIR}/app")
set(Renderive_Web_Server_source_dir "${CMAKE_CURRENT_LIST_DIR}/server")
append_glob_source(Renderive_Web_Server_sources "${Renderive_Web_Server_source_dir}")
add_executable(Renderive_Web_Server ${Renderive_Web_Server_sources})
target_compile_features(Renderive_Web_Server PRIVATE cxx_std_20)
target_link_libraries(Renderive_Web_Server PRIVATE Renderive_Web)
add_custom_target(Renderive_Web_Assets
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${CMAKE_CURRENT_LIST_DIR}/../webapp_gallery"
"${CMAKE_CURRENT_BINARY_DIR}/webapp_gallery"
COMMENT "Synchronizing Renderive Web assets"
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${CMAKE_CURRENT_LIST_DIR}/../webapp_gallery"
"${CMAKE_CURRENT_BINARY_DIR}/webapp_gallery"
COMMENT "Synchronizing Renderive Web assets"
)
add_dependencies(Renderive_Web_Server Renderive_Web_Assets)
if(MSVC)
if (MSVC)
target_compile_options(Renderive_Web PRIVATE /utf-8)
target_compile_options(Renderive_Web_Server PRIVATE /utf-8)
endif()
if(RENDERIVE_BUILD_TESTS)
endif ()
if (RENDERIVE_BUILD_TESTS)
set(Renderive_Web_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests")
append_glob_source(Renderive_Web_test_sources "${Renderive_Web_test_dir}")
foreach(Renderive_Web_test_source IN LISTS Renderive_Web_test_sources)
if(NOT Renderive_Web_test_source MATCHES "\\.(c|cc|cpp|cxx)$")
foreach (Renderive_Web_test_source IN LISTS Renderive_Web_test_sources)
if (NOT Renderive_Web_test_source MATCHES "\\.(c|cc|cpp|cxx)$")
continue()
endif()
endif ()
file(RELATIVE_PATH Renderive_Web_test_name "${Renderive_Web_test_dir}" "${Renderive_Web_test_source}")
string(MD5 Renderive_Web_test_hash "${Renderive_Web_test_name}")
string(SUBSTRING "${Renderive_Web_test_hash}" 0 8 Renderive_Web_test_hash)
@@ -81,5 +80,5 @@ if(RENDERIVE_BUILD_TESTS)
add_executable("${Renderive_Web_test_target}" "${Renderive_Web_test_source}")
target_link_libraries("${Renderive_Web_test_target}" PRIVATE Renderive_Web Adminive::Nlohmann GTest::gtest_main)
add_test(NAME "${Renderive_Web_test_target}" COMMAND "${Renderive_Web_test_target}")
endforeach()
endif()
endforeach ()
endif ()
@@ -1757,8 +1757,7 @@ struct Gallery_Plot_Session::Impl {
}
else if constexpr (std::is_same_v<T, Gallery_Request>) {
return value.kind != Gallery_Request_Kind::Catalog &&
value.kind != Gallery_Request_Kind::Refresh &&
value.kind != Gallery_Request_Kind::Feedback;
value.kind != Gallery_Request_Kind::Observe;
}
else {
return true;
@@ -1805,20 +1804,15 @@ struct Gallery_Plot_Session::Impl {
Web_Response_Type::Json,
Gallery_Protocol::error_json("请先发送 gallery_open")
};
if (request.kind == Gallery_Request_Kind::Feedback) {
if (request.kind == Gallery_Request_Kind::Observe) {
if (update_client_metrics(request.message)) {
++scheduler_revision;
scheduler_condition.notify_all();
}
return std::nullopt;
}
if (request.kind == Gallery_Request_Kind::Refresh) {
return Web_Response{
Web_Response_Type::Json,
Gallery_Protocol::case_json(scene->case_id(), scene->state(),
scene->telemetry_json(),
"后端状态已手动刷新",
scene->frame_mode())
Gallery_Protocol::observer_json(
scene->case_id(), scene->frame_mode(), scene->telemetry_json())
};
}
if (request.kind == Gallery_Request_Kind::Patch) {
@@ -522,72 +522,6 @@ Json value_json(const Gallery_Value& value) {
return std::visit([](const auto& item) { return Json(item); }, value);
}
Json control_amis_schema(const Control_Definition& definition) {
const auto& model = definition.model;
Json result{
{"name", model.id},
{"label", model.label},
{"description", model.description.empty() ? model.api : model.description},
{"remark", model.api},
{"size", "md"}
};
if (model.input == "boolean") {
result["type"] = "switch";
return result;
}
if (model.input == "select") {
result["type"] = "select";
result["required"] = true;
result["clearable"] = false;
result["options"] = Json::array();
for (const auto& option : definition.options)
result["options"].push_back({{"label", option}, {"value", option}});
return result;
}
if (model.input == "color") {
result["type"] = "input-color";
result["required"] = true;
result["clearable"] = false;
result["validations"] = {{"matchRegexp", "^#[0-9A-Fa-f]{6}$"}, {"maxLength", 7}};
result["validationErrors"] = {{"matchRegexp", "颜色必须使用 #RRGGBB"}, {"maxLength", "颜色必须使用 #RRGGBB"}};
return result;
}
if (model.input == "number") {
result["type"] = "input-number";
result["required"] = true;
result["min"] = model.minimum;
result["max"] = model.maximum;
result["step"] = model.step;
result["validations"] = {{"minimum", model.minimum}, {"maximum", model.maximum}};
result["validationErrors"] = {
{"minimum", "数值低于后端允许范围"},
{"maximum", "数值超出后端允许范围"}
};
if (model.integer) {
result["precision"] = 0;
result["validations"]["isInt"] = true;
result["validationErrors"]["isInt"] = "必须是整数";
}
return result;
}
result["type"] = "input-text";
result["validations"] = {{"maxLength", 160}};
result["validationErrors"] = {{"maxLength", "文本长度不能超过 160 个字符"}};
return result;
}
Json action_argument_amis_schema(const Action_Definition& definition) {
const auto& model = definition.model;
if (model.argument_input.empty())
return Json();
return {
{"type", model.argument_input == "number" ? "input-number" : "input-text"},
{"name", "action_" + model.id},
{"label", model.argument_label.empty() ? "参数" : model.argument_label},
{"required", true}
};
}
std::optional<Gallery_Value> parse_value(const Json& value,
const Control_Definition& definition,
std::string& error) {
@@ -755,7 +689,7 @@ using gallery_detail::Json;
Json protocol_base() {
return {{"category", "gallery"},
{"protocol", "renderive.control-gallery"},
{"protocol_version", 4}};
{"protocol_version", 3}};
}
Json case_contract(std::string_view case_id) {
@@ -889,7 +823,6 @@ std::string Gallery_Protocol::case_json(std::string_view case_id,
? gallery_detail::value_json(definition.initial)
: gallery_detail::value_json(found->second);
item["options"] = definition.options;
item["amis"] = gallery_detail::control_amis_schema(definition);
result["controls"]["data"].push_back(std::move(item));
}
result["actions"] = {
@@ -898,12 +831,9 @@ std::string Gallery_Protocol::case_json(std::string_view case_id,
gallery_detail::action_view())},
{"data", Json::array()}
};
for (const auto& definition : gallery_detail::actions(case_id, frame_mode)) {
Json item = adminive::to_frontend_json<Json>(definition.model);
if (!definition.model.argument_input.empty())
item["argument_amis"] = gallery_detail::action_argument_amis_schema(definition);
result["actions"]["data"].push_back(std::move(item));
}
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&) {
@@ -933,6 +863,21 @@ std::string Gallery_Protocol::error_json(std::string_view 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,
@@ -57,6 +57,10 @@ public:
[[nodiscard]] static std::string error_json(
std::string_view message,
std::string_view field = {});
[[nodiscard]] static std::string observer_json(
std::string_view case_id,
Gallery_Frame_Mode frame_mode,
std::string_view telemetry_json);
[[nodiscard]] static Gallery_Patch_Result apply_patch(
std::string_view case_id,
const Gallery_State& current,
@@ -27,7 +27,7 @@ struct Set_Smoothing {
bool enabled{};
};
struct Clear_Selection {};
enum class Gallery_Request_Kind : std::uint8_t { Catalog, Open, Refresh, Feedback, Patch, Action };
enum class Gallery_Request_Kind : std::uint8_t { Catalog, Open, Patch, Action, Observe };
struct Gallery_Request {
Gallery_Request_Kind kind = Gallery_Request_Kind::Catalog;
std::string message;
@@ -211,14 +211,12 @@ std::optional<Web_Event> Web_Event_Adapter::decode(std::string_view message) {
return Gallery_Request{Gallery_Request_Kind::Catalog, std::string(message)};
if (type == "gallery_open")
return Gallery_Request{Gallery_Request_Kind::Open, std::string(message)};
if (type == "gallery_refresh")
return Gallery_Request{Gallery_Request_Kind::Refresh, std::string(message)};
if (type == "gallery_feedback")
return Gallery_Request{Gallery_Request_Kind::Feedback, std::string(message)};
if (type == "gallery_patch")
return Gallery_Request{Gallery_Request_Kind::Patch, std::string(message)};
if (type == "gallery_action")
return Gallery_Request{Gallery_Request_Kind::Action, std::string(message)};
if (type == "gallery_observe")
return Gallery_Request{Gallery_Request_Kind::Observe, std::string(message)};
return std::nullopt;
}
@@ -1,4 +1,4 @@
#include "web_server/Web_Server.h"
#include "web_server/app/Web_Server.h"
#include <charconv>
#include <cstdint>
#include <filesystem>
+61 -148
View File
@@ -1,8 +1,8 @@
#include "web_server/Gallery_Plot_Session.h"
#include "web_server/Gallery_Protocol.h"
#include "web_server/Pixel_Frame.h"
#include "web_server/Web_Event_Adapter.h"
#include "web_server/Web_Plot_Session.h"
#include "../app/Gallery_Plot_Session.h"
#include "../app/Gallery_Protocol.h"
#include "../app/Pixel_Frame.h"
#include "../app/Web_Event_Adapter.h"
#include "../app/Web_Plot_Session.h"
#include <gtest/gtest.h>
#include <nlohmann/json.hpp>
@@ -52,10 +52,10 @@ nlohmann::json response_json(std::optional<Web_Response> response) {
return parse_json(response->payload);
}
nlohmann::json refresh_telemetry(Gallery_Plot_Session& session) {
nlohmann::json observe_telemetry(Gallery_Plot_Session& session) {
return response_json(session.handle(gallery_request(
Gallery_Request_Kind::Refresh,
R"({"category":"event","type":"gallery_refresh"})"))).value(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"))).value(
"telemetry", nlohmann::json::object());
}
@@ -76,11 +76,6 @@ nlohmann::json patch_controls(Gallery_Plot_Session& session, nlohmann::json patc
Gallery_Request_Kind::Patch, request.dump())));
}
void send_client_feedback(Gallery_Plot_Session& session, std::string_view encoded) {
EXPECT_FALSE(session.handle(gallery_request(
Gallery_Request_Kind::Feedback, std::string(encoded))).has_value());
}
template <class Predicate>
bool wait_for_condition(Predicate&& predicate,
std::chrono::milliseconds timeout = std::chrono::milliseconds(750)) {
@@ -116,7 +111,7 @@ void set_gallery_view_active(Gallery_Plot_Session& session, bool active) {
}
std::uint64_t successful_render_count(Gallery_Plot_Session& session) {
return refresh_telemetry(session).at("performance").at("successful_render_count")
return observe_telemetry(session).at("performance").at("successful_render_count")
.get<std::uint64_t>();
}
@@ -148,14 +143,10 @@ TEST(RenderiveWebBridge, DecodesOnlyEventMessages) {
ASSERT_TRUE(open.has_value());
EXPECT_EQ(std::get<Gallery_Request>(*open).kind, Gallery_Request_Kind::Open);
const auto refresh = Web_Event_Adapter::decode(
R"({"category":"event","type":"gallery_refresh"})");
ASSERT_TRUE(refresh.has_value());
EXPECT_EQ(std::get<Gallery_Request>(*refresh).kind, Gallery_Request_Kind::Refresh);
const auto feedback = Web_Event_Adapter::decode(
R"({"category":"event","type":"gallery_feedback","client_metrics":{}})");
ASSERT_TRUE(feedback.has_value());
EXPECT_EQ(std::get<Gallery_Request>(*feedback).kind, Gallery_Request_Kind::Feedback);
const auto observe = Web_Event_Adapter::decode(
R"({"category":"event","type":"gallery_observe"})");
ASSERT_TRUE(observe.has_value());
EXPECT_EQ(std::get<Gallery_Request>(*observe).kind, Gallery_Request_Kind::Observe);
const auto bounded = Web_Event_Adapter::decode(
R"({"category":"event","type":"resize","width":1e100,"height":-1e100})");
@@ -200,7 +191,6 @@ TEST(RenderiveWebGallery, AdminiveCatalogCoversEveryControlCaseAndThreeModes) {
EXPECT_EQ(catalog.at("category"), "gallery");
EXPECT_EQ(catalog.at("type"), "catalog");
EXPECT_EQ(catalog.at("protocol"), "renderive.control-gallery");
EXPECT_EQ(catalog.at("protocol_version"), 4);
EXPECT_EQ(catalog.at("case_descriptor").at("protocol"), "adminive.resource");
EXPECT_EQ(catalog.at("cases").size(), 8U);
EXPECT_EQ(catalog.at("frame_modes").size(), 3U);
@@ -320,8 +310,6 @@ TEST(RenderiveWebGallery, EveryPublishedControlValidatesItsCompleteInputContract
EXPECT_TRUE(ids.insert(id).second);
EXPECT_FALSE(control.at("api").get<std::string>().empty());
EXPECT_FALSE(control.at("group").get<std::string>().empty());
const auto& amis = control.at("amis");
EXPECT_EQ(amis.at("name"), id);
const std::string input = control.at("input");
const auto apply = [&](const nlohmann::json& value) {
@@ -337,28 +325,16 @@ TEST(RenderiveWebGallery, EveryPublishedControlValidatesItsCompleteInputContract
};
if (input == "boolean") {
EXPECT_EQ(amis.at("type"), "switch");
const bool alternative = !control.at("value").get<bool>();
const auto accepted = apply(alternative);
ASSERT_TRUE(accepted.candidate.has_value());
EXPECT_EQ(std::get<bool>(accepted.candidate->values.at(id)), alternative);
expect_rejected(1);
} else if (input == "number") {
EXPECT_EQ(amis.at("type"), "input-number");
EXPECT_TRUE(amis.at("required").get<bool>());
const double minimum = control.at("minimum");
const double maximum = control.at("maximum");
const double current = control.at("value");
const double step = std::max(control.at("step").get<double>(), 1e-9);
EXPECT_DOUBLE_EQ(amis.at("min"), minimum);
EXPECT_DOUBLE_EQ(amis.at("max"), maximum);
EXPECT_DOUBLE_EQ(amis.at("step"), control.at("step").get<double>());
EXPECT_DOUBLE_EQ(amis.at("validations").at("minimum"), minimum);
EXPECT_DOUBLE_EQ(amis.at("validations").at("maximum"), maximum);
if (control.at("integer").get<bool>()) {
EXPECT_EQ(amis.at("precision"), 0);
EXPECT_TRUE(amis.at("validations").at("isInt").get<bool>());
}
double alternative = current + step;
if (alternative > maximum)
alternative = current - step;
@@ -380,11 +356,7 @@ TEST(RenderiveWebGallery, EveryPublishedControlValidatesItsCompleteInputContract
expect_rejected(std::clamp(std::floor(current) + 0.5,
minimum + 0.5, maximum - 0.5));
} else if (input == "select") {
EXPECT_EQ(amis.at("type"), "select");
EXPECT_TRUE(amis.at("required").get<bool>());
EXPECT_FALSE(amis.at("clearable").get<bool>());
const auto& options = control.at("options");
EXPECT_EQ(amis.at("options").size(), options.size());
ASSERT_FALSE(options.empty());
const std::string current = control.at("value");
const auto alternative = std::find_if(
@@ -399,10 +371,6 @@ TEST(RenderiveWebGallery, EveryPublishedControlValidatesItsCompleteInputContract
expect_rejected("__invalid_gallery_option__");
expect_rejected(7);
} else if (input == "color") {
EXPECT_EQ(amis.at("type"), "input-color");
EXPECT_TRUE(amis.at("required").get<bool>());
EXPECT_FALSE(amis.at("clearable").get<bool>());
EXPECT_EQ(amis.at("validations").at("matchRegexp"), "^#[0-9A-Fa-f]{6}$");
const std::string value = control.at("value") == "#123456" ?
"#654321" : "#123456";
const auto accepted = apply(value);
@@ -412,8 +380,6 @@ TEST(RenderiveWebGallery, EveryPublishedControlValidatesItsCompleteInputContract
expect_rejected(7);
} else {
ASSERT_EQ(input, "text");
EXPECT_EQ(amis.at("type"), "input-text");
EXPECT_EQ(amis.at("validations").at("maxLength"), 160);
std::string value = control.at("value").get<std::string>() + "_qa";
const auto accepted = apply(value);
ASSERT_TRUE(accepted.candidate.has_value());
@@ -449,13 +415,6 @@ TEST(RenderiveWebGallery, EveryPublishedActionDispatchesToItsCanvasAndFrameMode)
EXPECT_TRUE(ids.insert(id).second);
EXPECT_FALSE(action.at("api").get<std::string>().empty());
EXPECT_FALSE(action.at("group").get<std::string>().empty());
const std::string argument_input = action.at("argument_input");
if (!argument_input.empty()) {
const auto& amis = action.at("argument_amis");
EXPECT_EQ(amis.at("name"), "action_" + id);
EXPECT_TRUE(amis.at("required").get<bool>());
EXPECT_EQ(amis.at("type"), argument_input == "number" ? "input-number" : "input-text");
}
Gallery_Plot_Session session;
ASSERT_EQ(response_json(session.handle(gallery_request(
@@ -700,7 +659,7 @@ TEST(RenderiveWebGallery, SelectionOverlayPointerLifecycleCreatesAndClearsRegion
EXPECT_FALSE(session.handle(*event).has_value());
}
EXPECT_EQ(refresh_telemetry(session).at("selection_regions"), 1);
EXPECT_EQ(observe_telemetry(session).at("selection_regions"), 1);
EXPECT_EQ(invoke_action(session, "clear_selection").at("telemetry")
.at("selection_regions"), 0);
const auto rebound = invoke_action(session, "rebind_selection_axes").at("telemetry");
@@ -756,7 +715,7 @@ TEST(RenderiveWebGallery, EveryCanvasAcceptsTheCompleteWebEventSetAndStillRender
EXPECT_EQ(frame->payload.substr(0, 4), "RVP1");
EXPECT_EQ(read_u32_le(frame->payload, 4), 420U);
EXPECT_EQ(read_u32_le(frame->payload, 8), 260U);
const auto telemetry = refresh_telemetry(session);
const auto telemetry = observe_telemetry(session);
EXPECT_TRUE(telemetry.at("view_active").get<bool>());
EXPECT_EQ(telemetry.at("viewport").at("width"), 420);
EXPECT_EQ(telemetry.at("viewport").at("height"), 260);
@@ -809,11 +768,11 @@ TEST(RenderiveWebGallery, EveryIndependentCore2CanvasBuildsAndRenders) {
EXPECT_EQ(read_u32_le(frame->payload, 4), 480U) << case_id;
EXPECT_EQ(read_u32_le(frame->payload, 8), 280U) << case_id;
const auto observed = session.handle(gallery_request(
Gallery_Request_Kind::Refresh,
R"({"category":"event","type":"gallery_refresh"})"));
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"));
ASSERT_TRUE(observed.has_value()) << case_id;
const auto telemetry = parse_json(observed->payload);
EXPECT_EQ(telemetry.at("type"), "case_state") << case_id;
EXPECT_EQ(telemetry.at("type"), "observer_state") << case_id;
EXPECT_EQ(telemetry.at("frame_mode").at("id"), std::string(mode)) << case_id;
EXPECT_TRUE(telemetry.at("telemetry").contains("kernel_observer")) << case_id;
EXPECT_TRUE(telemetry.at("telemetry").contains("performance")) << case_id;
@@ -824,54 +783,6 @@ TEST(RenderiveWebGallery, EveryIndependentCore2CanvasBuildsAndRenders) {
}
}
TEST(RenderiveWebGallery, BackendPublishesAmisValidationSchemas) {
Gallery_Plot_Session low_latency;
const auto opened = low_latency.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")));
ASSERT_TRUE(opened.has_value());
const auto state = parse_json(opened->payload);
const auto& controls = state.at("controls").at("data");
const auto max_fps = std::find_if(controls.begin(), controls.end(), [](const auto& item) {
return item.at("id") == "max_render_fps";
});
ASSERT_NE(max_fps, controls.end());
const auto& max_fps_amis = max_fps->at("amis");
EXPECT_EQ(max_fps_amis.at("type"), "input-number");
EXPECT_TRUE(max_fps_amis.at("required").get<bool>());
EXPECT_DOUBLE_EQ(max_fps_amis.at("min").get<double>(), 0.01);
EXPECT_DOUBLE_EQ(max_fps_amis.at("max").get<double>(), 1'000'000'000.0);
EXPECT_DOUBLE_EQ(max_fps_amis.at("validations").at("minimum").get<double>(), 0.01);
EXPECT_DOUBLE_EQ(max_fps_amis.at("validations").at("maximum").get<double>(), 1'000'000'000.0);
const auto background = std::find_if(controls.begin(), controls.end(), [](const auto& item) {
return item.at("id") == "background_color";
});
ASSERT_NE(background, controls.end());
EXPECT_TRUE(background->at("amis").at("required").get<bool>());
EXPECT_FALSE(background->at("amis").at("clearable").get<bool>());
EXPECT_EQ(background->at("amis").at("validations").at("matchRegexp"),
"^#[0-9A-Fa-f]{6}$");
const auto patched = Gallery_Protocol::apply_patch(
"spectrum", Gallery_Protocol::default_state("spectrum", Gallery_Frame_Mode::Low_Latency),
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":100}})",
Gallery_Frame_Mode::Low_Latency);
ASSERT_TRUE(patched.candidate.has_value());
EXPECT_DOUBLE_EQ(std::get<double>(patched.candidate->values.at("max_render_fps")), 100.0);
Gallery_Plot_Session playback;
const auto playback_opened = playback.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "playback")));
ASSERT_TRUE(playback_opened.has_value());
const auto playback_state = parse_json(playback_opened->payload);
const auto& actions = playback_state.at("actions").at("data");
const auto burst = std::find_if(actions.begin(), actions.end(), [](const auto& item) {
return item.at("id") == "mode_enqueue_burst";
});
ASSERT_NE(burst, actions.end());
const auto& burst_amis = burst->at("argument_amis");
EXPECT_EQ(burst_amis.at("type"), "input-number");
EXPECT_EQ(burst_amis.at("name"), "action_mode_enqueue_burst");
EXPECT_TRUE(burst_amis.at("required").get<bool>());
}
TEST(RenderiveWebGallery, ActionsMutateLiveCore2ObjectsAndReturnTelemetry) {
Gallery_Plot_Session session;
ASSERT_TRUE(session.handle(gallery_request(
@@ -908,10 +819,10 @@ TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers
request["argument"] = *argument;
return session.handle(gallery_request(Gallery_Request_Kind::Action, request.dump()));
};
const auto refresh = [](Gallery_Plot_Session& session) {
const auto observe = [](Gallery_Plot_Session& session) {
const auto response = session.handle(gallery_request(
Gallery_Request_Kind::Refresh,
R"({"category":"event","type":"gallery_refresh"})"));
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"));
EXPECT_TRUE(response.has_value());
return parse_json(response->payload).at("telemetry");
};
@@ -920,12 +831,12 @@ TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers
ASSERT_TRUE(manual.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "manual"))));
ASSERT_TRUE(action(manual, "mode_prepare"));
auto manual_observer = refresh(manual).at("kernel_observer");
auto manual_observer = observe(manual).at("kernel_observer");
EXPECT_EQ(manual_observer.at("last_event"), "prepared");
EXPECT_EQ(manual_observer.at("pending_frame_count"), 1);
ASSERT_TRUE(action(manual, "mode_refresh"));
ASSERT_TRUE(action(manual, "mode_render"));
manual_observer = refresh(manual).at("kernel_observer");
manual_observer = observe(manual).at("kernel_observer");
EXPECT_EQ(manual_observer.at("last_event"), "rendered");
EXPECT_GE(manual_observer.at("consumed_frame_count").get<std::uint64_t>(), 2U);
@@ -938,7 +849,7 @@ TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers
ASSERT_TRUE(high_frequency.has_value());
ASSERT_EQ(parse_json(high_frequency->payload).at("type"), "case_state");
ASSERT_TRUE(low_latency.handle(Frame_Request{}));
const auto low_telemetry = refresh(low_latency);
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"));
@@ -971,11 +882,11 @@ TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers
ASSERT_TRUE(playback.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "playback"))));
ASSERT_TRUE(action(playback, "mode_enqueue_burst", 5));
auto playback_observer = refresh(playback).at("kernel_observer");
auto playback_observer = observe(playback).at("kernel_observer");
EXPECT_EQ(playback_observer.at("last_event"), "enqueued");
EXPECT_EQ(playback_observer.at("pending_frame_count"), 5);
ASSERT_TRUE(action(playback, "mode_dequeue"));
playback_observer = refresh(playback).at("kernel_observer");
playback_observer = observe(playback).at("kernel_observer");
EXPECT_EQ(playback_observer.at("last_event"), "rendered");
EXPECT_EQ(playback_observer.at("pending_frame_count"), 4);
EXPECT_GT(playback_observer.at("queue_wait_ns").get<std::uint64_t>(), 0U);
@@ -991,7 +902,7 @@ TEST(RenderiveWebGallery, ManualStrategyRunsOnlyExplicitPrepareRefreshRenderCycl
EXPECT_EQ(baseline.at("limit_state"), "not_applicable");
std::this_thread::sleep_for(std::chrono::milliseconds(80));
auto observer = refresh_telemetry(session).at("kernel_observer");
auto observer = observe_telemetry(session).at("kernel_observer");
EXPECT_EQ(observer.at("latest_sequence"), baseline.at("latest_sequence"));
EXPECT_EQ(observer.at("produced_frame_count"), baseline.at("produced_frame_count"));
EXPECT_EQ(observer.at("consumed_frame_count"), baseline.at("consumed_frame_count"));
@@ -1032,7 +943,7 @@ TEST(RenderiveWebGallery, ManualStrategyRunsOnlyExplicitPrepareRefreshRenderCycl
const auto sequence_after_actions = observer.at("latest_sequence");
std::this_thread::sleep_for(std::chrono::milliseconds(80));
EXPECT_EQ(refresh_telemetry(session).at("kernel_observer").at("latest_sequence"),
EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("latest_sequence"),
sequence_after_actions);
}
@@ -1056,7 +967,7 @@ TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggres
const auto frame = session.handle(Frame_Request{});
ASSERT_TRUE(frame.has_value()) << iteration;
ASSERT_EQ(frame->type, Web_Response_Type::Pixels) << iteration;
const auto telemetry = refresh_telemetry(session);
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);
@@ -1067,7 +978,7 @@ TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggres
}
EXPECT_NE(first_pixels, last_pixels);
const auto telemetry = refresh_telemetry(session);
const auto telemetry = observe_telemetry(session);
const auto& observer = telemetry.at("kernel_observer");
EXPECT_EQ(observer.at("target_interval_ns"), 1'000'000);
EXPECT_GT(observer.at("latest_sequence").get<std::uint64_t>(), 4U);
@@ -1111,7 +1022,7 @@ TEST(RenderiveWebGallery, FrequencyLimitCannotBeBypassedByForegroundRescheduleEv
const auto rendered = after - before;
EXPECT_GT(rendered, 0U);
EXPECT_LE(rendered, 16U);
const auto telemetry = refresh_telemetry(session);
const auto telemetry = observe_telemetry(session);
const auto& observer = telemetry.at("kernel_observer");
EXPECT_EQ(observer.at("target_interval_ns"), 33'333'333);
EXPECT_GE(observer.at("next_refresh_interval_ns").get<std::uint64_t>(), 33'333'333U);
@@ -1127,10 +1038,10 @@ TEST(RenderiveWebGallery, ConsumerFeedbackCannotBeBypassedByForegroundReschedule
{"frequency_limit_enabled", false},
{"consumer_feedback_enabled", true}
}).at("type"), "case_state");
send_client_feedback(session,
R"({"category":"event","type":"gallery_feedback","client_metrics":{"transport_fps":25,"presentation_fps":25,"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":40,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})");
auto observed = refresh_telemetry(session);
auto observer = observed.at("kernel_observer");
auto observed = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":25,"presentation_fps":25,"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":40,"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_FALSE(observer.at("frequency_limit_enabled").get<bool>());
EXPECT_TRUE(observer.at("consumer_feedback_enabled").get<bool>());
EXPECT_EQ(observer.at("consumer_interval_ns"), 40'000'000);
@@ -1151,7 +1062,7 @@ TEST(RenderiveWebGallery, ConsumerFeedbackCannotBeBypassedByForegroundReschedule
const auto rendered = after - before;
EXPECT_GT(rendered, 0U);
EXPECT_LE(rendered, 14U);
observed = refresh_telemetry(session);
observed = observe_telemetry(session);
observer = observed.at("kernel_observer");
EXPECT_EQ(observer.at("consumer_interval_ns"), 40'000'000);
EXPECT_GE(observer.at("next_refresh_interval_ns").get<std::uint64_t>(), 40'000'000U);
@@ -1166,11 +1077,11 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000000000}})"))).at("type"),
"case_state");
send_client_feedback(session,
R"({"category":"event","type":"gallery_feedback","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 observed = refresh_telemetry(session);
auto observer = observed.at("kernel_observer");
auto limit = observed.at("low_latency_limit");
auto observed = response_json(session.handle(gallery_request(
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");
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);
@@ -1180,10 +1091,10 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback
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");
send_client_feedback(session,
R"({"category":"event","type":"gallery_feedback","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}})");
observed = refresh_telemetry(session);
observer = observed.at("kernel_observer");
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}})")));
observer = observed.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 22'000'000);
EXPECT_EQ(observer.at("consumer_smoothed_interval_ns"), 21'000'000);
EXPECT_EQ(observer.at("consumer_variation_ns"), 125'000);
@@ -1218,10 +1129,10 @@ TEST(RenderiveWebGallery, ConsumerFeedbackSourcesCanBeSelectedIndependentlyAndMa
{"consumer_presentation_feedback_enabled", false},
{"consumer_manual_feedback_enabled", false}
}).at("type"), "case_state");
send_client_feedback(session,
R"({"category":"event","type":"gallery_feedback","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 observed = refresh_telemetry(session);
auto observer = observed.at("kernel_observer");
auto observed = response_json(session.handle(gallery_request(
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);
@@ -1262,7 +1173,7 @@ TEST(RenderiveWebGallery, PlaybackStrategyQueuesWithoutAutomaticConsumptionAndRe
EXPECT_EQ(baseline.at("limit_state"), "not_applicable");
std::this_thread::sleep_for(std::chrono::milliseconds(80));
auto observer = refresh_telemetry(session).at("kernel_observer");
auto observer = observe_telemetry(session).at("kernel_observer");
EXPECT_EQ(observer.at("latest_sequence"), baseline.at("latest_sequence"));
EXPECT_EQ(observer.at("consumed_frame_count"), baseline.at("consumed_frame_count"));
@@ -1271,7 +1182,7 @@ TEST(RenderiveWebGallery, PlaybackStrategyQueuesWithoutAutomaticConsumptionAndRe
EXPECT_EQ(observer.at("last_event"), "enqueued");
EXPECT_EQ(observer.at("pending_frame_count"), 5);
std::this_thread::sleep_for(std::chrono::milliseconds(20));
EXPECT_EQ(refresh_telemetry(session).at("kernel_observer").at("pending_frame_count"), 5);
EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("pending_frame_count"), 5);
state = invoke_action(session, "mode_dequeue");
observer = state.at("telemetry").at("kernel_observer");
@@ -1298,7 +1209,7 @@ TEST(RenderiveWebGallery, PlaybackStrategyQueuesWithoutAutomaticConsumptionAndRe
const auto sequence_after_actions = observer.at("latest_sequence");
std::this_thread::sleep_for(std::chrono::milliseconds(80));
EXPECT_EQ(refresh_telemetry(session).at("kernel_observer").at("latest_sequence"),
EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("latest_sequence"),
sequence_after_actions);
}
@@ -1335,9 +1246,11 @@ TEST(RenderiveWebGallery, ProductionLowLatencySessionRendersWithoutPixelPulls) {
ASSERT_TRUE(wait_for_condition([&session] {
return successful_render_count(session) >= 4;
}));
send_client_feedback(session,
R"({"category":"event","type":"gallery_feedback","client_metrics":{"transport_fps":120,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":17,"duplicate_pixel_frames":3,"frame_request_timeout_count":2,"frame_round_trip_ms":4.5,"display_interval_ms":16.7,"display_interval_latest_ms":16.9,"display_interval_p95_ms":17.4,"display_jitter_ms":0.7,"overwritten_pixel_frames":5,"last_pixel_receive_age_ms":8.5,"last_pixel_change_age_ms":12.5}})");
const auto telemetry = refresh_telemetry(session);
const auto observed = session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":120,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":17,"duplicate_pixel_frames":3,"frame_request_timeout_count":2,"frame_round_trip_ms":4.5,"display_interval_ms":16.7,"display_interval_latest_ms":16.9,"display_interval_p95_ms":17.4,"display_jitter_ms":0.7,"overwritten_pixel_frames":5,"last_pixel_receive_age_ms":8.5,"last_pixel_change_age_ms":12.5}})"));
ASSERT_TRUE(observed.has_value());
const auto telemetry = parse_json(observed->payload).at("telemetry");
const auto& performance = telemetry.at("performance");
EXPECT_TRUE(performance.at("automatic_low_latency_scheduler").get<bool>());
EXPECT_GE(performance.at("successful_render_count").get<std::uint64_t>(), 4U);
@@ -1449,8 +1362,8 @@ void expect_low_latency_canvas_to_change(std::string_view case_id) {
EXPECT_NE(first->payload, second->payload);
const auto observed = session.handle(gallery_request(
Gallery_Request_Kind::Refresh,
R"({"category":"event","type":"gallery_refresh"})"));
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"));
ASSERT_TRUE(observed.has_value());
const auto telemetry = parse_json(observed->payload).at("telemetry");
EXPECT_GT(telemetry.at("frame_index").get<std::uint64_t>(), 2U);
@@ -1525,8 +1438,8 @@ TEST(RenderiveWebGallery, BuilderOnlyControlsRebuildAndAllImageModesRender) {
ASSERT_TRUE(palette.has_value());
ASSERT_TRUE(waterfall.handle(Frame_Request{}).has_value());
const auto waterfall_observer = waterfall.handle(gallery_request(
Gallery_Request_Kind::Refresh,
R"({"category":"event","type":"gallery_refresh"})"));
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"));
ASSERT_TRUE(waterfall_observer.has_value());
const auto waterfall_telemetry = parse_json(waterfall_observer->payload).at("telemetry");
EXPECT_GT(waterfall_telemetry.at("waterfall").at("row_count").get<std::size_t>(), 0U);