diff --git a/web_server/Gallery_Plot_Session.cpp b/web_server/Gallery_Plot_Session.cpp index d21f6c1..9c2fd85 100644 --- a/web_server/Gallery_Plot_Session.cpp +++ b/web_server/Gallery_Plot_Session.cpp @@ -40,6 +40,9 @@ double number_value(const Gallery_State& state, std::string_view id) { std::uint64_t milliseconds_to_ns(double milliseconds) { return milliseconds > 0.0 ? static_cast(std::llround(milliseconds * 1'000'000.0)) : 0; } +std::uint64_t frequency_to_ns(double frequency_hz) { + return frequency_hz > 0.0 ? static_cast(std::llround(1'000'000'000.0 / frequency_hz)) : 0; +} int integer_value(const Gallery_State& state, std::string_view id) { return static_cast(std::lround(number_value(state, id))); @@ -262,14 +265,7 @@ public: 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 && - bool_value(state_, "consumer_feedback_enabled")) { - const auto display_interval_ns = milliseconds_to_ns(client_display_interval_ms_); - const auto interval_ns = display_interval_ns != 0 ? - display_interval_ns : milliseconds_to_ns(client_frame_round_trip_ms_); - if (interval_ns != 0) - plot_.set_consumer_feedback({interval_ns}); - } + apply_consumer_feedback(); } [[nodiscard]] bool requires_rebuild(const Gallery_State& candidate) const { @@ -292,7 +288,11 @@ public: const bool render_schedule_changed = frame_mode_ == Gallery_Frame_Mode::Low_Latency && (bool_value(state_, "frequency_limit_enabled") != bool_value(state, "frequency_limit_enabled") || number_value(state_, "max_render_fps") != number_value(state, "max_render_fps") || - bool_value(state_, "consumer_feedback_enabled") != bool_value(state, "consumer_feedback_enabled")); + bool_value(state_, "consumer_feedback_enabled") != bool_value(state, "consumer_feedback_enabled") || + bool_value(state_, "consumer_pixel_feedback_enabled") != bool_value(state, "consumer_pixel_feedback_enabled") || + bool_value(state_, "consumer_presentation_feedback_enabled") != bool_value(state, "consumer_presentation_feedback_enabled") || + bool_value(state_, "consumer_manual_feedback_enabled") != bool_value(state, "consumer_manual_feedback_enabled") || + number_value(state_, "consumer_manual_fps") != number_value(state, "consumer_manual_fps")); state_ = std::move(state); apply_state(); if (render_schedule_changed) @@ -654,6 +654,24 @@ public: {"target_interval_ns", observer.target_interval_ns}, {"frequency_limit_enabled", observer.frequency_limit_enabled}, {"consumer_feedback_enabled", observer.consumer_feedback_enabled}, + {"consumer_feedback_master_enabled", + frame_mode_ == Gallery_Frame_Mode::Low_Latency && + bool_value(state_, "consumer_feedback_enabled")}, + {"consumer_pixel_feedback_enabled", + frame_mode_ == Gallery_Frame_Mode::Low_Latency && + bool_value(state_, "consumer_pixel_feedback_enabled")}, + {"consumer_presentation_feedback_enabled", + frame_mode_ == Gallery_Frame_Mode::Low_Latency && + bool_value(state_, "consumer_presentation_feedback_enabled")}, + {"consumer_manual_feedback_enabled", + frame_mode_ == Gallery_Frame_Mode::Low_Latency && + bool_value(state_, "consumer_manual_feedback_enabled")}, + {"consumer_manual_fps", frame_mode_ == Gallery_Frame_Mode::Low_Latency ? + number_value(state_, "consumer_manual_fps") : 0.0}, + {"consumer_feedback_source", consumer_feedback_source_}, + {"consumer_pixel_interval_ns", consumer_pixel_interval_ns_}, + {"consumer_presentation_interval_ns", consumer_presentation_interval_ns_}, + {"consumer_manual_interval_ns", consumer_manual_interval_ns_}, {"bottleneck_duration_ns", observer.bottleneck_duration_ns}, {"consumer_sample_interval_ns", observer.consumer_sample_interval_ns}, {"consumer_smoothed_interval_ns", observer.consumer_smoothed_interval_ns}, @@ -703,7 +721,15 @@ public: {"configured_frequency_hz", observer.frequency_hz}, {"frequency_limit_enabled", observer.frequency_limit_enabled}, {"consumer_feedback_enabled", observer.consumer_feedback_enabled}, - {"consumer_feedback_source", "web_runtime"}, + {"consumer_feedback_master_enabled", bool_value(state_, "consumer_feedback_enabled")}, + {"consumer_pixel_feedback_enabled", bool_value(state_, "consumer_pixel_feedback_enabled")}, + {"consumer_presentation_feedback_enabled", bool_value(state_, "consumer_presentation_feedback_enabled")}, + {"consumer_manual_feedback_enabled", bool_value(state_, "consumer_manual_feedback_enabled")}, + {"consumer_manual_fps", number_value(state_, "consumer_manual_fps")}, + {"consumer_feedback_source", consumer_feedback_source_}, + {"consumer_pixel_interval_ns", consumer_pixel_interval_ns_}, + {"consumer_presentation_interval_ns", consumer_presentation_interval_ns_}, + {"consumer_manual_interval_ns", consumer_manual_interval_ns_}, {"target_interval_ns", observer.target_interval_ns}, {"paint_duration_ns", observer.paint_duration_ns}, {"render_duration_ns", observer.render_duration_ns}, @@ -910,6 +936,47 @@ private: return static_cast(bytes) / seconds / 1e6; } + void apply_consumer_feedback() { + if (frame_mode_ != Gallery_Frame_Mode::Low_Latency) + return; + consumer_pixel_interval_ns_ = frequency_to_ns(client_transport_fps_); + consumer_presentation_interval_ns_ = milliseconds_to_ns(client_display_interval_ms_); + consumer_manual_interval_ns_ = frequency_to_ns(number_value(state_, "consumer_manual_fps")); + consumer_feedback_source_ = "none"; + if (!bool_value(state_, "consumer_feedback_enabled")) { + plot_.clear_consumer_feedback(); + consumer_feedback_source_ = "disabled"; + return; + } + std::uint64_t interval_ns = 0; + const auto select_source = [&interval_ns, this](bool enabled, std::uint64_t candidate, + std::string_view source) { + if (!enabled || candidate == 0) + return; + if (candidate > interval_ns) { + interval_ns = candidate; + consumer_feedback_source_ = source; + return; + } + if (candidate == interval_ns) { + if (consumer_feedback_source_ != "none") + consumer_feedback_source_ += '+'; + consumer_feedback_source_ += source; + } + }; + select_source(bool_value(state_, "consumer_pixel_feedback_enabled"), + consumer_pixel_interval_ns_, "pixel"); + select_source(bool_value(state_, "consumer_presentation_feedback_enabled"), + consumer_presentation_interval_ns_, "presentation"); + select_source(bool_value(state_, "consumer_manual_feedback_enabled"), + consumer_manual_interval_ns_, "manual"); + if (interval_ns == 0) { + plot_.clear_consumer_feedback(); + return; + } + plot_.set_consumer_feedback({interval_ns}); + } + void record_performance(std::chrono::steady_clock::time_point started, bool rendered) { const auto finished = std::chrono::steady_clock::now(); ++render_attempt_count_; @@ -1273,8 +1340,7 @@ private: plot_.set_max_render_fps(number_value(state_, "max_render_fps")); else plot_.clear_max_render_fps(); - if (!bool_value(state_, "consumer_feedback_enabled")) - plot_.clear_consumer_feedback(); + apply_consumer_feedback(); } set_performance_plot_name(plot_, case_id_ + "/" + std::string(frame_mode_name(frame_mode_))); set_performance_overlay_enabled(plot_, bool_value(state_, "performance_overlay")); @@ -1520,6 +1586,10 @@ private: double client_display_interval_latest_ms_{}; double client_display_interval_p95_ms_{}; double client_display_jitter_ms_{}; + std::uint64_t consumer_pixel_interval_ns_{}; + std::uint64_t consumer_presentation_interval_ns_{}; + std::uint64_t consumer_manual_interval_ns_{}; + std::string consumer_feedback_source_{"none"}; std::uint64_t client_overwritten_pixel_frames_{}; double client_last_pixel_receive_age_ms_{}; double client_last_pixel_change_age_ms_{}; diff --git a/web_server/Gallery_Protocol.cpp b/web_server/Gallery_Protocol.cpp index 58e138f..1a2a517 100644 --- a/web_server/Gallery_Protocol.cpp +++ b/web_server/Gallery_Protocol.cpp @@ -131,7 +131,20 @@ void append_common(std::vector& result, Gallery_Frame_Mode f "仅在启用频率上限时参与调度。")); result.push_back(flag("consumer_feedback_enabled", "启用消费者反馈", "Plot_Core::set_consumer_feedback / clear_consumer_feedback", true, - "Low_Latency_Strategy", "关闭后消费者反馈不参与调度。")); + "Low_Latency_Strategy", "消费者反馈总开关;关闭后所有消费者来源都不参与调度。")); + result.push_back(flag("consumer_pixel_feedback_enabled", "启用像素响应反馈", + "Gallery_Plot_Session::set_client_metrics", false, + "Low_Latency_Strategy", "启用后按浏览器实际收到像素帧的 FPS 计算消费者周期。")); + result.push_back(flag("consumer_presentation_feedback_enabled", "启用浏览器呈现反馈", + "Gallery_Plot_Session::set_client_metrics", true, + "Low_Latency_Strategy", "启用后按浏览器 RAF 中位周期计算消费者周期。")); + result.push_back(flag("consumer_manual_feedback_enabled", "启用手动消费者反馈", + "Gallery_Plot_Session::apply_consumer_feedback", false, + "Low_Latency_Strategy", "启用后手动消费者 FPS 参与消费者限速。")); + result.push_back(number("consumer_manual_fps", "手动消费者 FPS", + "Gallery_Plot_Session::apply_consumer_feedback", + 60, 0.01, 1'000'000'000, 1, "Low_Latency_Strategy", false, + "仅在启用手动消费者反馈时参与调度。")); } 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 55af6e7..26fae37 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -1076,7 +1076,7 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback EXPECT_EQ(observer.at("consumer_safety_interval_ns"), 20'000'000); EXPECT_EQ(observer.at("consumer_interval_ns"), 20'000'000); EXPECT_EQ(observer.at("limit_state"), "consumer_limited"); - EXPECT_EQ(limit.at("consumer_feedback_source"), "web_runtime"); + EXPECT_EQ(limit.at("consumer_feedback_source"), "presentation"); observed = response_json(session.handle(gallery_request( Gallery_Request_Kind::Observe, R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":45,"presentation_fps":45,"websocket_buffered_bytes":0,"changed_pixel_frames":20,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":4,"display_interval_ms":22,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); @@ -1103,6 +1103,52 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback EXPECT_EQ(rejected.at("type"), "error"); } +TEST(RenderiveWebGallery, ConsumerFeedbackSourcesCanBeSelectedIndependentlyAndManually) { + 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"); + ASSERT_EQ(patch_controls(session, { + {"frequency_limit_enabled", false}, + {"consumer_feedback_enabled", true}, + {"consumer_pixel_feedback_enabled", true}, + {"consumer_presentation_feedback_enabled", false}, + {"consumer_manual_feedback_enabled", false} + }).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":25,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":1,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":5,"display_interval_ms":16,"display_interval_latest_ms":16,"display_interval_p95_ms":17,"display_jitter_ms":1,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); + auto observer = observed.at("telemetry").at("kernel_observer"); + EXPECT_EQ(observer.at("consumer_feedback_source"), "pixel"); + EXPECT_EQ(observer.at("consumer_pixel_interval_ns"), 40'000'000); + EXPECT_EQ(observer.at("consumer_presentation_interval_ns"), 16'000'000); + EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 40'000'000); + observed = patch_controls(session, { + {"consumer_pixel_feedback_enabled", false}, + {"consumer_presentation_feedback_enabled", true} + }); + observer = observed.at("telemetry").at("kernel_observer"); + EXPECT_EQ(observer.at("consumer_feedback_source"), "presentation"); + EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 16'000'000); + observed = patch_controls(session, { + {"consumer_presentation_feedback_enabled", false}, + {"consumer_manual_feedback_enabled", true}, + {"consumer_manual_fps", 20.0} + }); + observer = observed.at("telemetry").at("kernel_observer"); + EXPECT_EQ(observer.at("consumer_feedback_source"), "manual"); + EXPECT_EQ(observer.at("consumer_manual_interval_ns"), 50'000'000); + EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 50'000'000); + observed = patch_controls(session, { + {"consumer_manual_feedback_enabled", false} + }); + observer = observed.at("telemetry").at("kernel_observer"); + EXPECT_EQ(observer.at("consumer_feedback_source"), "none"); + EXPECT_FALSE(observer.at("consumer_feedback_enabled").get()); + EXPECT_TRUE(observer.at("consumer_feedback_master_enabled").get()); + EXPECT_EQ(observer.at("consumer_interval_ns"), 0); +} + TEST(RenderiveWebGallery, PlaybackStrategyQueuesWithoutAutomaticConsumptionAndReportsEmptyQueue) { Gallery_Plot_Session session(true); const auto opened = response_json(session.handle(gallery_request( diff --git a/webapp_gallery/app.js b/webapp_gallery/app.js index 8769924..869e862 100644 --- a/webapp_gallery/app.js +++ b/webapp_gallery/app.js @@ -289,20 +289,44 @@ class GalleryCard { updateObserverDashboard(observer) { const nanosecondFields = new Set([ "paint_duration_ns", "render_duration_ns", "target_interval_ns", - "bottleneck_duration_ns", "consumer_sample_interval_ns", "consumer_smoothed_interval_ns", + "bottleneck_duration_ns", "consumer_pixel_interval_ns", "consumer_presentation_interval_ns", + "consumer_manual_interval_ns", "consumer_sample_interval_ns", "consumer_smoothed_interval_ns", "consumer_variation_ns", "consumer_safety_interval_ns", "consumer_interval_ns", "next_refresh_interval_ns", "paint_lease_wait_ns", "paint_state_wait_ns", "publish_state_wait_ns", "ready_wait_ns", "frame_age_at_render_ns", "render_lease_wait_ns", "render_state_wait_ns", "render_finish_state_wait_ns", "queue_wait_ns", "end_to_end_ns" ]); + const booleanFields = new Set([ + "consumer_feedback_master_enabled", "consumer_feedback_enabled", + "consumer_pixel_feedback_enabled", "consumer_presentation_feedback_enabled", + "consumer_manual_feedback_enabled" + ]); for (const [name, field] of this.observerFields) { - const raw = observer[name] ?? (name === "limit_state" ? "not_applicable" : name === "last_event" ? "none" : 0); + const raw = observer[name] ?? (name === "limit_state" ? "not_applicable" : + name === "last_event" || name === "consumer_feedback_source" ? "none" : 0); if (name === "consumer_effective_fps") { const interval = Number(observer.consumer_interval_ns || 0); field.textContent = interval > 0 ? `${(1e9 / interval).toFixed(2)} FPS` : "0 FPS"; field.title = interval > 0 ? `${1e9 / interval} FPS` : "0 FPS"; continue; } + if (name === "consumer_manual_fps") { + field.textContent = `${Number(raw || 0).toFixed(2)} FPS`; + field.title = `${Number(raw || 0)} FPS`; + continue; + } + if (name === "consumer_feedback_source") { + const names = {disabled: "总开关关闭", none: "无", pixel: "像素响应", + presentation: "浏览器呈现", manual: "手动"}; + field.textContent = String(raw).split("+").map(value => names[value] || value).join(" + "); + field.title = String(raw); + continue; + } + if (booleanFields.has(name)) { + field.textContent = raw ? "启用" : "关闭"; + field.title = String(Boolean(raw)); + continue; + } field.textContent = nanosecondFields.has(name) ? formatNanoseconds(raw) : name === "configured_frequency_hz" ? (observer.frequency_limit_enabled === false ? "已关闭" : `${Number(raw || 0).toLocaleString()} Hz`) : typeof raw === "number" ? raw.toLocaleString() : String(raw); diff --git a/webapp_gallery/index.html b/webapp_gallery/index.html index 36b5aa1..a45bd59 100644 --- a/webapp_gallery/index.html +++ b/webapp_gallery/index.html @@ -121,6 +121,16 @@
PaintEvent0 ns
后台渲染0 ns
内部瓶颈0 ns
+
消费者总开关关闭
+
Kernel 反馈有效关闭
+
像素响应反馈关闭
+
浏览器呈现反馈关闭
+
手动消费者反馈关闭
+
手动消费者 FPS0 FPS
+
生效反馈来源none
+
像素响应周期0 ns
+
浏览器呈现周期0 ns
+
手动消费者周期0 ns
消费者原始采样0 ns
消费者平滑周期0 ns
消费者抖动0 ns