From 6c8c205e866ec0d0561e068bc6156b2cc90fe506 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Tue, 11 Aug 2026 11:50:17 +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 --- Core2/plot/Plot_Core.cpp | 1 + Core2/plot/Plot_Core.h | 1 + Core2/tests/Core2_Frame_Pipeline_Tests.cpp | 1 + .../low_latency/Low_Latency_Strategy.hpp | 1 + .../low_latency/Low_Latency_Strategy.inl | 21 ++++--- .../low_latency/Low_Latency_Strategy_Test.cpp | 28 ++++++++-- web_server/Gallery_Plot_Session.cpp | 30 +++++++++- web_server/tests/Web_Bridge_Tests.cpp | 19 ++++--- webapp_gallery/app.js | 55 +++++++++++++++++-- webapp_gallery/index.html | 28 ++++++++-- webapp_gallery/styles.css | 8 +-- 11 files changed, 157 insertions(+), 36 deletions(-) diff --git a/Core2/plot/Plot_Core.cpp b/Core2/plot/Plot_Core.cpp index 772b90a..18eb7ef 100644 --- a/Core2/plot/Plot_Core.cpp +++ b/Core2/plot/Plot_Core.cpp @@ -546,6 +546,7 @@ Frame_Observer_Snapshot Plot_Core::frame_observer_snapshot() const { snapshot.consumer_sample_interval_ns = state.consumer_sample_interval_ns; snapshot.consumer_smoothed_interval_ns = state.consumer_smoothed_interval_ns; snapshot.consumer_variation_ns = state.consumer_variation_ns; + snapshot.consumer_safety_interval_ns = state.consumer_safety_interval_ns; snapshot.consumer_interval_ns = state.consumer_interval_ns; snapshot.next_refresh_interval_ns = state.next_refresh_interval_ns; snapshot.end_to_end_ns = state.end_to_end_ns; diff --git a/Core2/plot/Plot_Core.h b/Core2/plot/Plot_Core.h index abbb68a..c10a5b4 100644 --- a/Core2/plot/Plot_Core.h +++ b/Core2/plot/Plot_Core.h @@ -56,6 +56,7 @@ struct Frame_Observer_Snapshot { std::uint64_t consumer_sample_interval_ns{}; std::uint64_t consumer_smoothed_interval_ns{}; std::uint64_t consumer_variation_ns{}; + std::uint64_t consumer_safety_interval_ns{}; std::uint64_t consumer_interval_ns{}; std::uint64_t next_refresh_interval_ns{}; std::uint64_t paint_lease_wait_ns{}; diff --git a/Core2/tests/Core2_Frame_Pipeline_Tests.cpp b/Core2/tests/Core2_Frame_Pipeline_Tests.cpp index 3f77495..734f312 100644 --- a/Core2/tests/Core2_Frame_Pipeline_Tests.cpp +++ b/Core2/tests/Core2_Frame_Pipeline_Tests.cpp @@ -96,6 +96,7 @@ TEST(Renderive_Core2_Frame_Pipeline, LowLatencyConsumerFeedbackFlowsThroughCore2 EXPECT_EQ(snapshot.consumer_sample_interval_ns, 40'000'000u); EXPECT_EQ(snapshot.consumer_smoothed_interval_ns, 40'000'000u); EXPECT_EQ(snapshot.consumer_variation_ns, 0u); + EXPECT_EQ(snapshot.consumer_safety_interval_ns, 40'000'000u); EXPECT_EQ(snapshot.consumer_interval_ns, 40'000'000u); EXPECT_EQ(snapshot.next_refresh_interval_ns, 40'000'000u); EXPECT_EQ(plot.refresh_feedback_snapshot().next_refresh_interval_ns, 40'000'000u); diff --git a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp index da7ca37..f3129d4 100644 --- a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp @@ -78,6 +78,7 @@ public: std::uint64_t consumer_sample_interval_ns{}; std::uint64_t consumer_smoothed_interval_ns{}; std::uint64_t consumer_variation_ns{}; + std::uint64_t consumer_safety_interval_ns{}; std::uint64_t consumer_interval_ns{}; std::uint64_t end_to_end_ns{}; std::uint64_t next_refresh_interval_ns{}; diff --git a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl index c578829..c9c82bf 100644 --- a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl @@ -248,6 +248,7 @@ void Low_Latency_Strategy::clear_consumer_feedback state_.consumer_sample_interval_ns = 0; state_.consumer_smoothed_interval_ns = 0; state_.consumer_variation_ns = 0; + state_.consumer_safety_interval_ns = 0; state_.consumer_interval_ns = 0; update_refresh_control(); } @@ -368,29 +369,31 @@ void Low_Latency_Strategy::update_refresh_control( } template void Low_Latency_Strategy::update_consumer_estimator(std::uint64_t sample_interval_ns) { + const std::uint64_t previous_sample = state_.consumer_sample_interval_ns; state_.consumer_sample_interval_ns = sample_interval_ns; if (!state_.consumer_feedback_enabled) { state_.consumer_feedback_enabled = true; state_.consumer_smoothed_interval_ns = sample_interval_ns; state_.consumer_variation_ns = 0; } else { - const std::uint64_t smoothed = state_.consumer_smoothed_interval_ns; - const std::uint64_t deviation = sample_interval_ns >= smoothed ? - sample_interval_ns - smoothed : smoothed - sample_interval_ns; - if (deviation >= state_.consumer_variation_ns) - state_.consumer_variation_ns += (deviation - state_.consumer_variation_ns) / 4; + const std::uint64_t sample_delta = sample_interval_ns >= previous_sample ? + sample_interval_ns - previous_sample : previous_sample - sample_interval_ns; + if (sample_delta >= state_.consumer_variation_ns) + state_.consumer_variation_ns += (sample_delta - state_.consumer_variation_ns) / 16; else - state_.consumer_variation_ns -= (state_.consumer_variation_ns - deviation) / 4; + state_.consumer_variation_ns -= (state_.consumer_variation_ns - sample_delta) / 16; if (sample_interval_ns >= state_.consumer_smoothed_interval_ns) - state_.consumer_smoothed_interval_ns += (sample_interval_ns - state_.consumer_smoothed_interval_ns) / 8; + state_.consumer_smoothed_interval_ns += (sample_interval_ns - state_.consumer_smoothed_interval_ns) / 2; else state_.consumer_smoothed_interval_ns -= (state_.consumer_smoothed_interval_ns - sample_interval_ns) / 8; } const auto maximum = std::numeric_limits::max(); const std::uint64_t variation_margin = state_.consumer_variation_ns > maximum / 4 ? maximum : state_.consumer_variation_ns * 4; - state_.consumer_interval_ns = state_.consumer_smoothed_interval_ns > maximum - variation_margin ? - maximum : state_.consumer_smoothed_interval_ns + variation_margin; + const std::uint64_t safety_interval = state_.consumer_smoothed_interval_ns > maximum - variation_margin ? + maximum : state_.consumer_smoothed_interval_ns + variation_margin; + state_.consumer_interval_ns = std::max(state_.consumer_smoothed_interval_ns, sample_interval_ns); + state_.consumer_safety_interval_ns = std::max(state_.consumer_interval_ns, safety_interval); } template void Low_Latency_Strategy::update_state(const Frame& frame) { diff --git a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp index 22a1813..1b05f10 100644 --- a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp @@ -125,7 +125,7 @@ TEST(low_latency_strategy_test, clamps_extremely_small_frequency_interval_withou } EXPECT_EQ(strategy.state().target_interval_ns, std::numeric_limits::max()); } -TEST(low_latency_strategy_test, smooths_consumer_feedback_in_kernel) { +TEST(low_latency_strategy_test, separates_consumer_rate_from_jitter_safety_margin) { Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); @@ -136,15 +136,34 @@ TEST(low_latency_strategy_test, smooths_consumer_feedback_in_kernel) { EXPECT_EQ(state.consumer_sample_interval_ns, 10'000'000u); EXPECT_EQ(state.consumer_smoothed_interval_ns, 10'000'000u); EXPECT_EQ(state.consumer_variation_ns, 0u); + EXPECT_EQ(state.consumer_safety_interval_ns, 10'000'000u); EXPECT_EQ(state.consumer_interval_ns, 10'000'000u); strategy.set_consumer_feedback({14'000'000}); state = strategy.state(); EXPECT_EQ(state.consumer_sample_interval_ns, 14'000'000u); - EXPECT_EQ(state.consumer_smoothed_interval_ns, 10'500'000u); - EXPECT_EQ(state.consumer_variation_ns, 1'000'000u); - EXPECT_EQ(state.consumer_interval_ns, 14'500'000u); + EXPECT_EQ(state.consumer_smoothed_interval_ns, 12'000'000u); + EXPECT_EQ(state.consumer_variation_ns, 250'000u); + EXPECT_EQ(state.consumer_safety_interval_ns, 14'000'000u); + EXPECT_EQ(state.consumer_interval_ns, 14'000'000u); + EXPECT_EQ(state.next_refresh_interval_ns, 14'000'000u); EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::consumer_limited); } +TEST(low_latency_strategy_test, consumer_jitter_changes_safety_deadline_without_becoming_rate_limit) { + Low_Latency_Test_Time_Source time_source; + Low_Latency_Test_Observer observer; + auto strategy = make_low_latency_test_strategy(time_source, observer); + strategy.set_frequency_hz(1000.0); + strategy.set_consumer_feedback({20'000'000}); + strategy.set_consumer_feedback({22'000'000}); + strategy.set_consumer_feedback({20'000'000}); + const auto state = strategy.state(); + EXPECT_EQ(state.consumer_sample_interval_ns, 20'000'000u); + EXPECT_EQ(state.consumer_smoothed_interval_ns, 20'875'000u); + EXPECT_EQ(state.consumer_variation_ns, 242'187u); + EXPECT_EQ(state.consumer_interval_ns, 20'875'000u); + EXPECT_EQ(state.consumer_safety_interval_ns, 21'843'748u); + EXPECT_EQ(state.next_refresh_interval_ns, 20'875'000u); +} TEST(low_latency_strategy_test, allows_frequency_and_consumer_limits_to_be_cleared) { Low_Latency_Test_Time_Source time_source; Low_Latency_Test_Observer observer; @@ -154,6 +173,7 @@ TEST(low_latency_strategy_test, allows_frequency_and_consumer_limits_to_be_clear strategy.clear_consumer_feedback(); auto state = strategy.state(); EXPECT_FALSE(state.consumer_feedback_enabled); + EXPECT_EQ(state.consumer_safety_interval_ns, 0u); EXPECT_EQ(state.consumer_interval_ns, 0u); EXPECT_EQ(state.limit_state, Low_Latency_Test_Strategy::Limit_State::frequency_limited); strategy.clear_frequency_limit(); diff --git a/web_server/Gallery_Plot_Session.cpp b/web_server/Gallery_Plot_Session.cpp index e328e7d..d21f6c1 100644 --- a/web_server/Gallery_Plot_Session.cpp +++ b/web_server/Gallery_Plot_Session.cpp @@ -233,6 +233,9 @@ public: std::uint64_t frame_request_timeout_count, double frame_round_trip_ms, double display_interval_ms, + double display_interval_latest_ms, + double display_interval_p95_ms, + double display_jitter_ms, std::uint64_t overwritten_pixel_frames, double last_pixel_receive_age_ms, double last_pixel_change_age_ms) noexcept { @@ -248,6 +251,12 @@ public: 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_display_interval_latest_ms_ = std::isfinite(display_interval_latest_ms) ? + std::max(0.0, display_interval_latest_ms) : 0.0; + client_display_interval_p95_ms_ = std::isfinite(display_interval_p95_ms) ? + std::max(0.0, display_interval_p95_ms) : 0.0; + client_display_jitter_ms_ = std::isfinite(display_jitter_ms) ? + std::max(0.0, display_jitter_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; @@ -255,8 +264,9 @@ public: 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 interval_ns = std::max(milliseconds_to_ns(client_frame_round_trip_ms_), - milliseconds_to_ns(client_display_interval_ms_)); + 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}); } @@ -648,6 +658,7 @@ public: {"consumer_sample_interval_ns", observer.consumer_sample_interval_ns}, {"consumer_smoothed_interval_ns", observer.consumer_smoothed_interval_ns}, {"consumer_variation_ns", observer.consumer_variation_ns}, + {"consumer_safety_interval_ns", observer.consumer_safety_interval_ns}, {"consumer_interval_ns", observer.consumer_interval_ns}, {"next_refresh_interval_ns", observer.next_refresh_interval_ns}, {"paint_lease_wait_ns", observer.paint_lease_wait_ns}, @@ -670,6 +681,9 @@ public: {"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_}, + {"display_interval_latest_ms", client_display_interval_latest_ms_}, + {"display_interval_p95_ms", client_display_interval_p95_ms_}, + {"display_jitter_ms", client_display_jitter_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_} @@ -697,6 +711,7 @@ public: {"consumer_sample_interval_ns", observer.consumer_sample_interval_ns}, {"consumer_smoothed_interval_ns", observer.consumer_smoothed_interval_ns}, {"consumer_variation_ns", observer.consumer_variation_ns}, + {"consumer_safety_interval_ns", observer.consumer_safety_interval_ns}, {"consumer_interval_ns", observer.consumer_interval_ns}, {"scheduler_interval_ns", scheduler_interval_ns}, {"scheduler_frequency_hz", scheduler_interval_ns == 0 ? 0.0 : @@ -956,6 +971,7 @@ private: {"consumer_feedback_sample_interval_ns", low_latency ? observer.consumer_sample_interval_ns : 0}, {"consumer_feedback_smoothed_interval_ns", low_latency ? observer.consumer_smoothed_interval_ns : 0}, {"consumer_feedback_variation_ns", low_latency ? observer.consumer_variation_ns : 0}, + {"consumer_feedback_safety_interval_ns", low_latency ? observer.consumer_safety_interval_ns : 0}, {"consumer_feedback_interval_ns", low_latency ? observer.consumer_interval_ns : 0}, {"backend_render_fps", recent_rate(render_history_, now)}, {"pixel_response_fps", pixel_fps}, @@ -966,6 +982,9 @@ private: {"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_display_interval_latest_ms", client_display_interval_latest_ms_}, + {"client_display_interval_p95_ms", client_display_interval_p95_ms_}, + {"client_display_jitter_ms", client_display_jitter_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_}, @@ -994,6 +1013,7 @@ private: {"consumer_sample_interval_ns", observer.consumer_sample_interval_ns}, {"consumer_smoothed_interval_ns", observer.consumer_smoothed_interval_ns}, {"consumer_variation_ns", observer.consumer_variation_ns}, + {"consumer_safety_interval_ns", observer.consumer_safety_interval_ns}, {"consumer_interval_ns", observer.consumer_interval_ns}, {"next_refresh_interval_ns", observer.next_refresh_interval_ns}, {"paint_lease_wait_ns", observer.paint_lease_wait_ns}, @@ -1497,6 +1517,9 @@ private: std::uint64_t client_frame_request_timeout_count_{}; double client_frame_round_trip_ms_{}; double client_display_interval_ms_{}; + double client_display_interval_latest_ms_{}; + double client_display_interval_p95_ms_{}; + double client_display_jitter_ms_{}; std::uint64_t client_overwritten_pixel_frames_{}; double client_last_pixel_receive_age_ms_{}; double client_last_pixel_change_age_ms_{}; @@ -1560,6 +1583,9 @@ struct Gallery_Plot_Session::Impl { unsigned_metric("frame_request_timeout_count"), finite_metric("frame_round_trip_ms"), finite_metric("display_interval_ms"), + finite_metric("display_interval_latest_ms"), + finite_metric("display_interval_p95_ms"), + finite_metric("display_jitter_ms"), unsigned_metric("overwritten_pixel_frames"), finite_metric("last_pixel_receive_age_ms"), finite_metric("last_pixel_change_age_ms")); diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index dfa1a6f..55af6e7 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -849,13 +849,13 @@ TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers 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")); - constexpr std::array observer_fields{ + constexpr std::array observer_fields{ "mode", "last_event", "limit_state", "configured_frequency_hz", "observation_count", "produced_frame_count", "consumed_frame_count", "dropped_frame_count", "failed_operation_count", "pending_frame_count", "latest_sequence", "paint_duration_ns", "render_duration_ns", "target_interval_ns", "frequency_limit_enabled", "consumer_feedback_enabled", "bottleneck_duration_ns", "consumer_sample_interval_ns", - "consumer_smoothed_interval_ns", "consumer_variation_ns", "consumer_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", @@ -1065,7 +1065,7 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback "case_state"); auto observed = response_json(session.handle(gallery_request( Gallery_Request_Kind::Observe, - R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":50,"presentation_fps":50,"websocket_buffered_bytes":0,"changed_pixel_frames":10,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":2,"display_interval_ms":20,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); + R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":50,"presentation_fps":50,"websocket_buffered_bytes":0,"changed_pixel_frames":10,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":80,"display_interval_ms":20,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); auto observer = observed.at("telemetry").at("kernel_observer"); auto limit = observed.at("telemetry").at("low_latency_limit"); EXPECT_TRUE(observer.at("frequency_limit_enabled").get()); @@ -1073,6 +1073,7 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 20'000'000); EXPECT_EQ(observer.at("consumer_smoothed_interval_ns"), 20'000'000); EXPECT_EQ(observer.at("consumer_variation_ns"), 0); + 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"); @@ -1081,9 +1082,10 @@ TEST(RenderiveWebGallery, WebRuntimeMetricsPublishKernelSmoothedConsumerFeedback R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":45,"presentation_fps":45,"websocket_buffered_bytes":0,"changed_pixel_frames":20,"duplicate_pixel_frames":0,"frame_request_timeout_count":0,"frame_round_trip_ms":4,"display_interval_ms":22,"overwritten_pixel_frames":0,"last_pixel_receive_age_ms":1,"last_pixel_change_age_ms":1}})"))); observer = observed.at("telemetry").at("kernel_observer"); EXPECT_EQ(observer.at("consumer_sample_interval_ns"), 22'000'000); - EXPECT_EQ(observer.at("consumer_smoothed_interval_ns"), 20'250'000); - EXPECT_EQ(observer.at("consumer_variation_ns"), 500'000); - EXPECT_EQ(observer.at("consumer_interval_ns"), 22'250'000); + EXPECT_EQ(observer.at("consumer_smoothed_interval_ns"), 21'000'000); + EXPECT_EQ(observer.at("consumer_variation_ns"), 125'000); + EXPECT_EQ(observer.at("consumer_safety_interval_ns"), 22'000'000); + EXPECT_EQ(observer.at("consumer_interval_ns"), 22'000'000); observed = response_json(session.handle(gallery_request( Gallery_Request_Kind::Patch, R"({"category":"event","type":"gallery_patch","patch":{"consumer_feedback_enabled":false,"frequency_limit_enabled":false}})"))); @@ -1186,7 +1188,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,"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}})")); + R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":120,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":17,"duplicate_pixel_frames":3,"frame_request_timeout_count":2,"frame_round_trip_ms":4.5,"display_interval_ms":16.7,"display_interval_latest_ms":16.9,"display_interval_p95_ms":17.4,"display_jitter_ms":0.7,"overwritten_pixel_frames":5,"last_pixel_receive_age_ms":8.5,"last_pixel_change_age_ms":12.5}})")); ASSERT_TRUE(observed.has_value()); const auto telemetry = parse_json(observed->payload).at("telemetry"); const auto& performance = telemetry.at("performance"); @@ -1206,6 +1208,9 @@ TEST(RenderiveWebGallery, ProductionLowLatencySessionRendersWithoutPixelPulls) { 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_DOUBLE_EQ(telemetry.at("client_performance").at("display_interval_latest_ms"), 16.9); + EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("display_interval_p95_ms"), 17.4); + EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("display_jitter_ms"), 0.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); diff --git a/webapp_gallery/app.js b/webapp_gallery/app.js index d6936b5..8769924 100644 --- a/webapp_gallery/app.js +++ b/webapp_gallery/app.js @@ -29,6 +29,10 @@ let streamsPaused = false; let toastTimer = 0; let lastAnimationFrameAt = 0; let displayIntervalMs = 0; +let displayIntervalLatestMs = 0; +let displayIntervalP95Ms = 0; +let displayJitterMs = 0; +const displayIntervalSamples = []; const message = (type, payload = {}) => JSON.stringify({category: "event", type, ...payload}); const setConnection = (state, text) => { @@ -63,6 +67,25 @@ function formatNanoseconds(value) { if (nanoseconds < 1_000_000) return `${(nanoseconds / 1_000).toFixed(2)} µs`; return `${(nanoseconds / 1_000_000).toFixed(3)} ms`; } +function updateDisplayTiming(time) { + if (lastAnimationFrameAt > 0) { + displayIntervalLatestMs = Math.max(0, time - lastAnimationFrameAt); + displayIntervalSamples.push({time, interval: displayIntervalLatestMs}); + } + lastAnimationFrameAt = time; + while (displayIntervalSamples.length && displayIntervalSamples[0].time < time - 1000) displayIntervalSamples.shift(); + if (!displayIntervalSamples.length) { + displayIntervalMs = 0; + displayIntervalP95Ms = 0; + displayJitterMs = 0; + return; + } + const values = displayIntervalSamples.map(sample => sample.interval).sort((left, right) => left - right); + const percentile = ratio => values[Math.min(values.length - 1, Math.floor((values.length - 1) * ratio))]; + displayIntervalMs = percentile(0.5); + displayIntervalP95Ms = percentile(0.95); + displayJitterMs = Math.max(0, displayIntervalP95Ms - displayIntervalMs); +} class GalleryCard { constructor(definition, mode) { @@ -251,6 +274,7 @@ class GalleryCard { 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.updateClientDashboard(this.telemetry.client_performance || {}); this.updateMotionStatus(); const setLimitFlag = (selector, enabled, active, duration) => { const flag = this.node.querySelector(selector); @@ -266,19 +290,36 @@ class GalleryCard { const nanosecondFields = new Set([ "paint_duration_ns", "render_duration_ns", "target_interval_ns", "bottleneck_duration_ns", "consumer_sample_interval_ns", "consumer_smoothed_interval_ns", - "consumer_variation_ns", "consumer_interval_ns", "next_refresh_interval_ns", "paint_lease_wait_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" ]); for (const [name, field] of this.observerFields) { const raw = observer[name] ?? (name === "limit_state" ? "not_applicable" : name === "last_event" ? "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; + } 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); field.title = nanosecondFields.has(name) ? `${Number(raw || 0).toLocaleString()} ns` : String(raw); } } + updateClientDashboard(client) { + for (const field of this.node.querySelectorAll("[data-client-field]")) { + const name = field.dataset.clientField; + const raw = Number(client[name] || 0); + if (name.endsWith("_fps")) field.textContent = `${raw.toFixed(2)} FPS`; + else if (name.endsWith("_ms")) field.textContent = `${raw.toFixed(3)} ms`; + else if (name === "websocket_buffered_bytes") field.textContent = `${raw.toLocaleString()} B`; + else field.textContent = raw.toLocaleString(); + field.title = String(raw); + } + } pixelSignature(buffer, width, height, stride) { const bytes = new Uint8Array(buffer, 16, stride * height); let hash = (2166136261 ^ width ^ (height << 16)) >>> 0; @@ -409,6 +450,9 @@ class GalleryCard { frame_request_timeout_count: this.frameTimeoutCount, frame_round_trip_ms: this.frameRoundTripMs, display_interval_ms: displayIntervalMs, + display_interval_latest_ms: displayIntervalLatestMs, + display_interval_p95_ms: displayIntervalP95Ms, + display_jitter_ms: displayJitterMs, 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 @@ -627,10 +671,7 @@ function connectCatalog() { }); } function loop(time) { - if (lastAnimationFrameAt > 0) { - displayIntervalMs = Math.max(0, time - lastAnimationFrameAt); - } - lastAnimationFrameAt = time; + updateDisplayTiming(time); const page = pages.get(activeMode); for (const card of page?.cards || []) { card.presentLatest(time); @@ -653,6 +694,10 @@ document.addEventListener("pointerdown", event => { if (!elements.menu.hidden && document.addEventListener("visibilitychange", () => { lastAnimationFrameAt = 0; displayIntervalMs = 0; + displayIntervalLatestMs = 0; + displayIntervalP95Ms = 0; + displayJitterMs = 0; + displayIntervalSamples.length = 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(); } }); diff --git a/webapp_gallery/index.html b/webapp_gallery/index.html index 776500b..36b5aa1 100644 --- a/webapp_gallery/index.html +++ b/webapp_gallery/index.html @@ -89,7 +89,7 @@
0
Kernel 待处理
0
Kernel 丢弃
0→0
输入→绘制
-
N/A
当前瓶颈
+
N/A
当前瓶颈
none
观察事件
0
像素超时
最近像素龄
@@ -121,13 +121,31 @@
PaintEvent0 ns
后台渲染0 ns
内部瓶颈0 ns
-
消费原始样本0 ns
-
消费 SRTT0 ns
-
消费 RTTVAR0 ns
-
消费者反馈0 ns
+
消费者原始采样0 ns
+
消费者平滑周期0 ns
+
消费者抖动0 ns
+
消费者安全期限0 ns
+
消费者限速周期0 ns
+
消费者限速 FPS0 FPS
下次刷新0 ns
端到端延迟0 ns
+
+
RAF 最新周期0 ms
+
RAF 中位周期0 ms
+
RAF P95 周期0 ms
+
RAF P95-P50 抖动0 ms
+
WS 往返0 ms
+
像素响应 FPS0 FPS
+
浏览器呈现 FPS0 FPS
+
WS 缓冲0 B
+
未呈现覆盖0
+
变化像素帧0
+
重复像素帧0
+
像素请求超时0
+
最近像素龄0 ms
+
最近变化龄0 ms
+
Painter Lease 等待0 ns
Paint State 等待0 ns
diff --git a/webapp_gallery/styles.css b/webapp_gallery/styles.css index 6a1f268..6a4ddb7 100644 --- a/webapp_gallery/styles.css +++ b/webapp_gallery/styles.css @@ -62,9 +62,9 @@ button:hover { border-color: var(--accent); } button:active { transform: transla .kernel-observer-panel { border-bottom:1px solid var(--line); background:#080d14; font-family:ui-monospace,monospace; } .kernel-observer-panel > header { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:9px 11px; border-bottom:1px solid var(--line); color:var(--muted); font-size:8px; flex-wrap:wrap; } .kernel-observer-panel > header div:first-child { display:flex; align-items:center; gap:9px; min-width:0; flex-wrap:wrap; }.kernel-observer-panel > header span { color:var(--accent); letter-spacing:.08em; overflow-wrap:anywhere; }.kernel-observer-panel > header strong,.kernel-observer-panel > header b { color:var(--text); font-weight:700; overflow-wrap:anywhere; } -.observer-counters,.latency-summary,.latency-details { display:grid; gap:1px; background:var(--line); } -.observer-counters { grid-template-columns:repeat(auto-fit,minmax(90px,1fr)); }.latency-summary { grid-template-columns:repeat(auto-fit,minmax(105px,1fr)); border-top:1px solid var(--line); }.latency-details { grid-template-columns:repeat(auto-fit,minmax(145px,1fr)); border-top:1px solid var(--line); } -.observer-counters div,.latency-summary div,.latency-details div { min-width:0; padding:8px 7px; background:#0b1119; } +.observer-counters,.latency-summary,.client-summary,.latency-details { display:grid; gap:1px; background:var(--line); } +.observer-counters { grid-template-columns:repeat(auto-fit,minmax(90px,1fr)); }.latency-summary,.client-summary { grid-template-columns:repeat(auto-fit,minmax(105px,1fr)); border-top:1px solid var(--line); }.latency-details { grid-template-columns:repeat(auto-fit,minmax(145px,1fr)); border-top:1px solid var(--line); } +.observer-counters div,.latency-summary div,.client-summary div,.latency-details div { min-width:0; padding:8px 7px; background:#0b1119; } .kernel-observer-panel small { display:block; min-width:0; overflow:visible; margin-bottom:5px; color:var(--muted); font-size:7px; line-height:1.35; text-overflow:clip; white-space:normal; overflow-wrap:anywhere; word-break:break-word; } .kernel-observer-panel b { display:block; min-width:0; overflow:visible; color:#b7c7dc; font-size:9px; line-height:1.35; text-overflow:clip; white-space:normal; overflow-wrap:anywhere; word-break:break-word; } .latency-summary b { color:var(--accent); font-size:10px; }.latency-summary .critical { background:#10201e; }.latency-summary .critical b { color:#72f3d9; } @@ -81,4 +81,4 @@ button:hover { border-color: var(--accent); } button:active { transform: transla .menu-footer { display:flex; justify-content:space-between; gap:14px; padding:10px 14px; border-top:1px solid var(--line); color:var(--muted); font-size:9px; }.menu-footer code { color:var(--accent); } .toast { position:fixed; z-index:140; left:50%; bottom:22px; max-width:min(560px,calc(100vw - 30px)); padding:11px 15px; border:1px solid var(--strong); border-radius:8px; background:var(--panel2); box-shadow:0 18px 50px #00000073; transform:translateX(-50%); font-size:11px; }.toast[data-error="true"] { border-color:var(--danger); } @media (max-width:980px) { .hero { grid-template-columns:1fr; }.gallery { grid-template-columns:1fr; }.mode-tabs button { flex-direction:column; align-items:flex-start; }.page-header { align-items:flex-start; flex-direction:column; }.page-description { text-align:left; } } -@media (max-width:650px) { .topbar { align-items:flex-start; }.topbar-actions { align-items:flex-end; flex-direction:column; }.connection-status span { display:none; }.metrics { grid-template-columns:repeat(2,1fr); }.mode-tabs { grid-template-columns:1fr; }.mode-tabs button { flex-direction:row; }.card-meta { grid-template-columns:1fr; }.performance-strip { grid-template-columns:repeat(auto-fit,minmax(92px,1fr)); }.limit-flags { grid-template-columns:1fr; }.observer-counters { grid-template-columns:repeat(auto-fit,minmax(80px,1fr)); }.latency-summary,.latency-details { grid-template-columns:repeat(auto-fit,minmax(100px,1fr)); }.context-menu { inset:auto 6px 6px!important; width:auto; max-height:calc(100vh - 12px); } } +@media (max-width:650px) { .topbar { align-items:flex-start; }.topbar-actions { align-items:flex-end; flex-direction:column; }.connection-status span { display:none; }.metrics { grid-template-columns:repeat(2,1fr); }.mode-tabs { grid-template-columns:1fr; }.mode-tabs button { flex-direction:row; }.card-meta { grid-template-columns:1fr; }.performance-strip { grid-template-columns:repeat(auto-fit,minmax(92px,1fr)); }.limit-flags { grid-template-columns:1fr; }.observer-counters { grid-template-columns:repeat(auto-fit,minmax(80px,1fr)); }.latency-summary,.client-summary,.latency-details { grid-template-columns:repeat(auto-fit,minmax(100px,1fr)); }.context-menu { inset:auto 6px 6px!important; width:auto; max-height:calc(100vh - 12px); } }