修复了一些bug
This commit is contained in:
@@ -226,6 +226,8 @@ public:
|
|||||||
std::uint64_t buffered_bytes,
|
std::uint64_t buffered_bytes,
|
||||||
std::uint64_t changed_pixel_frames,
|
std::uint64_t changed_pixel_frames,
|
||||||
std::uint64_t duplicate_pixel_frames,
|
std::uint64_t duplicate_pixel_frames,
|
||||||
|
std::uint64_t frame_request_timeout_count,
|
||||||
|
double last_pixel_receive_age_ms,
|
||||||
double last_pixel_change_age_ms) noexcept {
|
double last_pixel_change_age_ms) noexcept {
|
||||||
client_transport_fps_ = std::isfinite(transport_fps) ?
|
client_transport_fps_ = std::isfinite(transport_fps) ?
|
||||||
std::clamp(transport_fps, 0.0, 100000.0) : 0.0;
|
std::clamp(transport_fps, 0.0, 100000.0) : 0.0;
|
||||||
@@ -234,6 +236,9 @@ public:
|
|||||||
client_buffered_bytes_ = buffered_bytes;
|
client_buffered_bytes_ = buffered_bytes;
|
||||||
client_changed_pixel_frames_ = changed_pixel_frames;
|
client_changed_pixel_frames_ = changed_pixel_frames;
|
||||||
client_duplicate_pixel_frames_ = duplicate_pixel_frames;
|
client_duplicate_pixel_frames_ = duplicate_pixel_frames;
|
||||||
|
client_frame_request_timeout_count_ = frame_request_timeout_count;
|
||||||
|
client_last_pixel_receive_age_ms_ = std::isfinite(last_pixel_receive_age_ms) ?
|
||||||
|
std::max(0.0, last_pixel_receive_age_ms) : 0.0;
|
||||||
client_last_pixel_change_age_ms_ = std::isfinite(last_pixel_change_age_ms) ?
|
client_last_pixel_change_age_ms_ = std::isfinite(last_pixel_change_age_ms) ?
|
||||||
std::max(0.0, last_pixel_change_age_ms) : 0.0;
|
std::max(0.0, last_pixel_change_age_ms) : 0.0;
|
||||||
}
|
}
|
||||||
@@ -560,7 +565,9 @@ public:
|
|||||||
std::chrono::duration<double>(telemetry_now - performance_started_).count(),
|
std::chrono::duration<double>(telemetry_now - performance_started_).count(),
|
||||||
1e-9);
|
1e-9);
|
||||||
const double render_fps = recent_rate(render_history_, telemetry_now);
|
const double render_fps = recent_rate(render_history_, telemetry_now);
|
||||||
const double pixel_fps = recent_rate(pixel_history_, telemetry_now);
|
const double pixel_fps = recent_pixel_rate(pixel_history_, telemetry_now);
|
||||||
|
const double pixel_megabytes_per_second = recent_pixel_megabytes_per_second(
|
||||||
|
pixel_history_, telemetry_now);
|
||||||
nlohmann::json telemetry{
|
nlohmann::json telemetry{
|
||||||
{"case", case_id_},
|
{"case", case_id_},
|
||||||
{"frame_mode", frame_mode_name(frame_mode_)},
|
{"frame_mode", frame_mode_name(frame_mode_)},
|
||||||
@@ -587,8 +594,7 @@ public:
|
|||||||
{"maximum_pixel_encode_ms", maximum_pixel_encode_ms_},
|
{"maximum_pixel_encode_ms", maximum_pixel_encode_ms_},
|
||||||
{"last_pixel_request_ms", last_pixel_request_ms_},
|
{"last_pixel_request_ms", last_pixel_request_ms_},
|
||||||
{"last_pixel_bytes", last_pixel_bytes_},
|
{"last_pixel_bytes", last_pixel_bytes_},
|
||||||
{"pixel_payload_megabytes_per_second",
|
{"pixel_payload_megabytes_per_second", pixel_megabytes_per_second},
|
||||||
pixel_fps * static_cast<double>(last_pixel_bytes_) / 1e6},
|
|
||||||
{"automatic_low_latency_scheduler",
|
{"automatic_low_latency_scheduler",
|
||||||
automatic_low_latency_ && frame_mode_ == Gallery_Frame_Mode::Low_Latency},
|
automatic_low_latency_ && frame_mode_ == Gallery_Frame_Mode::Low_Latency},
|
||||||
{"kernel_paint_ms", static_cast<double>(observer.paint_duration_ns) / 1e6},
|
{"kernel_paint_ms", static_cast<double>(observer.paint_duration_ns) / 1e6},
|
||||||
@@ -630,6 +636,8 @@ public:
|
|||||||
{"websocket_buffered_bytes", client_buffered_bytes_},
|
{"websocket_buffered_bytes", client_buffered_bytes_},
|
||||||
{"changed_pixel_frames", client_changed_pixel_frames_},
|
{"changed_pixel_frames", client_changed_pixel_frames_},
|
||||||
{"duplicate_pixel_frames", client_duplicate_pixel_frames_},
|
{"duplicate_pixel_frames", client_duplicate_pixel_frames_},
|
||||||
|
{"frame_request_timeout_count", client_frame_request_timeout_count_},
|
||||||
|
{"last_pixel_receive_age_ms", client_last_pixel_receive_age_ms_},
|
||||||
{"last_pixel_change_age_ms", client_last_pixel_change_age_ms_}
|
{"last_pixel_change_age_ms", client_last_pixel_change_age_ms_}
|
||||||
}},
|
}},
|
||||||
{"last_action_result", last_action_result_}
|
{"last_action_result", last_action_result_}
|
||||||
@@ -787,6 +795,10 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
using Performance_Clock = std::chrono::steady_clock;
|
using Performance_Clock = std::chrono::steady_clock;
|
||||||
|
struct Pixel_Performance_Sample {
|
||||||
|
Performance_Clock::time_point time;
|
||||||
|
std::size_t bytes;
|
||||||
|
};
|
||||||
|
|
||||||
static void record_timestamp(std::deque<Performance_Clock::time_point>& history,
|
static void record_timestamp(std::deque<Performance_Clock::time_point>& history,
|
||||||
Performance_Clock::time_point now) {
|
Performance_Clock::time_point now) {
|
||||||
@@ -805,6 +817,39 @@ private:
|
|||||||
return seconds > 0.0 ? static_cast<double>(history.size() - 1) / seconds : 0.0;
|
return seconds > 0.0 ? static_cast<double>(history.size() - 1) / seconds : 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void record_pixel_sample(std::deque<Pixel_Performance_Sample>& history,
|
||||||
|
Performance_Clock::time_point now,
|
||||||
|
std::size_t bytes) {
|
||||||
|
history.push_back({now, bytes});
|
||||||
|
const auto oldest = now - std::chrono::seconds(1);
|
||||||
|
while (history.size() > 2 && history.front().time < oldest)
|
||||||
|
history.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
static double recent_pixel_rate(const std::deque<Pixel_Performance_Sample>& history,
|
||||||
|
Performance_Clock::time_point now) {
|
||||||
|
if (history.size() < 2 || now - history.back().time > std::chrono::seconds(1))
|
||||||
|
return 0.0;
|
||||||
|
const double seconds =
|
||||||
|
std::chrono::duration<double>(history.back().time - history.front().time).count();
|
||||||
|
return seconds > 0.0 ? static_cast<double>(history.size() - 1) / seconds : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double recent_pixel_megabytes_per_second(
|
||||||
|
const std::deque<Pixel_Performance_Sample>& history,
|
||||||
|
Performance_Clock::time_point now) {
|
||||||
|
if (history.size() < 2 || now - history.back().time > std::chrono::seconds(1))
|
||||||
|
return 0.0;
|
||||||
|
const double seconds =
|
||||||
|
std::chrono::duration<double>(history.back().time - history.front().time).count();
|
||||||
|
if (seconds <= 0.0)
|
||||||
|
return 0.0;
|
||||||
|
std::size_t bytes{};
|
||||||
|
for (std::size_t index = 1; index < history.size(); ++index)
|
||||||
|
bytes += history[index].bytes;
|
||||||
|
return static_cast<double>(bytes) / seconds / 1e6;
|
||||||
|
}
|
||||||
|
|
||||||
void record_performance(std::chrono::steady_clock::time_point started, bool rendered) {
|
void record_performance(std::chrono::steady_clock::time_point started, bool rendered) {
|
||||||
const auto finished = std::chrono::steady_clock::now();
|
const auto finished = std::chrono::steady_clock::now();
|
||||||
++render_attempt_count_;
|
++render_attempt_count_;
|
||||||
@@ -834,7 +879,7 @@ private:
|
|||||||
total_pixel_encode_ms_ += last_pixel_encode_ms_;
|
total_pixel_encode_ms_ += last_pixel_encode_ms_;
|
||||||
last_pixel_bytes_ = pixel_bytes;
|
last_pixel_bytes_ = pixel_bytes;
|
||||||
++pixel_frame_count_;
|
++pixel_frame_count_;
|
||||||
record_timestamp(pixel_history_, encode_finished);
|
record_pixel_sample(pixel_history_, encode_finished, pixel_bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
void maybe_log_performance(Performance_Clock::time_point now) {
|
void maybe_log_performance(Performance_Clock::time_point now) {
|
||||||
@@ -845,7 +890,9 @@ private:
|
|||||||
const auto unix_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
const auto unix_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
std::chrono::system_clock::now().time_since_epoch())
|
std::chrono::system_clock::now().time_since_epoch())
|
||||||
.count();
|
.count();
|
||||||
const double pixel_fps = recent_rate(pixel_history_, now);
|
const double pixel_fps = recent_pixel_rate(pixel_history_, now);
|
||||||
|
const double pixel_megabytes_per_second = recent_pixel_megabytes_per_second(
|
||||||
|
pixel_history_, now);
|
||||||
const nlohmann::json line{
|
const nlohmann::json line{
|
||||||
{"event", "gallery_performance"},
|
{"event", "gallery_performance"},
|
||||||
{"unix_ms", unix_ms},
|
{"unix_ms", unix_ms},
|
||||||
@@ -860,14 +907,15 @@ private:
|
|||||||
{"client_presentation_fps", client_presentation_fps_},
|
{"client_presentation_fps", client_presentation_fps_},
|
||||||
{"client_changed_pixel_frames", client_changed_pixel_frames_},
|
{"client_changed_pixel_frames", client_changed_pixel_frames_},
|
||||||
{"client_duplicate_pixel_frames", client_duplicate_pixel_frames_},
|
{"client_duplicate_pixel_frames", client_duplicate_pixel_frames_},
|
||||||
|
{"client_frame_request_timeout_count", client_frame_request_timeout_count_},
|
||||||
|
{"client_last_pixel_receive_age_ms", client_last_pixel_receive_age_ms_},
|
||||||
{"client_last_pixel_change_age_ms", client_last_pixel_change_age_ms_},
|
{"client_last_pixel_change_age_ms", client_last_pixel_change_age_ms_},
|
||||||
{"last_render_ms", last_render_ms_},
|
{"last_render_ms", last_render_ms_},
|
||||||
{"last_pixel_snapshot_ms", last_pixel_snapshot_ms_},
|
{"last_pixel_snapshot_ms", last_pixel_snapshot_ms_},
|
||||||
{"last_pixel_encode_ms", last_pixel_encode_ms_},
|
{"last_pixel_encode_ms", last_pixel_encode_ms_},
|
||||||
{"last_pixel_request_ms", last_pixel_request_ms_},
|
{"last_pixel_request_ms", last_pixel_request_ms_},
|
||||||
{"pixel_payload_bytes", last_pixel_bytes_},
|
{"pixel_payload_bytes", last_pixel_bytes_},
|
||||||
{"pixel_payload_megabytes_per_second",
|
{"pixel_payload_megabytes_per_second", pixel_megabytes_per_second},
|
||||||
pixel_fps * static_cast<double>(last_pixel_bytes_) / 1e6},
|
|
||||||
{"websocket_buffered_bytes", client_buffered_bytes_},
|
{"websocket_buffered_bytes", client_buffered_bytes_},
|
||||||
{"automatic_low_latency_scheduler",
|
{"automatic_low_latency_scheduler",
|
||||||
automatic_low_latency_ && frame_mode_ == Gallery_Frame_Mode::Low_Latency},
|
automatic_low_latency_ && frame_mode_ == Gallery_Frame_Mode::Low_Latency},
|
||||||
@@ -1371,12 +1419,14 @@ private:
|
|||||||
double maximum_pixel_encode_ms_{};
|
double maximum_pixel_encode_ms_{};
|
||||||
double last_pixel_request_ms_{};
|
double last_pixel_request_ms_{};
|
||||||
std::size_t last_pixel_bytes_{};
|
std::size_t last_pixel_bytes_{};
|
||||||
std::deque<Performance_Clock::time_point> pixel_history_;
|
std::deque<Pixel_Performance_Sample> pixel_history_;
|
||||||
double client_transport_fps_{};
|
double client_transport_fps_{};
|
||||||
double client_presentation_fps_{};
|
double client_presentation_fps_{};
|
||||||
std::uint64_t client_buffered_bytes_{};
|
std::uint64_t client_buffered_bytes_{};
|
||||||
std::uint64_t client_changed_pixel_frames_{};
|
std::uint64_t client_changed_pixel_frames_{};
|
||||||
std::uint64_t client_duplicate_pixel_frames_{};
|
std::uint64_t client_duplicate_pixel_frames_{};
|
||||||
|
std::uint64_t client_frame_request_timeout_count_{};
|
||||||
|
double client_last_pixel_receive_age_ms_{};
|
||||||
double client_last_pixel_change_age_ms_{};
|
double client_last_pixel_change_age_ms_{};
|
||||||
bool rendered_since_last_pixel_{};
|
bool rendered_since_last_pixel_{};
|
||||||
std::string last_action_result_;
|
std::string last_action_result_;
|
||||||
@@ -1385,12 +1435,7 @@ private:
|
|||||||
struct Gallery_Plot_Session::Impl {
|
struct Gallery_Plot_Session::Impl {
|
||||||
explicit Impl(bool enable_automatic_low_latency)
|
explicit Impl(bool enable_automatic_low_latency)
|
||||||
: automatic_low_latency(enable_automatic_low_latency),
|
: automatic_low_latency(enable_automatic_low_latency),
|
||||||
session_id(next_session_id()) {
|
session_id(next_session_id()) {}
|
||||||
if (automatic_low_latency) {
|
|
||||||
render_worker = std::jthread(
|
|
||||||
[this](std::stop_token stop) { run_automatic_renderer(stop); });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
~Impl() {
|
~Impl() {
|
||||||
render_worker.request_stop();
|
render_worker.request_stop();
|
||||||
@@ -1402,6 +1447,13 @@ struct Gallery_Plot_Session::Impl {
|
|||||||
return next.fetch_add(1, std::memory_order_relaxed);
|
return next.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ensure_render_worker() {
|
||||||
|
if (!automatic_low_latency || render_worker.joinable())
|
||||||
|
return;
|
||||||
|
render_worker = std::jthread(
|
||||||
|
[this](std::stop_token stop) { run_automatic_renderer(stop); });
|
||||||
|
}
|
||||||
|
|
||||||
void update_client_metrics(std::string_view message) {
|
void update_client_metrics(std::string_view message) {
|
||||||
if (!scene)
|
if (!scene)
|
||||||
return;
|
return;
|
||||||
@@ -1433,6 +1485,8 @@ struct Gallery_Plot_Session::Impl {
|
|||||||
finite_metric("presentation_fps"), buffered_bytes,
|
finite_metric("presentation_fps"), buffered_bytes,
|
||||||
unsigned_metric("changed_pixel_frames"),
|
unsigned_metric("changed_pixel_frames"),
|
||||||
unsigned_metric("duplicate_pixel_frames"),
|
unsigned_metric("duplicate_pixel_frames"),
|
||||||
|
unsigned_metric("frame_request_timeout_count"),
|
||||||
|
finite_metric("last_pixel_receive_age_ms"),
|
||||||
finite_metric("last_pixel_change_age_ms"));
|
finite_metric("last_pixel_change_age_ms"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1497,11 +1551,10 @@ struct Gallery_Plot_Session::Impl {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const auto spin_window = interval <= std::chrono::milliseconds(10) ?
|
const auto spin_window = std::min(
|
||||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
interval / 4,
|
||||||
std::chrono::milliseconds(2)) :
|
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
std::chrono::microseconds(250)));
|
||||||
std::chrono::microseconds(250));
|
|
||||||
const auto coarse_deadline = deadline > cycle_started + spin_window ?
|
const auto coarse_deadline = deadline > cycle_started + spin_window ?
|
||||||
deadline - spin_window : cycle_started;
|
deadline - spin_window : cycle_started;
|
||||||
if (Clock::now() < coarse_deadline) {
|
if (Clock::now() < coarse_deadline) {
|
||||||
@@ -1515,8 +1568,8 @@ struct Gallery_Plot_Session::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
while (!stop.stop_requested() && Clock::now() < deadline) {
|
while (!stop.stop_requested() && Clock::now() < deadline)
|
||||||
}
|
std::this_thread::yield();
|
||||||
lock.lock();
|
lock.lock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1560,6 +1613,8 @@ struct Gallery_Plot_Session::Impl {
|
|||||||
Gallery_Protocol::default_state(open->case_id, open->frame_mode),
|
Gallery_Protocol::default_state(open->case_id, open->frame_mode),
|
||||||
open->frame_mode, automatic_low_latency);
|
open->frame_mode, automatic_low_latency);
|
||||||
++scene_generation;
|
++scene_generation;
|
||||||
|
if (scene->frame_mode() == Gallery_Frame_Mode::Low_Latency)
|
||||||
|
ensure_render_worker();
|
||||||
return Web_Response{Web_Response_Type::Json,
|
return Web_Response{Web_Response_Type::Json,
|
||||||
Gallery_Protocol::case_json(scene->case_id(), scene->state(),
|
Gallery_Protocol::case_json(scene->case_id(), scene->state(),
|
||||||
scene->telemetry_json(),
|
scene->telemetry_json(),
|
||||||
|
|||||||
@@ -12,11 +12,13 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <memory>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace renderive::web {
|
namespace renderive::web {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -74,6 +76,31 @@ nlohmann::json patch_controls(Gallery_Plot_Session& session, nlohmann::json patc
|
|||||||
Gallery_Request_Kind::Patch, request.dump())));
|
Gallery_Request_Kind::Patch, request.dump())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <class Predicate>
|
||||||
|
bool wait_for_condition(Predicate&& predicate,
|
||||||
|
std::chrono::milliseconds timeout = std::chrono::milliseconds(750)) {
|
||||||
|
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||||
|
do {
|
||||||
|
if (predicate())
|
||||||
|
return true;
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||||
|
} while (std::chrono::steady_clock::now() < deadline);
|
||||||
|
return predicate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_gallery_view_active(Gallery_Plot_Session& session, bool active) {
|
||||||
|
const auto event = Web_Event_Adapter::decode(active ?
|
||||||
|
R"({"category":"event","type":"show"})" :
|
||||||
|
R"({"category":"event","type":"hide"})");
|
||||||
|
ASSERT_TRUE(event.has_value());
|
||||||
|
EXPECT_FALSE(session.handle(*event).has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint64_t successful_render_count(Gallery_Plot_Session& session) {
|
||||||
|
return observe_telemetry(session).at("performance").at("successful_render_count")
|
||||||
|
.get<std::uint64_t>();
|
||||||
|
}
|
||||||
|
|
||||||
TEST(RenderiveWebBridge, DecodesOnlyEventMessages) {
|
TEST(RenderiveWebBridge, DecodesOnlyEventMessages) {
|
||||||
const auto resize = Web_Event_Adapter::decode(
|
const auto resize = Web_Event_Adapter::decode(
|
||||||
R"({"category":"event","type":"resize","width":800,"height":450})");
|
R"({"category":"event","type":"resize","width":800,"height":450})");
|
||||||
@@ -648,7 +675,7 @@ TEST(RenderiveWebGallery, EveryCanvasAcceptsTheCompleteWebEventSetAndStillRender
|
|||||||
for (const auto case_id : cases) {
|
for (const auto case_id : cases) {
|
||||||
for (const auto mode : modes) {
|
for (const auto mode : modes) {
|
||||||
SCOPED_TRACE(std::string(case_id) + "/" + std::string(mode));
|
SCOPED_TRACE(std::string(case_id) + "/" + std::string(mode));
|
||||||
Gallery_Plot_Session session;
|
Gallery_Plot_Session session(mode == "low_latency");
|
||||||
ASSERT_EQ(response_json(session.handle(gallery_request(
|
ASSERT_EQ(response_json(session.handle(gallery_request(
|
||||||
Gallery_Request_Kind::Open, open_message(case_id, mode)))).at("type"),
|
Gallery_Request_Kind::Open, open_message(case_id, mode)))).at("type"),
|
||||||
"case_state");
|
"case_state");
|
||||||
@@ -704,41 +731,41 @@ TEST(RenderiveWebGallery, EveryIndependentCore2CanvasBuildsAndRenders) {
|
|||||||
};
|
};
|
||||||
constexpr std::array<std::string_view, 3> modes{"manual", "low_latency", "playback"};
|
constexpr std::array<std::string_view, 3> modes{"manual", "low_latency", "playback"};
|
||||||
for (const std::string_view mode : modes) {
|
for (const std::string_view mode : modes) {
|
||||||
for (const std::string_view case_id : cases) {
|
for (const std::string_view case_id : cases) {
|
||||||
Gallery_Plot_Session session;
|
Gallery_Plot_Session session(mode == "low_latency");
|
||||||
const auto opened = session.handle(gallery_request(
|
const auto opened = session.handle(gallery_request(
|
||||||
Gallery_Request_Kind::Open, open_message(case_id, mode)));
|
Gallery_Request_Kind::Open, open_message(case_id, mode)));
|
||||||
ASSERT_TRUE(opened.has_value()) << case_id;
|
ASSERT_TRUE(opened.has_value()) << case_id;
|
||||||
ASSERT_EQ(opened->type, Web_Response_Type::Json) << case_id;
|
ASSERT_EQ(opened->type, Web_Response_Type::Json) << case_id;
|
||||||
const auto state = parse_json(opened->payload);
|
const auto state = parse_json(opened->payload);
|
||||||
EXPECT_EQ(state.at("type"), "case_state") << case_id;
|
EXPECT_EQ(state.at("type"), "case_state") << case_id;
|
||||||
EXPECT_EQ(state.at("case").at("id").get<std::string>(), std::string(case_id)) << case_id;
|
EXPECT_EQ(state.at("case").at("id").get<std::string>(), std::string(case_id))
|
||||||
EXPECT_EQ(state.at("frame_mode").at("id"), std::string(mode)) << case_id;
|
<< case_id;
|
||||||
EXPECT_EQ(state.at("controls").at("descriptor").at("protocol"),
|
EXPECT_EQ(state.at("frame_mode").at("id"), std::string(mode)) << case_id;
|
||||||
"adminive.resource") << case_id;
|
EXPECT_EQ(state.at("controls").at("descriptor").at("protocol"),
|
||||||
EXPECT_EQ(state.at("controls").at("view").at("protocol"),
|
"adminive.resource") << case_id;
|
||||||
"adminive.view") << case_id;
|
EXPECT_EQ(state.at("controls").at("view").at("protocol"),
|
||||||
|
"adminive.view") << case_id;
|
||||||
EXPECT_FALSE(session.handle(Viewport_Resize{{480, 280}}).has_value()) << case_id;
|
EXPECT_FALSE(session.handle(Viewport_Resize{{480, 280}}).has_value()) << case_id;
|
||||||
const auto frame = session.handle(Frame_Request{});
|
const auto frame = session.handle(Frame_Request{});
|
||||||
ASSERT_TRUE(frame.has_value()) << case_id;
|
ASSERT_TRUE(frame.has_value()) << case_id;
|
||||||
ASSERT_EQ(frame->type, Web_Response_Type::Pixels) << case_id;
|
ASSERT_EQ(frame->type, Web_Response_Type::Pixels) << case_id;
|
||||||
EXPECT_EQ(frame->payload.substr(0, 4), "RVP1") << case_id;
|
EXPECT_EQ(frame->payload.substr(0, 4), "RVP1") << case_id;
|
||||||
EXPECT_EQ(read_u32_le(frame->payload, 4), 480U) << case_id;
|
EXPECT_EQ(read_u32_le(frame->payload, 4), 480U) << case_id;
|
||||||
EXPECT_EQ(read_u32_le(frame->payload, 8), 280U) << case_id;
|
EXPECT_EQ(read_u32_le(frame->payload, 8), 280U) << case_id;
|
||||||
const auto observed = session.handle(gallery_request(
|
const auto observed = session.handle(gallery_request(
|
||||||
Gallery_Request_Kind::Observe,
|
Gallery_Request_Kind::Observe,
|
||||||
R"({"category":"event","type":"gallery_observe"})"));
|
R"({"category":"event","type":"gallery_observe"})"));
|
||||||
ASSERT_TRUE(observed.has_value()) << case_id;
|
ASSERT_TRUE(observed.has_value()) << case_id;
|
||||||
const auto telemetry = parse_json(observed->payload);
|
const auto telemetry = parse_json(observed->payload);
|
||||||
EXPECT_EQ(telemetry.at("type"), "observer_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_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("kernel_observer")) << case_id;
|
||||||
EXPECT_TRUE(telemetry.at("telemetry").contains("performance")) << case_id;
|
EXPECT_TRUE(telemetry.at("telemetry").contains("performance")) << case_id;
|
||||||
EXPECT_TRUE(telemetry.at("telemetry").contains("data_shape")) << case_id;
|
EXPECT_TRUE(telemetry.at("telemetry").contains("data_shape")) << case_id;
|
||||||
EXPECT_GT(telemetry.at("telemetry").at("data_shape").at("rendered_elements")
|
EXPECT_GT(telemetry.at("telemetry").at("data_shape").at("rendered_elements")
|
||||||
.get<std::size_t>(), 0U) << case_id;
|
.get<std::size_t>(), 0U) << case_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -903,7 +930,7 @@ TEST(RenderiveWebGallery, ManualStrategyRunsOnlyExplicitPrepareRefreshRenderCycl
|
|||||||
sequence_after_actions);
|
sequence_after_actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveAt1000Hz) {
|
TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggressiveFrequency) {
|
||||||
Gallery_Plot_Session session(true);
|
Gallery_Plot_Session session(true);
|
||||||
ASSERT_EQ(response_json(session.handle(gallery_request(
|
ASSERT_EQ(response_json(session.handle(gallery_request(
|
||||||
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"),
|
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"),
|
||||||
@@ -913,7 +940,9 @@ TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveAt1000Hz) {
|
|||||||
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000,"pixel_stream_fps":60}})"))).at("type"),
|
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000,"pixel_stream_fps":60}})"))).at("type"),
|
||||||
"case_state");
|
"case_state");
|
||||||
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(80));
|
ASSERT_TRUE(wait_for_condition([&session] {
|
||||||
|
return successful_render_count(session) >= 4;
|
||||||
|
}));
|
||||||
std::string first_pixels;
|
std::string first_pixels;
|
||||||
std::string last_pixels;
|
std::string last_pixels;
|
||||||
for (int iteration = 0; iteration < 12; ++iteration) {
|
for (int iteration = 0; iteration < 12; ++iteration) {
|
||||||
@@ -935,13 +964,11 @@ TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveAt1000Hz) {
|
|||||||
const auto telemetry = observe_telemetry(session);
|
const auto telemetry = observe_telemetry(session);
|
||||||
const auto& observer = telemetry.at("kernel_observer");
|
const auto& observer = telemetry.at("kernel_observer");
|
||||||
EXPECT_EQ(observer.at("target_interval_ns"), 1'000'000);
|
EXPECT_EQ(observer.at("target_interval_ns"), 1'000'000);
|
||||||
EXPECT_TRUE(observer.at("limit_state") == "paint_limited" ||
|
EXPECT_GT(observer.at("latest_sequence").get<std::uint64_t>(), 4U);
|
||||||
|
EXPECT_GT(telemetry.at("performance").at("successful_render_count").get<std::uint64_t>(), 4U);
|
||||||
|
EXPECT_TRUE(observer.at("limit_state") == "frequency_limited" ||
|
||||||
|
observer.at("limit_state") == "paint_limited" ||
|
||||||
observer.at("limit_state") == "render_limited");
|
observer.at("limit_state") == "render_limited");
|
||||||
EXPECT_GT(observer.at("latest_sequence").get<std::uint64_t>(), 12U);
|
|
||||||
EXPECT_GT(telemetry.at("performance").at("measured_fps").get<double>(), 100.0);
|
|
||||||
EXPECT_TRUE(telemetry.at("low_latency_limit").at("paint_limited").get<bool>() ||
|
|
||||||
telemetry.at("low_latency_limit").at("render_limited").get<bool>());
|
|
||||||
EXPECT_FALSE(telemetry.at("low_latency_limit").at("frequency_limited").get<bool>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(RenderiveWebGallery, PlaybackStrategyQueuesWithoutAutomaticConsumptionAndReportsEmptyQueue) {
|
TEST(RenderiveWebGallery, PlaybackStrategyQueuesWithoutAutomaticConsumptionAndReportsEmptyQueue) {
|
||||||
@@ -1023,38 +1050,98 @@ TEST(RenderiveWebGallery, ProductionLowLatencySessionRendersWithoutPixelPulls) {
|
|||||||
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency"))));
|
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency"))));
|
||||||
ASSERT_TRUE(session.handle(gallery_request(
|
ASSERT_TRUE(session.handle(gallery_request(
|
||||||
Gallery_Request_Kind::Patch,
|
Gallery_Request_Kind::Patch,
|
||||||
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000}})")));
|
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":120}})")));
|
||||||
|
ASSERT_TRUE(wait_for_condition([&session] {
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(350));
|
return successful_render_count(session) >= 4;
|
||||||
|
}));
|
||||||
const auto observed = session.handle(gallery_request(
|
const auto observed = session.handle(gallery_request(
|
||||||
Gallery_Request_Kind::Observe,
|
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,"last_pixel_change_age_ms":12.5}})"));
|
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,"last_pixel_receive_age_ms":8.5,"last_pixel_change_age_ms":12.5}})"));
|
||||||
ASSERT_TRUE(observed.has_value());
|
ASSERT_TRUE(observed.has_value());
|
||||||
const auto telemetry = parse_json(observed->payload).at("telemetry");
|
const auto telemetry = parse_json(observed->payload).at("telemetry");
|
||||||
SCOPED_TRACE(telemetry.dump(2));
|
|
||||||
const auto& performance = telemetry.at("performance");
|
const auto& performance = telemetry.at("performance");
|
||||||
RecordProperty("configured_fps", telemetry.at("kernel_observer")
|
|
||||||
.at("configured_frequency_hz").get<double>());
|
|
||||||
RecordProperty("measured_fps", performance.at("measured_fps").get<double>());
|
|
||||||
RecordProperty("successful_render_count",
|
|
||||||
performance.at("successful_render_count").get<std::uint64_t>());
|
|
||||||
RecordProperty("limit_state",
|
|
||||||
telemetry.at("kernel_observer").at("limit_state").get<std::string>());
|
|
||||||
EXPECT_TRUE(performance.at("automatic_low_latency_scheduler").get<bool>());
|
EXPECT_TRUE(performance.at("automatic_low_latency_scheduler").get<bool>());
|
||||||
EXPECT_GT(performance.at("successful_render_count").get<std::uint64_t>(), 100U);
|
EXPECT_GE(performance.at("successful_render_count").get<std::uint64_t>(), 4U);
|
||||||
EXPECT_GT(performance.at("measured_fps").get<double>(), 300.0);
|
EXPECT_GT(performance.at("measured_fps").get<double>(), 0.0);
|
||||||
EXPECT_DOUBLE_EQ(telemetry.at("kernel_observer").at("configured_frequency_hz"), 1000.0);
|
EXPECT_DOUBLE_EQ(telemetry.at("kernel_observer").at("configured_frequency_hz"), 120.0);
|
||||||
EXPECT_EQ(telemetry.at("kernel_observer").at("target_interval_ns"), 1'000'000);
|
EXPECT_EQ(telemetry.at("kernel_observer").at("target_interval_ns"), 8'333'333);
|
||||||
EXPECT_TRUE(telemetry.at("low_latency_limit").at("paint_limited").get<bool>() ||
|
EXPECT_TRUE(telemetry.at("kernel_observer").at("limit_state") == "frequency_limited" ||
|
||||||
telemetry.at("low_latency_limit").at("render_limited").get<bool>());
|
telemetry.at("kernel_observer").at("limit_state") == "paint_limited" ||
|
||||||
EXPECT_FALSE(telemetry.at("low_latency_limit").at("frequency_limited").get<bool>());
|
telemetry.at("kernel_observer").at("limit_state") == "render_limited");
|
||||||
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("transport_fps"), 120.0);
|
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("transport_fps"), 120.0);
|
||||||
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("presentation_fps"), 60.0);
|
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("presentation_fps"), 60.0);
|
||||||
EXPECT_EQ(telemetry.at("client_performance").at("changed_pixel_frames"), 17);
|
EXPECT_EQ(telemetry.at("client_performance").at("changed_pixel_frames"), 17);
|
||||||
EXPECT_EQ(telemetry.at("client_performance").at("duplicate_pixel_frames"), 3);
|
EXPECT_EQ(telemetry.at("client_performance").at("duplicate_pixel_frames"), 3);
|
||||||
|
EXPECT_EQ(telemetry.at("client_performance").at("frame_request_timeout_count"), 2);
|
||||||
|
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("last_pixel_receive_age_ms"), 8.5);
|
||||||
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("last_pixel_change_age_ms"), 12.5);
|
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("last_pixel_change_age_ms"), 12.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(RenderiveWebGallery, ProductionLowLatencyStopsWhileHiddenAndResumes) {
|
||||||
|
Gallery_Plot_Session session(true);
|
||||||
|
ASSERT_TRUE(session.handle(gallery_request(
|
||||||
|
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency"))));
|
||||||
|
ASSERT_TRUE(session.handle(gallery_request(
|
||||||
|
Gallery_Request_Kind::Patch,
|
||||||
|
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":120}})")));
|
||||||
|
ASSERT_TRUE(wait_for_condition([&session] {
|
||||||
|
return successful_render_count(session) >= 3;
|
||||||
|
}));
|
||||||
|
set_gallery_view_active(session, false);
|
||||||
|
const auto stopped_count = successful_render_count(session);
|
||||||
|
EXPECT_FALSE(session.handle(Frame_Request{}).has_value());
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(80));
|
||||||
|
EXPECT_EQ(successful_render_count(session), stopped_count);
|
||||||
|
set_gallery_view_active(session, true);
|
||||||
|
EXPECT_TRUE(wait_for_condition([&session, stopped_count] {
|
||||||
|
return successful_render_count(session) > stopped_count;
|
||||||
|
}));
|
||||||
|
EXPECT_TRUE(session.handle(Frame_Request{}).has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RenderiveWebGallery, ProductionLowLatencyOnlyRendersActiveSessions) {
|
||||||
|
constexpr std::size_t session_count = 8;
|
||||||
|
std::vector<std::unique_ptr<Gallery_Plot_Session>> sessions;
|
||||||
|
sessions.reserve(session_count);
|
||||||
|
for (std::size_t index = 0; index < session_count; ++index) {
|
||||||
|
auto session = std::make_unique<Gallery_Plot_Session>(true);
|
||||||
|
ASSERT_TRUE(session->handle(gallery_request(
|
||||||
|
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency"))));
|
||||||
|
ASSERT_TRUE(session->handle(gallery_request(
|
||||||
|
Gallery_Request_Kind::Patch,
|
||||||
|
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":60}})")));
|
||||||
|
if (index >= 2)
|
||||||
|
set_gallery_view_active(*session, false);
|
||||||
|
sessions.push_back(std::move(session));
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0; index < 2; ++index) {
|
||||||
|
ASSERT_TRUE(wait_for_condition([&sessions, index] {
|
||||||
|
return successful_render_count(*sessions[index]) >= 2;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
std::array<std::uint64_t, session_count> hidden_counts{};
|
||||||
|
for (std::size_t index = 2; index < session_count; ++index)
|
||||||
|
hidden_counts[index] = successful_render_count(*sessions[index]);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(80));
|
||||||
|
for (std::size_t index = 2; index < session_count; ++index)
|
||||||
|
EXPECT_EQ(successful_render_count(*sessions[index]), hidden_counts[index]) << index;
|
||||||
|
set_gallery_view_active(*sessions[0], false);
|
||||||
|
set_gallery_view_active(*sessions[1], false);
|
||||||
|
const auto first_stopped = successful_render_count(*sessions[0]);
|
||||||
|
const auto second_stopped = successful_render_count(*sessions[1]);
|
||||||
|
set_gallery_view_active(*sessions[2], true);
|
||||||
|
set_gallery_view_active(*sessions[3], true);
|
||||||
|
EXPECT_TRUE(wait_for_condition([&sessions, baseline = hidden_counts[2]] {
|
||||||
|
return successful_render_count(*sessions[2]) > baseline;
|
||||||
|
}));
|
||||||
|
EXPECT_TRUE(wait_for_condition([&sessions, baseline = hidden_counts[3]] {
|
||||||
|
return successful_render_count(*sessions[3]) > baseline;
|
||||||
|
}));
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(80));
|
||||||
|
EXPECT_EQ(successful_render_count(*sessions[0]), first_stopped);
|
||||||
|
EXPECT_EQ(successful_render_count(*sessions[1]), second_stopped);
|
||||||
|
}
|
||||||
|
|
||||||
void expect_low_latency_canvas_to_change(std::string_view case_id) {
|
void expect_low_latency_canvas_to_change(std::string_view case_id) {
|
||||||
SCOPED_TRACE(std::string(case_id));
|
SCOPED_TRACE(std::string(case_id));
|
||||||
Gallery_Plot_Session session(true);
|
Gallery_Plot_Session session(true);
|
||||||
|
|||||||
+67
-30
@@ -35,13 +35,13 @@ const state = {
|
|||||||
socket: null,
|
socket: null,
|
||||||
reconnectTimer: 0,
|
reconnectTimer: 0,
|
||||||
frameTimer: 0,
|
frameTimer: 0,
|
||||||
|
frameTimeout: 0,
|
||||||
framePending: false,
|
framePending: false,
|
||||||
requestStarted: 0,
|
requestStarted: 0,
|
||||||
lastFrameAt: 0,
|
lastFrameAt: 0,
|
||||||
smoothedFps: 0,
|
smoothedFps: 0,
|
||||||
frameCount: 0,
|
frameCount: 0,
|
||||||
paused: false,
|
paused: false,
|
||||||
manuallyReconnecting: false,
|
|
||||||
requestedWidth: 960,
|
requestedWidth: 960,
|
||||||
requestedHeight: 600
|
requestedHeight: 600
|
||||||
};
|
};
|
||||||
@@ -120,20 +120,68 @@ function syncViewport() {
|
|||||||
sendEvent("resize", {width, height});
|
sendEvent("resize", {width, height});
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleFrame(delay = 42) {
|
function streamActive() {
|
||||||
|
return !state.paused && !document.hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopFramePump() {
|
||||||
window.clearTimeout(state.frameTimer);
|
window.clearTimeout(state.frameTimer);
|
||||||
if (!state.paused)
|
state.frameTimer = 0;
|
||||||
state.frameTimer = window.setTimeout(requestFrame, delay);
|
}
|
||||||
|
|
||||||
|
function clearFrameRequest() {
|
||||||
|
window.clearTimeout(state.frameTimeout);
|
||||||
|
state.frameTimeout = 0;
|
||||||
|
state.framePending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleFrame(delay = 42) {
|
||||||
|
stopFramePump();
|
||||||
|
if (!streamActive())
|
||||||
|
return;
|
||||||
|
state.frameTimer = window.setTimeout(() => {
|
||||||
|
state.frameTimer = 0;
|
||||||
|
requestFrame();
|
||||||
|
}, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestFrame() {
|
function requestFrame() {
|
||||||
if (state.paused || state.framePending)
|
if (!streamActive() || state.framePending)
|
||||||
|
return;
|
||||||
|
if (!sendEvent("frame"))
|
||||||
return;
|
return;
|
||||||
state.requestStarted = performance.now();
|
state.requestStarted = performance.now();
|
||||||
state.framePending = sendEvent("frame");
|
state.framePending = true;
|
||||||
|
window.clearTimeout(state.frameTimeout);
|
||||||
|
state.frameTimeout = window.setTimeout(() => {
|
||||||
|
state.frameTimeout = 0;
|
||||||
|
if (!state.framePending)
|
||||||
|
return;
|
||||||
|
state.framePending = false;
|
||||||
|
if (!streamActive() || state.socket?.readyState !== WebSocket.OPEN)
|
||||||
|
return;
|
||||||
|
sendEvent("show");
|
||||||
|
scheduleFrame(42);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncStreamActivity() {
|
||||||
|
stopFramePump();
|
||||||
|
const active = streamActive();
|
||||||
|
if (!active) {
|
||||||
|
state.lastFrameAt = 0;
|
||||||
|
state.smoothedFps = 0;
|
||||||
|
elements.fps.textContent = "0.0";
|
||||||
|
}
|
||||||
|
if (!state.socket || state.socket.readyState !== WebSocket.OPEN)
|
||||||
|
return;
|
||||||
|
sendEvent(active ? "show" : "hide");
|
||||||
|
if (active)
|
||||||
|
requestFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawPixelFrame(buffer) {
|
function drawPixelFrame(buffer) {
|
||||||
|
clearFrameRequest();
|
||||||
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength < 16)
|
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength < 16)
|
||||||
throw new Error("像素帧长度无效");
|
throw new Error("像素帧长度无效");
|
||||||
const bytes = new Uint8Array(buffer);
|
const bytes = new Uint8Array(buffer);
|
||||||
@@ -163,7 +211,6 @@ function drawPixelFrame(buffer) {
|
|||||||
const megabitsPerSecond = roundTrip > 0 ? buffer.byteLength * 8 / roundTrip / 1000 : 0;
|
const megabitsPerSecond = roundTrip > 0 ? buffer.byteLength * 8 / roundTrip / 1000 : 0;
|
||||||
state.lastFrameAt = now;
|
state.lastFrameAt = now;
|
||||||
state.frameCount += 1;
|
state.frameCount += 1;
|
||||||
state.framePending = false;
|
|
||||||
|
|
||||||
elements.fps.textContent = state.smoothedFps.toFixed(1);
|
elements.fps.textContent = state.smoothedFps.toFixed(1);
|
||||||
elements.latency.textContent = roundTrip.toFixed(1);
|
elements.latency.textContent = roundTrip.toFixed(1);
|
||||||
@@ -176,12 +223,10 @@ function drawPixelFrame(buffer) {
|
|||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
window.clearTimeout(state.reconnectTimer);
|
window.clearTimeout(state.reconnectTimer);
|
||||||
window.clearTimeout(state.frameTimer);
|
stopFramePump();
|
||||||
state.framePending = false;
|
clearFrameRequest();
|
||||||
if (state.socket && state.socket.readyState < WebSocket.CLOSING) {
|
if (state.socket && state.socket.readyState < WebSocket.CLOSING)
|
||||||
state.manuallyReconnecting = true;
|
|
||||||
state.socket.close(1000, "reconnect");
|
state.socket.close(1000, "reconnect");
|
||||||
}
|
|
||||||
|
|
||||||
setConnectionState("", "正在连接", "建立纯 WebSocket 像素通道");
|
setConnectionState("", "正在连接", "建立纯 WebSocket 像素通道");
|
||||||
const socket = new WebSocket(endpoint);
|
const socket = new WebSocket(endpoint);
|
||||||
@@ -191,14 +236,12 @@ function connect() {
|
|||||||
socket.addEventListener("open", () => {
|
socket.addEventListener("open", () => {
|
||||||
if (socket !== state.socket)
|
if (socket !== state.socket)
|
||||||
return;
|
return;
|
||||||
state.manuallyReconnecting = false;
|
|
||||||
setConnectionState("online", "通道在线", "等待首个 RGBA 像素帧");
|
setConnectionState("online", "通道在线", "等待首个 RGBA 像素帧");
|
||||||
elements.connectionLabel.textContent = "通道在线";
|
elements.connectionLabel.textContent = "通道在线";
|
||||||
sendEvent("show");
|
|
||||||
syncViewport();
|
syncViewport();
|
||||||
sendEvent("resize", {width: state.requestedWidth, height: state.requestedHeight});
|
sendEvent("resize", {width: state.requestedWidth, height: state.requestedHeight});
|
||||||
sendControlSnapshot();
|
sendControlSnapshot();
|
||||||
requestFrame();
|
syncStreamActivity();
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener("message", event => {
|
socket.addEventListener("message", event => {
|
||||||
@@ -207,7 +250,7 @@ function connect() {
|
|||||||
try {
|
try {
|
||||||
drawPixelFrame(event.data);
|
drawPixelFrame(event.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
state.framePending = false;
|
clearFrameRequest();
|
||||||
setConnectionState("offline", "帧协议错误", error.message);
|
setConnectionState("offline", "帧协议错误", error.message);
|
||||||
socket.close(1003, "invalid pixel frame");
|
socket.close(1003, "invalid pixel frame");
|
||||||
}
|
}
|
||||||
@@ -216,10 +259,10 @@ function connect() {
|
|||||||
socket.addEventListener("close", event => {
|
socket.addEventListener("close", event => {
|
||||||
if (socket !== state.socket)
|
if (socket !== state.socket)
|
||||||
return;
|
return;
|
||||||
state.framePending = false;
|
stopFramePump();
|
||||||
|
clearFrameRequest();
|
||||||
setConnectionState("offline", "通道离线", event.reason || `WebSocket 已关闭 (${event.code})`);
|
setConnectionState("offline", "通道离线", event.reason || `WebSocket 已关闭 (${event.code})`);
|
||||||
if (!state.manuallyReconnecting)
|
state.reconnectTimer = window.setTimeout(connect, 1200);
|
||||||
state.reconnectTimer = window.setTimeout(connect, 1200);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener("error", () => {
|
socket.addEventListener("error", () => {
|
||||||
@@ -309,9 +352,7 @@ elements.pause.addEventListener("click", () => {
|
|||||||
elements.pause.setAttribute("aria-pressed", String(state.paused));
|
elements.pause.setAttribute("aria-pressed", String(state.paused));
|
||||||
elements.pause.classList.toggle("paused", state.paused);
|
elements.pause.classList.toggle("paused", state.paused);
|
||||||
elements.pauseLabel.textContent = state.paused ? "PAUSED" : "LIVE";
|
elements.pauseLabel.textContent = state.paused ? "PAUSED" : "LIVE";
|
||||||
sendEvent(state.paused ? "hide" : "show");
|
syncStreamActivity();
|
||||||
if (!state.paused)
|
|
||||||
requestFrame();
|
|
||||||
});
|
});
|
||||||
elements.reconnect.addEventListener("click", connect);
|
elements.reconnect.addEventListener("click", connect);
|
||||||
|
|
||||||
@@ -321,15 +362,11 @@ new ResizeObserver(() => {
|
|||||||
resizeFrame = requestAnimationFrame(syncViewport);
|
resizeFrame = requestAnimationFrame(syncViewport);
|
||||||
}).observe(elements.stage);
|
}).observe(elements.stage);
|
||||||
|
|
||||||
document.addEventListener("visibilitychange", () => {
|
document.addEventListener("visibilitychange", syncStreamActivity);
|
||||||
if (document.hidden) {
|
window.addEventListener("beforeunload", () => {
|
||||||
sendEvent("hide");
|
stopFramePump();
|
||||||
} else if (!state.paused) {
|
sendEvent("hide");
|
||||||
sendEvent("show");
|
|
||||||
requestFrame();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
window.addEventListener("beforeunload", () => sendEvent("hide"));
|
|
||||||
|
|
||||||
syncViewport();
|
syncViewport();
|
||||||
connect();
|
connect();
|
||||||
|
|||||||
+2
-2
@@ -93,9 +93,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="metric-rack" aria-label="流状态">
|
<div class="metric-rack" aria-label="流状态">
|
||||||
<div><small>FRAME RATE</small><strong><span id="fps-value">0.0</span> fps</strong></div>
|
<div><small>FRAME RATE</small><strong><span id="fps-value">0.0</span> fps</strong></div>
|
||||||
<div><small>ROUND TRIP</small><strong><span id="latency-value">—</span> ms</strong></div>
|
<div><small>FRAME RTT</small><strong><span id="latency-value">—</span> ms</strong></div>
|
||||||
<div><small>RESOLUTION</small><strong id="resolution-value">— × —</strong></div>
|
<div><small>RESOLUTION</small><strong id="resolution-value">— × —</strong></div>
|
||||||
<div><small>PIXEL RATE</small><strong><span id="throughput-value">0.0</span> Mb/s</strong></div>
|
<div><small>EFFECTIVE PIXEL RATE</small><strong><span id="throughput-value">0.0</span> Mb/s</strong></div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
+91
-28
@@ -73,6 +73,7 @@ class GalleryCard {
|
|||||||
this.framePending = false;
|
this.framePending = false;
|
||||||
this.frameTimer = null;
|
this.frameTimer = null;
|
||||||
this.frameTimeout = null;
|
this.frameTimeout = null;
|
||||||
|
this.frameTimeoutCount = 0;
|
||||||
this.latestPixelBuffer = null;
|
this.latestPixelBuffer = null;
|
||||||
this.transportTimes = [];
|
this.transportTimes = [];
|
||||||
this.presentationTimes = [];
|
this.presentationTimes = [];
|
||||||
@@ -87,7 +88,8 @@ class GalleryCard {
|
|||||||
this.reconnectTimer = null;
|
this.reconnectTimer = null;
|
||||||
this.disposed = false;
|
this.disposed = false;
|
||||||
this.ready = false;
|
this.ready = false;
|
||||||
this.visible = true;
|
this.intersecting = false;
|
||||||
|
this.backendActive = null;
|
||||||
this.lastFrameRequest = 0;
|
this.lastFrameRequest = 0;
|
||||||
this.lastObserveRequest = 0;
|
this.lastObserveRequest = 0;
|
||||||
this.node = elements.cardTemplate.content.firstElementChild.cloneNode(true);
|
this.node = elements.cardTemplate.content.firstElementChild.cloneNode(true);
|
||||||
@@ -122,18 +124,44 @@ class GalleryCard {
|
|||||||
this.resizeObserver = new ResizeObserver(() => this.resize());
|
this.resizeObserver = new ResizeObserver(() => this.resize());
|
||||||
this.resizeObserver.observe(this.shell);
|
this.resizeObserver.observe(this.shell);
|
||||||
this.intersectionObserver = new IntersectionObserver(entries => {
|
this.intersectionObserver = new IntersectionObserver(entries => {
|
||||||
const wasVisible = this.visible;
|
const intersecting = entries[0]?.isIntersecting === true;
|
||||||
this.visible = entries[0]?.isIntersecting !== false;
|
if (this.intersecting === intersecting) return;
|
||||||
if (this.ready && wasVisible !== this.visible) this.send(this.visible ? "show" : "hide");
|
this.intersecting = intersecting;
|
||||||
if (this.visible) this.scheduleFramePump();
|
this.syncActivity();
|
||||||
}, {rootMargin: "160px"});
|
}, {rootMargin: "160px"});
|
||||||
this.intersectionObserver.observe(this.node);
|
this.intersectionObserver.observe(this.node);
|
||||||
this.connect();
|
|
||||||
}
|
}
|
||||||
setSocketState(state, text) {
|
setSocketState(state, text) {
|
||||||
this.socketState.dataset.state = state;
|
this.socketState.dataset.state = state;
|
||||||
this.socketState.querySelector("span").textContent = text;
|
this.socketState.querySelector("span").textContent = text;
|
||||||
}
|
}
|
||||||
|
categoryVisible() {
|
||||||
|
return activeCategory === "全部" || this.definition.category === activeCategory;
|
||||||
|
}
|
||||||
|
displayVisible() {
|
||||||
|
return !document.hidden && this.mode.id === activeMode && this.categoryVisible() && this.intersecting;
|
||||||
|
}
|
||||||
|
streamActive() {
|
||||||
|
return !streamsPaused && this.displayVisible();
|
||||||
|
}
|
||||||
|
stopFramePump() {
|
||||||
|
if (this.frameTimer !== null) clearTimeout(this.frameTimer);
|
||||||
|
this.frameTimer = null;
|
||||||
|
}
|
||||||
|
syncActivity(force = false) {
|
||||||
|
const visible = this.displayVisible();
|
||||||
|
if (visible && ![WebSocket.OPEN, WebSocket.CONNECTING].includes(this.socket?.readyState)) {
|
||||||
|
this.connect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const active = !streamsPaused && visible;
|
||||||
|
if (!active) this.stopFramePump();
|
||||||
|
if (this.ready && (force || this.backendActive !== active)) {
|
||||||
|
this.send(active ? "show" : "hide");
|
||||||
|
this.backendActive = active;
|
||||||
|
}
|
||||||
|
if (active) this.scheduleFramePump(true);
|
||||||
|
}
|
||||||
connect() {
|
connect() {
|
||||||
if (this.disposed || [WebSocket.OPEN, WebSocket.CONNECTING].includes(this.socket?.readyState)) return;
|
if (this.disposed || [WebSocket.OPEN, WebSocket.CONNECTING].includes(this.socket?.readyState)) return;
|
||||||
clearTimeout(this.reconnectTimer);
|
clearTimeout(this.reconnectTimer);
|
||||||
@@ -153,12 +181,13 @@ class GalleryCard {
|
|||||||
socket.addEventListener("close", () => {
|
socket.addEventListener("close", () => {
|
||||||
if (this.socket !== socket) return;
|
if (this.socket !== socket) return;
|
||||||
this.ready = false;
|
this.ready = false;
|
||||||
|
this.backendActive = null;
|
||||||
this.framePending = false;
|
this.framePending = false;
|
||||||
clearTimeout(this.frameTimer); this.frameTimer = null;
|
this.stopFramePump();
|
||||||
clearTimeout(this.frameTimeout); this.frameTimeout = null;
|
clearTimeout(this.frameTimeout); this.frameTimeout = null;
|
||||||
this.setSocketState("error", "连接关闭 · 自动重连");
|
this.setSocketState("error", this.displayVisible() ? "连接关闭 · 自动重连" : "连接关闭 · 等待可见");
|
||||||
this.updateMotionStatus();
|
this.updateMotionStatus();
|
||||||
if (!this.disposed) this.reconnectTimer = setTimeout(() => this.connect(), 1000);
|
if (!this.disposed && this.displayVisible()) this.reconnectTimer = setTimeout(() => this.connect(), 1000);
|
||||||
});
|
});
|
||||||
socket.addEventListener("error", () => {
|
socket.addEventListener("error", () => {
|
||||||
if (this.socket === socket) this.setSocketState("error", "连接错误 · 自动重连");
|
if (this.socket === socket) this.setSocketState("error", "连接错误 · 自动重连");
|
||||||
@@ -187,13 +216,13 @@ class GalleryCard {
|
|||||||
this.actions = data.actions?.data || [];
|
this.actions = data.actions?.data || [];
|
||||||
this.telemetry = data.telemetry || {};
|
this.telemetry = data.telemetry || {};
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
|
this.backendActive = null;
|
||||||
this.node.dataset.ready = "true";
|
this.node.dataset.ready = "true";
|
||||||
this.send(this.visible ? "show" : "hide");
|
this.syncActivity();
|
||||||
this.node.querySelector(".card-controls").textContent = this.controls.length;
|
this.node.querySelector(".card-controls").textContent = this.controls.length;
|
||||||
this.node.querySelector(".card-actions").textContent = this.actions.length;
|
this.node.querySelector(".card-actions").textContent = this.actions.length;
|
||||||
this.setSocketState("ready", `${data.frame_mode?.strategy || "Core2"} 在线`);
|
this.setSocketState("ready", `${data.frame_mode?.strategy || "Core2"} 在线`);
|
||||||
this.updatePerformance();
|
this.updatePerformance();
|
||||||
this.scheduleFramePump(true);
|
|
||||||
if (data.notice && !data.notice.includes("已创建")) toast(`${this.definition.title}:${data.notice}`);
|
if (data.notice && !data.notice.includes("已创建")) toast(`${this.definition.title}:${data.notice}`);
|
||||||
if (activeCard === this) { elements.menuStatus.textContent = data.notice || "后端状态已回读"; renderMenuBody(); }
|
if (activeCard === this) { elements.menuStatus.textContent = data.notice || "后端状态已回读"; renderMenuBody(); }
|
||||||
}
|
}
|
||||||
@@ -214,6 +243,8 @@ class GalleryCard {
|
|||||||
this.node.querySelector(".perf-points").textContent = `${Number(dataShape.input_points || 0).toLocaleString()}→${Number(dataShape.rendered_elements || 0).toLocaleString()}`;
|
this.node.querySelector(".perf-points").textContent = `${Number(dataShape.input_points || 0).toLocaleString()}→${Number(dataShape.rendered_elements || 0).toLocaleString()}`;
|
||||||
this.node.querySelector(".perf-limit").textContent = limitNames[observer.limit_state] || observer.limit_state || "N/A";
|
this.node.querySelector(".perf-limit").textContent = limitNames[observer.limit_state] || observer.limit_state || "N/A";
|
||||||
this.node.querySelector(".perf-event").textContent = observer.last_event || "none";
|
this.node.querySelector(".perf-event").textContent = observer.last_event || "none";
|
||||||
|
this.node.querySelector(".perf-timeouts").textContent = this.frameTimeoutCount.toLocaleString();
|
||||||
|
this.node.querySelector(".perf-pixel-age").textContent = this.lastPixelReceivedAt ? `${Math.max(0, performance.now() - this.lastPixelReceivedAt).toFixed(0)} ms` : "—";
|
||||||
this.updateObserverDashboard(observer);
|
this.updateObserverDashboard(observer);
|
||||||
this.updateMotionStatus();
|
this.updateMotionStatus();
|
||||||
const setLimitFlag = (selector, active, duration) => {
|
const setLimitFlag = (selector, active, duration) => {
|
||||||
@@ -244,14 +275,20 @@ class GalleryCard {
|
|||||||
pixelSignature(buffer, width, height, stride) {
|
pixelSignature(buffer, width, height, stride) {
|
||||||
const bytes = new Uint8Array(buffer, 16, stride * height);
|
const bytes = new Uint8Array(buffer, 16, stride * height);
|
||||||
let hash = (2166136261 ^ width ^ (height << 16)) >>> 0;
|
let hash = (2166136261 ^ width ^ (height << 16)) >>> 0;
|
||||||
for (let offset = 0; offset < bytes.length; offset += 64)
|
const sampleCount = Math.min(4096, bytes.length);
|
||||||
|
if (sampleCount <= 1) return Math.imul(hash ^ (bytes[0] || 0), 16777619) >>> 0;
|
||||||
|
for (let index = 0; index < sampleCount; index++) {
|
||||||
|
const offset = Math.floor(index * (bytes.length - 1) / (sampleCount - 1));
|
||||||
hash = Math.imul(hash ^ bytes[offset], 16777619) >>> 0;
|
hash = Math.imul(hash ^ bytes[offset], 16777619) >>> 0;
|
||||||
|
}
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
updateMotionStatus(now = performance.now()) {
|
updateMotionStatus(now = performance.now()) {
|
||||||
let state = "waiting", label = "等待动态帧";
|
let state = "waiting", label = "等待动态帧";
|
||||||
if (!this.ready || this.socket?.readyState !== WebSocket.OPEN) {
|
if (!this.ready || this.socket?.readyState !== WebSocket.OPEN) {
|
||||||
state = "stalled"; label = "像素流断开";
|
state = "stalled"; label = "像素流断开";
|
||||||
|
} else if (!this.streamActive()) {
|
||||||
|
state = "waiting"; label = streamsPaused && this.displayVisible() ? "像素流已暂停" : "非活动视图";
|
||||||
} else if (this.lastPixelChangeAt && now - this.lastPixelChangeAt < 1500) {
|
} else if (this.lastPixelChangeAt && now - this.lastPixelChangeAt < 1500) {
|
||||||
state = "moving"; label = `画面变化 ${this.changedPixelFrames.toLocaleString()}`;
|
state = "moving"; label = `画面变化 ${this.changedPixelFrames.toLocaleString()}`;
|
||||||
} else if (this.lastPixelReceivedAt && now - this.lastPixelReceivedAt < 1500) {
|
} else if (this.lastPixelReceivedAt && now - this.lastPixelReceivedAt < 1500) {
|
||||||
@@ -268,11 +305,19 @@ class GalleryCard {
|
|||||||
receivePixels(buffer) {
|
receivePixels(buffer) {
|
||||||
this.framePending = false;
|
this.framePending = false;
|
||||||
if (this.frameTimeout !== null) { clearTimeout(this.frameTimeout); this.frameTimeout = null; }
|
if (this.frameTimeout !== null) { clearTimeout(this.frameTimeout); this.frameTimeout = null; }
|
||||||
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength < 16) return;
|
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength < 16) {
|
||||||
|
this.setSocketState("error", "像素帧无效 · 自动重连");
|
||||||
|
this.socket?.close(1003, "invalid pixel frame");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const header = new DataView(buffer, 0, 16);
|
const header = new DataView(buffer, 0, 16);
|
||||||
const magic = String.fromCharCode(...new Uint8Array(buffer, 0, 4));
|
const magic = String.fromCharCode(...new Uint8Array(buffer, 0, 4));
|
||||||
const width = header.getUint32(4, true), height = header.getUint32(8, true), stride = header.getUint32(12, true);
|
const width = header.getUint32(4, true), height = header.getUint32(8, true), stride = header.getUint32(12, true);
|
||||||
if (magic !== "RVP1" || !width || !height || stride < width * 4 || buffer.byteLength < 16 + stride * height) return;
|
if (magic !== "RVP1" || !width || !height || stride < width * 4 || buffer.byteLength < 16 + stride * height) {
|
||||||
|
this.setSocketState("error", "像素帧协议错误 · 自动重连");
|
||||||
|
this.socket?.close(1003, "invalid pixel frame");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const signature = this.pixelSignature(buffer, width, height, stride);
|
const signature = this.pixelSignature(buffer, width, height, stride);
|
||||||
if (this.lastPixelSignature === null || signature !== this.lastPixelSignature) {
|
if (this.lastPixelSignature === null || signature !== this.lastPixelSignature) {
|
||||||
@@ -282,6 +327,7 @@ class GalleryCard {
|
|||||||
this.lastPixelSignature = signature;
|
this.lastPixelSignature = signature;
|
||||||
this.lastPixelReceivedAt = now;
|
this.lastPixelReceivedAt = now;
|
||||||
this.latestPixelBuffer = buffer;
|
this.latestPixelBuffer = buffer;
|
||||||
|
this.setSocketState("ready", `${this.mode.strategy || "Core2"} 在线`);
|
||||||
this.transportFps = this.recordRate(this.transportTimes, now);
|
this.transportFps = this.recordRate(this.transportTimes, now);
|
||||||
this.frameCount++;
|
this.frameCount++;
|
||||||
this.frameLabel.textContent = this.frameCount.toLocaleString();
|
this.frameLabel.textContent = this.frameCount.toLocaleString();
|
||||||
@@ -304,54 +350,63 @@ class GalleryCard {
|
|||||||
}
|
}
|
||||||
this.presentationFps = this.recordRate(this.presentationTimes, time);
|
this.presentationFps = this.recordRate(this.presentationTimes, time);
|
||||||
}
|
}
|
||||||
recordRate(history, now) {
|
currentRate(history, now) {
|
||||||
history.push(now);
|
while (history.length && history[0] < now - 1000) history.shift();
|
||||||
while (history.length > 2 && history[0] < now - 1000) history.shift();
|
|
||||||
if (history.length < 2) return 0;
|
if (history.length < 2) return 0;
|
||||||
return (history.length - 1) * 1000 / Math.max(1, history[history.length - 1] - history[0]);
|
return (history.length - 1) * 1000 / Math.max(1, history[history.length - 1] - history[0]);
|
||||||
}
|
}
|
||||||
|
recordRate(history, now) {
|
||||||
|
history.push(now);
|
||||||
|
return this.currentRate(history, now);
|
||||||
|
}
|
||||||
resize() {
|
resize() {
|
||||||
if (this.socket?.readyState !== WebSocket.OPEN) return;
|
if (this.socket?.readyState !== WebSocket.OPEN) return;
|
||||||
const rect = this.shell.getBoundingClientRect();
|
const rect = this.shell.getBoundingClientRect();
|
||||||
this.send("resize", {width: Math.max(240, Math.round(rect.width)), height: Math.max(180, Math.round(rect.height))});
|
this.send("resize", {width: Math.max(240, Math.round(rect.width)), height: Math.max(180, Math.round(rect.height))});
|
||||||
}
|
}
|
||||||
requestFrame(time, explicit = false) {
|
requestFrame(time, explicit = false) {
|
||||||
if (streamsPaused && !explicit || !this.visible || !this.ready || this.framePending || this.socket?.readyState !== WebSocket.OPEN) return;
|
if ((!explicit && !this.streamActive()) || (explicit && !this.displayVisible()) || !this.ready || this.framePending || this.socket?.readyState !== WebSocket.OPEN) return;
|
||||||
if (!explicit && this.mode.id === "manual") return;
|
if (!explicit && this.mode.id === "manual") return;
|
||||||
const streamControl = this.controls.find(item => item.id === "pixel_stream_fps");
|
const streamControl = this.controls.find(item => item.id === "pixel_stream_fps");
|
||||||
const interval = this.mode.id === "low_latency" ? 1000 / Math.max(1, Math.min(60, Number(streamControl?.value || 30))) : 66;
|
const interval = this.mode.id === "low_latency" ? 1000 / Math.max(1, Math.min(60, Number(streamControl?.value || 30))) : 66;
|
||||||
if (!explicit && time - this.lastFrameRequest < interval) return;
|
if (!explicit && this.mode.id !== "low_latency" && time - this.lastFrameRequest < interval) return;
|
||||||
this.lastFrameRequest = time;
|
this.lastFrameRequest = time;
|
||||||
this.framePending = true;
|
this.framePending = true;
|
||||||
this.send("frame");
|
this.send("frame");
|
||||||
this.frameTimeout = setTimeout(() => {
|
this.frameTimeout = setTimeout(() => {
|
||||||
this.framePending = false;
|
this.framePending = false;
|
||||||
this.frameTimeout = null;
|
this.frameTimeout = null;
|
||||||
this.scheduleFramePump();
|
this.frameTimeoutCount++;
|
||||||
|
if (!this.streamActive() || this.socket?.readyState !== WebSocket.OPEN) return;
|
||||||
|
this.setSocketState("error", "像素响应超时 · 正在恢复");
|
||||||
|
this.syncActivity(true);
|
||||||
}, 1500);
|
}, 1500);
|
||||||
}
|
}
|
||||||
scheduleFramePump(reset = false) {
|
scheduleFramePump(reset = false) {
|
||||||
if (this.mode.id !== "low_latency") return;
|
if (this.mode.id !== "low_latency") return;
|
||||||
if (reset && this.frameTimer !== null) { clearTimeout(this.frameTimer); this.frameTimer = null; }
|
if (reset) this.stopFramePump();
|
||||||
if (this.frameTimer !== null || this.framePending || streamsPaused || !this.visible ||
|
if (this.frameTimer !== null || this.framePending || !this.streamActive() || !this.ready || this.socket?.readyState !== WebSocket.OPEN) return;
|
||||||
!this.ready || this.socket?.readyState !== WebSocket.OPEN) return;
|
|
||||||
const streamControl = this.controls.find(item => item.id === "pixel_stream_fps");
|
const streamControl = this.controls.find(item => item.id === "pixel_stream_fps");
|
||||||
const interval = 1000 / Math.max(1, Math.min(60, Number(streamControl?.value || 30)));
|
const interval = 1000 / Math.max(1, Math.min(60, Number(streamControl?.value || 30)));
|
||||||
const delay = Math.max(0, this.lastFrameRequest + interval - performance.now());
|
const delay = Math.max(0, Math.ceil(this.lastFrameRequest + interval - performance.now()));
|
||||||
this.frameTimer = setTimeout(() => {
|
this.frameTimer = setTimeout(() => {
|
||||||
this.frameTimer = null;
|
this.frameTimer = null;
|
||||||
this.requestFrame(performance.now());
|
this.requestFrame(performance.now());
|
||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
observe(time) {
|
observe(time) {
|
||||||
if (!this.ready || !this.visible || this.socket?.readyState !== WebSocket.OPEN || time - this.lastObserveRequest < 650) return;
|
if (!this.ready || !this.displayVisible() || this.socket?.readyState !== WebSocket.OPEN || time - this.lastObserveRequest < 650) return;
|
||||||
this.lastObserveRequest = time;
|
this.lastObserveRequest = time;
|
||||||
|
this.transportFps = this.currentRate(this.transportTimes, time);
|
||||||
|
this.presentationFps = this.currentRate(this.presentationTimes, time);
|
||||||
this.send("gallery_observe", {client_metrics: {
|
this.send("gallery_observe", {client_metrics: {
|
||||||
transport_fps: this.transportFps,
|
transport_fps: this.transportFps,
|
||||||
presentation_fps: this.presentationFps,
|
presentation_fps: this.presentationFps,
|
||||||
websocket_buffered_bytes: this.socket.bufferedAmount || 0,
|
websocket_buffered_bytes: this.socket.bufferedAmount || 0,
|
||||||
changed_pixel_frames: this.changedPixelFrames,
|
changed_pixel_frames: this.changedPixelFrames,
|
||||||
duplicate_pixel_frames: this.duplicatePixelFrames,
|
duplicate_pixel_frames: this.duplicatePixelFrames,
|
||||||
|
frame_request_timeout_count: this.frameTimeoutCount,
|
||||||
|
last_pixel_receive_age_ms: this.lastPixelReceivedAt ? Math.max(0, time - this.lastPixelReceivedAt) : 0,
|
||||||
last_pixel_change_age_ms: this.lastPixelChangeAt ? Math.max(0, time - this.lastPixelChangeAt) : 0
|
last_pixel_change_age_ms: this.lastPixelChangeAt ? Math.max(0, time - this.lastPixelChangeAt) : 0
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
@@ -385,6 +440,9 @@ function createPage(mode) {
|
|||||||
pages.set(mode.id, {mode, page, cards});
|
pages.set(mode.id, {mode, page, cards});
|
||||||
return pages.get(mode.id);
|
return pages.get(mode.id);
|
||||||
}
|
}
|
||||||
|
function syncCardActivity() {
|
||||||
|
for (const page of pages.values()) for (const card of page.cards) card.syncActivity();
|
||||||
|
}
|
||||||
function selectMode(id) {
|
function selectMode(id) {
|
||||||
activeMode = id;
|
activeMode = id;
|
||||||
let selected = pages.get(id);
|
let selected = pages.get(id);
|
||||||
@@ -393,12 +451,16 @@ function selectMode(id) {
|
|||||||
[...elements.modeTabs.children].forEach(button => button.classList.toggle("active", button.dataset.mode === id));
|
[...elements.modeTabs.children].forEach(button => button.classList.toggle("active", button.dataset.mode === id));
|
||||||
elements.modeDescription.textContent = selected.mode.description;
|
elements.modeDescription.textContent = selected.mode.description;
|
||||||
applyCategory();
|
applyCategory();
|
||||||
|
syncCardActivity();
|
||||||
closeMenu();
|
closeMenu();
|
||||||
}
|
}
|
||||||
function applyCategory() {
|
function applyCategory() {
|
||||||
const page = pages.get(activeMode);
|
const page = pages.get(activeMode);
|
||||||
if (!page) return;
|
if (!page) return;
|
||||||
for (const card of page.cards) card.node.hidden = activeCategory !== "全部" && card.definition.category !== activeCategory;
|
for (const card of page.cards) {
|
||||||
|
card.node.hidden = !card.categoryVisible();
|
||||||
|
card.syncActivity();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function installNavigation() {
|
function installNavigation() {
|
||||||
elements.modeTabs.replaceChildren(...modes.map(mode => {
|
elements.modeTabs.replaceChildren(...modes.map(mode => {
|
||||||
@@ -542,13 +604,14 @@ function loop(time) {
|
|||||||
elements.streamToggle.addEventListener("click", () => {
|
elements.streamToggle.addEventListener("click", () => {
|
||||||
streamsPaused = !streamsPaused;
|
streamsPaused = !streamsPaused;
|
||||||
elements.streamToggle.textContent = streamsPaused ? "恢复自动像素流" : "暂停自动像素流";
|
elements.streamToggle.textContent = streamsPaused ? "恢复自动像素流" : "暂停自动像素流";
|
||||||
if (!streamsPaused) for (const page of pages.values()) for (const card of page.cards) card.scheduleFramePump(true);
|
syncCardActivity();
|
||||||
});
|
});
|
||||||
elements.menuClose.addEventListener("click", closeMenu);
|
elements.menuClose.addEventListener("click", closeMenu);
|
||||||
elements.menuTabs.forEach(button => button.addEventListener("click", () => { activeTab = button.dataset.tab; elements.menuTabs.forEach(item => item.classList.toggle("active", item === button)); renderMenuBody(); }));
|
elements.menuTabs.forEach(button => button.addEventListener("click", () => { activeTab = button.dataset.tab; elements.menuTabs.forEach(item => item.classList.toggle("active", item === button)); renderMenuBody(); }));
|
||||||
document.addEventListener("keydown", event => { if (event.key === "Escape" && !elements.menu.hidden) closeMenu(); });
|
document.addEventListener("keydown", event => { if (event.key === "Escape" && !elements.menu.hidden) closeMenu(); });
|
||||||
document.addEventListener("pointerdown", event => { if (!elements.menu.hidden && !elements.menu.contains(event.target) && !event.target.closest(".open-menu")) closeMenu(); });
|
document.addEventListener("pointerdown", event => { if (!elements.menu.hidden && !elements.menu.contains(event.target) && !event.target.closest(".open-menu")) closeMenu(); });
|
||||||
window.addEventListener("beforeunload", () => { for (const page of pages.values()) for (const card of page.cards) { card.disposed = true; clearTimeout(card.reconnectTimer); card.socket?.close(); } });
|
document.addEventListener("visibilitychange", syncCardActivity);
|
||||||
|
window.addEventListener("beforeunload", () => { for (const page of pages.values()) for (const card of page.cards) { card.disposed = true; card.stopFramePump(); clearTimeout(card.frameTimeout); clearTimeout(card.reconnectTimer); card.send("hide"); card.socket?.close(); } });
|
||||||
|
|
||||||
connectCatalog();
|
connectCatalog();
|
||||||
requestAnimationFrame(loop);
|
requestAnimationFrame(loop);
|
||||||
|
|||||||
@@ -78,17 +78,19 @@
|
|||||||
<div class="canvas-loading"><span class="loader"></span><small>创建 Kernel Scene</small></div>
|
<div class="canvas-loading"><span class="loader"></span><small>创建 Kernel Scene</small></div>
|
||||||
</div>
|
</div>
|
||||||
<dl class="performance-strip">
|
<dl class="performance-strip">
|
||||||
<div><dt class="perf-fps">0.0</dt><dd>后台 FPS</dd></div>
|
<div><dt class="perf-fps">0.0</dt><dd>后端渲染 FPS</dd></div>
|
||||||
<div><dt class="perf-transport-fps">0.0</dt><dd>WS 像素 FPS</dd></div>
|
<div><dt class="perf-transport-fps">0.0</dt><dd>像素响应 FPS</dd></div>
|
||||||
<div><dt class="perf-present-fps">0.0</dt><dd>页面 FPS</dd></div>
|
<div><dt class="perf-present-fps">0.0</dt><dd>浏览器呈现 FPS</dd></div>
|
||||||
<div><dt class="perf-render">0.00</dt><dd>Core 渲染 ms</dd></div>
|
<div><dt class="perf-render">0.00</dt><dd>Core 渲染 ms</dd></div>
|
||||||
<div><dt class="perf-encode">0.00</dt><dd>像素编码 ms</dd></div>
|
<div><dt class="perf-encode">0.00</dt><dd>像素编码 ms</dd></div>
|
||||||
<div><dt class="perf-bandwidth">0.0</dt><dd>像素 MB/s</dd></div>
|
<div><dt class="perf-bandwidth">0.0</dt><dd>响应负载 MB/s</dd></div>
|
||||||
<div><dt class="perf-pending">0</dt><dd>队列</dd></div>
|
<div><dt class="perf-pending">0</dt><dd>Kernel 待处理</dd></div>
|
||||||
<div><dt class="perf-dropped">0</dt><dd>丢弃</dd></div>
|
<div><dt class="perf-dropped">0</dt><dd>Kernel 丢弃</dd></div>
|
||||||
<div><dt class="perf-points">0→0</dt><dd>输入→绘制</dd></div>
|
<div><dt class="perf-points">0→0</dt><dd>输入→绘制</dd></div>
|
||||||
<div><dt class="perf-limit">N/A</dt><dd>当前瓶颈</dd></div>
|
<div><dt class="perf-limit">N/A</dt><dd>当前瓶颈</dd></div>
|
||||||
<div><dt class="perf-event">none</dt><dd>观察事件</dd></div>
|
<div><dt class="perf-event">none</dt><dd>观察事件</dd></div>
|
||||||
|
<div><dt class="perf-timeouts">0</dt><dd>像素超时</dd></div>
|
||||||
|
<div><dt class="perf-pixel-age">—</dt><dd>最近像素龄</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
<div class="limit-flags" aria-label="低延迟限速来源">
|
<div class="limit-flags" aria-label="低延迟限速来源">
|
||||||
<strong>限速来源</strong>
|
<strong>限速来源</strong>
|
||||||
|
|||||||
Reference in New Issue
Block a user