From 996e6f615447ba402c796232ef7918f909a31043 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Tue, 11 Aug 2026 09:41:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9E=B6=E6=9E=84=E5=BE=AE=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web_server/Gallery_Plot_Session.cpp | 50 +++++++++++++++------ web_server/Gallery_Protocol.cpp | 4 -- web_server/tests/Web_Bridge_Tests.cpp | 64 +++++++++++++++------------ webapp/app.js | 24 +--------- webapp_gallery/app.js | 59 ++++++++++++------------ webapp_gallery/index.html | 2 + 6 files changed, 108 insertions(+), 95 deletions(-) diff --git a/web_server/Gallery_Plot_Session.cpp b/web_server/Gallery_Plot_Session.cpp index aa1ff63..9d44fda 100644 --- a/web_server/Gallery_Plot_Session.cpp +++ b/web_server/Gallery_Plot_Session.cpp @@ -37,8 +37,8 @@ double number_value(const Gallery_State& state, std::string_view id) { return state_value(state, id); } -std::uint64_t frequency_interval_ns(double frequency_hz) { - return static_cast(1'000'000'000.0 / frequency_hz); +std::uint64_t milliseconds_to_ns(double milliseconds) { + return milliseconds > 0.0 ? static_cast(std::llround(milliseconds * 1'000'000.0)) : 0; } int integer_value(const Gallery_State& state, std::string_view id) { @@ -231,6 +231,9 @@ public: std::uint64_t changed_pixel_frames, std::uint64_t duplicate_pixel_frames, std::uint64_t frame_request_timeout_count, + double frame_round_trip_ms, + double display_interval_ms, + std::uint64_t overwritten_pixel_frames, double last_pixel_receive_age_ms, double last_pixel_change_age_ms) noexcept { client_transport_fps_ = std::isfinite(transport_fps) ? @@ -241,10 +244,20 @@ public: client_changed_pixel_frames_ = changed_pixel_frames; client_duplicate_pixel_frames_ = duplicate_pixel_frames; client_frame_request_timeout_count_ = frame_request_timeout_count; + client_frame_round_trip_ms_ = std::isfinite(frame_round_trip_ms) ? + std::max(0.0, frame_round_trip_ms) : 0.0; + client_display_interval_ms_ = std::isfinite(display_interval_ms) ? + std::max(0.0, display_interval_ms) : 0.0; + client_overwritten_pixel_frames_ = overwritten_pixel_frames; 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) ? std::max(0.0, last_pixel_change_age_ms) : 0.0; + if (frame_mode_ == Gallery_Frame_Mode::Low_Latency) { + const auto interval_ns = std::max(milliseconds_to_ns(client_frame_round_trip_ms_), + milliseconds_to_ns(client_display_interval_ms_)); + plot_.set_consumer_feedback({interval_ns}); + } } [[nodiscard]] bool requires_rebuild(const Gallery_State& candidate) const { @@ -642,6 +655,9 @@ public: {"changed_pixel_frames", client_changed_pixel_frames_}, {"duplicate_pixel_frames", client_duplicate_pixel_frames_}, {"frame_request_timeout_count", client_frame_request_timeout_count_}, + {"frame_round_trip_ms", client_frame_round_trip_ms_}, + {"display_interval_ms", client_display_interval_ms_}, + {"overwritten_pixel_frames", client_overwritten_pixel_frames_}, {"last_pixel_receive_age_ms", client_last_pixel_receive_age_ms_}, {"last_pixel_change_age_ms", client_last_pixel_change_age_ms_} }}, @@ -658,8 +674,7 @@ public: {"render_limited", observer.limit_state == "render_limited"}, {"consumer_limited", observer.limit_state == "consumer_limited"}, {"configured_frequency_hz", observer.frequency_hz}, - {"consumer_feedback_source", "web_pixel_stream"}, - {"web_pixel_stream_fps", number_value(state_, "pixel_stream_fps")}, + {"consumer_feedback_source", "web_runtime"}, {"target_interval_ns", observer.target_interval_ns}, {"paint_duration_ns", observer.paint_duration_ns}, {"render_duration_ns", observer.render_duration_ns}, @@ -914,7 +929,6 @@ private: {"case", case_id_}, {"frame_mode", frame_mode_name(frame_mode_)}, {"configured_fps", low_latency ? plot_.max_render_fps() : 0.0}, - {"web_pixel_stream_fps", low_latency ? number_value(state_, "pixel_stream_fps") : 0.0}, {"automatic_scheduler_interval_ns", scheduler_interval_ns}, {"automatic_scheduler_fps", scheduler_interval_ns == 0 ? 0.0 : 1'000'000'000.0 / static_cast(scheduler_interval_ns)}, @@ -927,6 +941,9 @@ private: {"client_changed_pixel_frames", client_changed_pixel_frames_}, {"client_duplicate_pixel_frames", client_duplicate_pixel_frames_}, {"client_frame_request_timeout_count", client_frame_request_timeout_count_}, + {"client_frame_round_trip_ms", client_frame_round_trip_ms_}, + {"client_display_interval_ms", client_display_interval_ms_}, + {"client_overwritten_pixel_frames", client_overwritten_pixel_frames_}, {"client_last_pixel_receive_age_ms", client_last_pixel_receive_age_ms_}, {"client_last_pixel_change_age_ms", client_last_pixel_change_age_ms_}, {"last_render_ms", last_render_ms_}, @@ -1205,11 +1222,8 @@ private: void apply_state() { plot_.set_background_color(color_from_hex(string_value(state_, "background_color"))); - if (frame_mode_ == Gallery_Frame_Mode::Low_Latency) { + if (frame_mode_ == Gallery_Frame_Mode::Low_Latency) plot_.set_max_render_fps(number_value(state_, "max_render_fps")); - plot_.set_consumer_feedback( - {frequency_interval_ns(number_value(state_, "pixel_stream_fps"))}); - } set_performance_plot_name(plot_, case_id_ + "/" + std::string(frame_mode_name(frame_mode_))); set_performance_overlay_enabled(plot_, bool_value(state_, "performance_overlay")); if (primary_) { @@ -1449,6 +1463,9 @@ private: std::uint64_t client_changed_pixel_frames_{}; std::uint64_t client_duplicate_pixel_frames_{}; std::uint64_t client_frame_request_timeout_count_{}; + double client_frame_round_trip_ms_{}; + double client_display_interval_ms_{}; + std::uint64_t client_overwritten_pixel_frames_{}; double client_last_pixel_receive_age_ms_{}; double client_last_pixel_change_age_ms_{}; bool rendered_since_last_pixel_{}; @@ -1477,13 +1494,13 @@ struct Gallery_Plot_Session::Impl { [this](std::stop_token stop) { run_automatic_renderer(stop); }); } - void update_client_metrics(std::string_view message) { + bool update_client_metrics(std::string_view message) { if (!scene) - return; + return false; const auto root = nlohmann::json::parse(message, nullptr, false); if (root.is_discarded() || !root.is_object() || !root.contains("client_metrics") || !root["client_metrics"].is_object()) - return; + return false; const auto& metrics = root["client_metrics"]; const auto finite_metric = [&metrics](std::string_view name) { const auto iterator = metrics.find(std::string(name)); @@ -1509,8 +1526,12 @@ struct Gallery_Plot_Session::Impl { unsigned_metric("changed_pixel_frames"), unsigned_metric("duplicate_pixel_frames"), unsigned_metric("frame_request_timeout_count"), + finite_metric("frame_round_trip_ms"), + finite_metric("display_interval_ms"), + unsigned_metric("overwritten_pixel_frames"), finite_metric("last_pixel_receive_age_ms"), finite_metric("last_pixel_change_age_ms")); + return true; } [[nodiscard]] std::unique_lock acquire_foreground_lock() { @@ -1648,7 +1669,10 @@ struct Gallery_Plot_Session::Impl { return Web_Response{Web_Response_Type::Json, Gallery_Protocol::error_json("请先发送 gallery_open")}; if (request.kind == Gallery_Request_Kind::Observe) { - update_client_metrics(request.message); + if (update_client_metrics(request.message)) { + ++scheduler_revision; + scheduler_condition.notify_all(); + } return Web_Response{Web_Response_Type::Json, Gallery_Protocol::observer_json( scene->case_id(), scene->frame_mode(), scene->telemetry_json())}; diff --git a/web_server/Gallery_Protocol.cpp b/web_server/Gallery_Protocol.cpp index 99f8bb2..93df4d8 100644 --- a/web_server/Gallery_Protocol.cpp +++ b/web_server/Gallery_Protocol.cpp @@ -126,10 +126,6 @@ void append_common(std::vector& result, Gallery_Frame_Mode f result.push_back(number("max_render_fps", "最大渲染 FPS", "Plot_Core::set_max_render_fps", 30, 0.01, 1'000'000'000, 1, "Low_Latency_Strategy", false, "Kernel 接受任意有限正频率;高目标用于触发并观察 Painter 或后台 Render 瓶颈。")); - result.push_back(number("pixel_stream_fps", "WebSocket 像素 FPS", - "Plot_Core::set_consumer_feedback", 30, 1, 60, 1, - "Web Transport", false, - "限制浏览器拉取完整 RGBA 像素图的频率;Web 适配层将该频率转换为通用消费者反馈,Kernel 不感知 WebSocket。")); } result.push_back(flag("performance_overlay", "性能叠层", "set_performance_overlay_enabled", false, "Plot_Core")); diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index 60c8597..0ba80aa 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -841,12 +841,11 @@ TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers EXPECT_TRUE(low_telemetry.contains("render_fps")); ASSERT_TRUE(low_telemetry.contains("low_latency_limit")); const auto& limit = low_telemetry.at("low_latency_limit"); - EXPECT_TRUE(limit.at("consumer_limited").get()); + EXPECT_FALSE(limit.at("consumer_limited").get()); EXPECT_FALSE(limit.at("frequency_limited").get()); - EXPECT_FALSE(limit.at("paint_limited").get()); - EXPECT_FALSE(limit.at("render_limited").get()); + EXPECT_TRUE(limit.at("paint_limited").get() || limit.at("render_limited").get()); EXPECT_EQ(limit.at("target_interval_ns"), 1); - EXPECT_EQ(limit.at("consumer_interval_ns"), 33'333'333); + EXPECT_EQ(limit.at("consumer_interval_ns"), 0); EXPECT_GT(limit.at("bottleneck_duration_ns").get(), 1U); EXPECT_TRUE(low_telemetry.at("kernel_observer").contains("paint_lease_wait_ns")); EXPECT_TRUE(low_telemetry.at("kernel_observer").contains("render_finish_state_wait_ns")); @@ -939,7 +938,7 @@ TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggres "case_state"); ASSERT_EQ(response_json(session.handle(gallery_request( Gallery_Request_Kind::Patch, - 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}})"))).at("type"), "case_state"); ASSERT_TRUE(wait_for_condition([&session] { @@ -974,35 +973,41 @@ TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggres observer.at("limit_state") == "consumer_limited"); } -TEST(RenderiveWebGallery, WebAdapterPublishesGenericConsumerFeedbackToKernelScheduler) { +TEST(RenderiveWebGallery, WebRuntimeMetricsPublishGenericConsumerFeedbackWithoutTransportFpsCap) { Gallery_Plot_Session session(true); ASSERT_EQ(response_json(session.handle(gallery_request( Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"), "case_state"); - const auto patched = response_json(session.handle(gallery_request( + ASSERT_EQ(response_json(session.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000000000,"pixel_stream_fps":20}})"))); - ASSERT_EQ(patched.at("type"), "case_state"); - const auto& telemetry = patched.at("telemetry"); - const auto& observer = telemetry.at("kernel_observer"); - const auto& limit = telemetry.at("low_latency_limit"); + R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000000000}})"))).at("type"), + "case_state"); + auto observed = response_json(session.handle(gallery_request( + Gallery_Request_Kind::Observe, + R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":20,"presentation_fps":20,"websocket_buffered_bytes":0,"changed_pixel_frames":10,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":500,"display_interval_ms":500,"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_EQ(observer.at("configured_frequency_hz"), 1'000'000'000.0); EXPECT_EQ(observer.at("target_interval_ns"), 1); - EXPECT_EQ(observer.at("consumer_interval_ns"), 50'000'000); - EXPECT_EQ(observer.at("next_refresh_interval_ns"), 50'000'000); + EXPECT_EQ(observer.at("consumer_interval_ns"), 500'000'000); + EXPECT_EQ(observer.at("next_refresh_interval_ns"), 500'000'000); EXPECT_EQ(observer.at("limit_state"), "consumer_limited"); - EXPECT_EQ(limit.at("current"), "consumer_limited"); - EXPECT_TRUE(limit.at("consumer_limited").get()); - EXPECT_EQ(limit.at("consumer_feedback_source"), "web_pixel_stream"); - EXPECT_EQ(limit.at("consumer_interval_ns"), 50'000'000); - EXPECT_EQ(limit.at("scheduler_interval_ns"), 50'000'000); - EXPECT_DOUBLE_EQ(limit.at("scheduler_frequency_hz"), 20.0); - const auto baseline = successful_render_count(session); - ASSERT_TRUE(wait_for_condition([&session, baseline] { - return successful_render_count(session) > baseline; - })); - std::this_thread::sleep_for(std::chrono::milliseconds(250)); - EXPECT_LE(successful_render_count(session) - baseline, 9U); + EXPECT_EQ(limit.at("consumer_feedback_source"), "web_runtime"); + EXPECT_FALSE(limit.contains("web_pixel_stream_fps")); + observed = response_json(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":20,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":2,"display_interval_ms":20,"overwritten_pixel_frames":1,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); + observer = observed.at("telemetry").at("kernel_observer"); + EXPECT_EQ(observer.at("consumer_interval_ns"), 20'000'000); + observed = response_json(session.handle(gallery_request( + Gallery_Request_Kind::Observe, + R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":120,"presentation_fps":120,"websocket_buffered_bytes":0,"changed_pixel_frames":30,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":2,"display_interval_ms":8,"overwritten_pixel_frames":1,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); + observer = observed.at("telemetry").at("kernel_observer"); + EXPECT_EQ(observer.at("consumer_interval_ns"), 8'000'000); + const auto rejected = response_json(session.handle(gallery_request( + Gallery_Request_Kind::Patch, + R"({"category":"event","type":"gallery_patch","patch":{"pixel_stream_fps":30}})"))); + EXPECT_EQ(rejected.at("type"), "error"); } TEST(RenderiveWebGallery, PlaybackStrategyQueuesWithoutAutomaticConsumptionAndReportsEmptyQueue) { @@ -1090,7 +1095,7 @@ TEST(RenderiveWebGallery, ProductionLowLatencySessionRendersWithoutPixelPulls) { })); 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,"last_pixel_receive_age_ms":8.5,"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,"frame_round_trip_ms":4.5,"display_interval_ms":16.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"); @@ -1108,6 +1113,9 @@ TEST(RenderiveWebGallery, ProductionLowLatencySessionRendersWithoutPixelPulls) { 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("frame_request_timeout_count"), 2); + EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("frame_round_trip_ms"), 4.5); + EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("display_interval_ms"), 16.7); + EXPECT_EQ(telemetry.at("client_performance").at("overwritten_pixel_frames"), 5); 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); } @@ -1185,7 +1193,7 @@ void expect_low_latency_canvas_to_change(std::string_view case_id) { ASSERT_FALSE(session.handle(Viewport_Resize{{240, 180}}).has_value()); ASSERT_TRUE(session.handle(gallery_request( Gallery_Request_Kind::Patch, - R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":120,"pixel_stream_fps":30}})"))); + R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":120}})"))); std::this_thread::sleep_for(std::chrono::milliseconds(90)); const auto first = session.handle(Frame_Request{}); diff --git a/webapp/app.js b/webapp/app.js index a24a295..8474959 100644 --- a/webapp/app.js +++ b/webapp/app.js @@ -34,7 +34,6 @@ const elements = { const state = { socket: null, reconnectTimer: 0, - frameTimer: 0, frameTimeout: 0, framePending: false, requestStarted: 0, @@ -124,27 +123,12 @@ function streamActive() { return !state.paused && !document.hidden; } -function stopFramePump() { - window.clearTimeout(state.frameTimer); - state.frameTimer = 0; -} - 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() { if (!streamActive() || state.framePending) return; @@ -161,12 +145,11 @@ function requestFrame() { if (!streamActive() || state.socket?.readyState !== WebSocket.OPEN) return; sendEvent("show"); - scheduleFrame(42); + requestFrame(); }, 2000); } function syncStreamActivity() { - stopFramePump(); const active = streamActive(); if (!active) { state.lastFrameAt = 0; @@ -218,12 +201,11 @@ function drawPixelFrame(buffer) { elements.throughput.textContent = megabitsPerSecond.toFixed(1); elements.frameCounter.textContent = state.frameCount.toLocaleString("zh-CN"); elements.overlay.classList.add("hidden"); - scheduleFrame(); + requestFrame(); } function connect() { window.clearTimeout(state.reconnectTimer); - stopFramePump(); clearFrameRequest(); if (state.socket && state.socket.readyState < WebSocket.CLOSING) state.socket.close(1000, "reconnect"); @@ -259,7 +241,6 @@ function connect() { socket.addEventListener("close", event => { if (socket !== state.socket) return; - stopFramePump(); clearFrameRequest(); setConnectionState("offline", "通道离线", event.reason || `WebSocket 已关闭 (${event.code})`); state.reconnectTimer = window.setTimeout(connect, 1200); @@ -364,7 +345,6 @@ new ResizeObserver(() => { document.addEventListener("visibilitychange", syncStreamActivity); window.addEventListener("beforeunload", () => { - stopFramePump(); sendEvent("hide"); }); diff --git a/webapp_gallery/app.js b/webapp_gallery/app.js index 529ae86..8bfe833 100644 --- a/webapp_gallery/app.js +++ b/webapp_gallery/app.js @@ -27,6 +27,8 @@ let activeCard = null; let activeTab = "controls"; let streamsPaused = false; let toastTimer = 0; +let lastAnimationFrameAt = 0; +let displayIntervalMs = 0; const message = (type, payload = {}) => JSON.stringify({category: "event", type, ...payload}); const setConnection = (state, text) => { @@ -71,7 +73,6 @@ class GalleryCard { this.telemetry = {}; this.frameCount = 0; this.framePending = false; - this.frameTimer = null; this.frameTimeout = null; this.frameTimeoutCount = 0; this.latestPixelBuffer = null; @@ -90,7 +91,9 @@ class GalleryCard { this.ready = false; this.intersecting = false; this.backendActive = null; - this.lastFrameRequest = 0; + this.frameRequestStartedAt = 0; + this.frameRoundTripMs = 0; + this.overwrittenPixelFrames = 0; this.lastObserveRequest = 0; this.node = elements.cardTemplate.content.firstElementChild.cloneNode(true); this.node.dataset.category = definition.category; @@ -144,10 +147,6 @@ class GalleryCard { 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)) { @@ -155,12 +154,11 @@ class GalleryCard { 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); + if (active) this.requestFrame(performance.now()); } connect() { if (this.disposed || [WebSocket.OPEN, WebSocket.CONNECTING].includes(this.socket?.readyState)) return; @@ -183,7 +181,6 @@ class GalleryCard { this.ready = false; this.backendActive = null; this.framePending = false; - this.stopFramePump(); clearTimeout(this.frameTimeout); this.frameTimeout = null; this.setSocketState("error", this.displayVisible() ? "连接关闭 · 自动重连" : "连接关闭 · 等待可见"); this.updateMotionStatus(); @@ -235,6 +232,8 @@ class GalleryCard { this.node.querySelector(".perf-fps").textContent = Number(performanceData.measured_fps || 0).toFixed(1); this.node.querySelector(".perf-transport-fps").textContent = Number(performanceData.pixel_response_fps || 0).toFixed(1); this.node.querySelector(".perf-present-fps").textContent = Number(this.presentationFps || 0).toFixed(1); + this.node.querySelector(".perf-rtt").textContent = Number(this.frameRoundTripMs || 0).toFixed(2); + this.node.querySelector(".perf-overwritten").textContent = this.overwrittenPixelFrames.toLocaleString(); this.node.querySelector(".perf-render").textContent = Number(performanceData.last_render_ms || 0).toFixed(2); this.node.querySelector(".perf-encode").textContent = Number(performanceData.last_pixel_encode_ms || 0).toFixed(2); this.node.querySelector(".perf-bandwidth").textContent = Number(performanceData.pixel_payload_megabytes_per_second || 0).toFixed(1); @@ -327,13 +326,19 @@ class GalleryCard { } else this.duplicatePixelFrames++; this.lastPixelSignature = signature; this.lastPixelReceivedAt = now; + if (this.frameRequestStartedAt > 0) { + const sample = Math.max(0, now - this.frameRequestStartedAt); + this.frameRoundTripMs = this.frameRoundTripMs > 0 ? this.frameRoundTripMs * 0.8 + sample * 0.2 : sample; + this.frameRequestStartedAt = 0; + } + if (this.latestPixelBuffer !== null) this.overwrittenPixelFrames++; this.latestPixelBuffer = buffer; this.setSocketState("ready", `${this.mode.strategy || "Core2"} 在线`); this.transportFps = this.recordRate(this.transportTimes, now); this.frameCount++; this.frameLabel.textContent = this.frameCount.toLocaleString(); this.updateMotionStatus(now); - this.scheduleFramePump(); + this.requestFrame(now); } presentLatest(time) { const buffer = this.latestPixelBuffer; @@ -371,14 +376,12 @@ class GalleryCard { requestFrame(time, explicit = false) { if ((!explicit && !this.streamActive()) || (explicit && !this.displayVisible()) || !this.ready || this.framePending || this.socket?.readyState !== WebSocket.OPEN) return; if (!explicit && this.mode.id === "manual") return; - 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; - if (!explicit && this.mode.id !== "low_latency" && time - this.lastFrameRequest < interval) return; - this.lastFrameRequest = time; + this.frameRequestStartedAt = time; this.framePending = true; this.send("frame"); this.frameTimeout = setTimeout(() => { this.framePending = false; + this.frameRequestStartedAt = 0; this.frameTimeout = null; this.frameTimeoutCount++; if (!this.streamActive() || this.socket?.readyState !== WebSocket.OPEN) return; @@ -386,18 +389,6 @@ class GalleryCard { this.syncActivity(true); }, 1500); } - scheduleFramePump(reset = false) { - if (this.mode.id !== "low_latency") return; - if (reset) this.stopFramePump(); - if (this.frameTimer !== null || this.framePending || !this.streamActive() || !this.ready || this.socket?.readyState !== WebSocket.OPEN) return; - 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 delay = Math.max(0, Math.ceil(this.lastFrameRequest + interval - performance.now())); - this.frameTimer = setTimeout(() => { - this.frameTimer = null; - this.requestFrame(performance.now()); - }, delay); - } observe(time) { if (!this.ready || !this.displayVisible() || this.socket?.readyState !== WebSocket.OPEN || time - this.lastObserveRequest < 650) return; this.lastObserveRequest = time; @@ -410,6 +401,9 @@ class GalleryCard { changed_pixel_frames: this.changedPixelFrames, duplicate_pixel_frames: this.duplicatePixelFrames, frame_request_timeout_count: this.frameTimeoutCount, + frame_round_trip_ms: this.frameRoundTripMs, + display_interval_ms: displayIntervalMs, + overwritten_pixel_frames: this.overwrittenPixelFrames, 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 }}); @@ -595,6 +589,11 @@ function connectCatalog() { }); } function loop(time) { + if (lastAnimationFrameAt > 0) { + const sample = Math.max(0, time - lastAnimationFrameAt); + displayIntervalMs = displayIntervalMs > 0 ? displayIntervalMs * 0.8 + sample * 0.2 : sample; + } + lastAnimationFrameAt = time; const page = pages.get(activeMode); for (const card of page?.cards || []) { card.presentLatest(time); @@ -614,8 +613,12 @@ 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(); })); 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("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(); } }); +document.addEventListener("visibilitychange", () => { + lastAnimationFrameAt = 0; + displayIntervalMs = 0; + syncCardActivity(); +}); +window.addEventListener("beforeunload", () => { for (const page of pages.values()) for (const card of page.cards) { card.disposed = true; clearTimeout(card.frameTimeout); clearTimeout(card.reconnectTimer); card.send("hide"); card.socket?.close(); } }); connectCatalog(); requestAnimationFrame(loop); diff --git a/webapp_gallery/index.html b/webapp_gallery/index.html index 06f1b3b..34012f0 100644 --- a/webapp_gallery/index.html +++ b/webapp_gallery/index.html @@ -81,6 +81,8 @@
0.0
后端渲染 FPS
0.0
像素响应 FPS
0.0
浏览器呈现 FPS
+
0.00
WS 往返 ms
+
0
未呈现覆盖
0.00
Core 渲染 ms
0.00
像素编码 ms
0.0
响应负载 MB/s