浏览器端反馈控制

This commit is contained in:
2026-08-11 11:57:59 +08:00
parent 6c8c205e86
commit 67f93307e2
5 changed files with 179 additions and 16 deletions
+82 -12
View File
@@ -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::uint64_t>(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::uint64_t>(std::llround(1'000'000'000.0 / frequency_hz)) : 0;
}
int integer_value(const Gallery_State& state, std::string_view id) {
return static_cast<int>(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<double>(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_{};
+14 -1
View File
@@ -131,7 +131,20 @@ void append_common(std::vector<Control_Definition>& 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"));
+47 -1
View File
@@ -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<bool>());
EXPECT_TRUE(observer.at("consumer_feedback_master_enabled").get<bool>());
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(
+26 -2
View File
@@ -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);
+10
View File
@@ -121,6 +121,16 @@
<div><small>PaintEvent</small><b data-observer-field="paint_duration_ns">0 ns</b></div>
<div><small>后台渲染</small><b data-observer-field="render_duration_ns">0 ns</b></div>
<div><small>内部瓶颈</small><b data-observer-field="bottleneck_duration_ns">0 ns</b></div>
<div><small>消费者总开关</small><b data-observer-field="consumer_feedback_master_enabled">关闭</b></div>
<div><small>Kernel 反馈有效</small><b data-observer-field="consumer_feedback_enabled">关闭</b></div>
<div><small>像素响应反馈</small><b data-observer-field="consumer_pixel_feedback_enabled">关闭</b></div>
<div><small>浏览器呈现反馈</small><b data-observer-field="consumer_presentation_feedback_enabled">关闭</b></div>
<div><small>手动消费者反馈</small><b data-observer-field="consumer_manual_feedback_enabled">关闭</b></div>
<div><small>手动消费者 FPS</small><b data-observer-field="consumer_manual_fps">0 FPS</b></div>
<div><small>生效反馈来源</small><b data-observer-field="consumer_feedback_source">none</b></div>
<div><small>像素响应周期</small><b data-observer-field="consumer_pixel_interval_ns">0 ns</b></div>
<div><small>浏览器呈现周期</small><b data-observer-field="consumer_presentation_interval_ns">0 ns</b></div>
<div><small>手动消费者周期</small><b data-observer-field="consumer_manual_interval_ns">0 ns</b></div>
<div><small>消费者原始采样</small><b data-observer-field="consumer_sample_interval_ns">0 ns</b></div>
<div><small>消费者平滑周期</small><b data-observer-field="consumer_smoothed_interval_ns">0 ns</b></div>
<div><small>消费者抖动</small><b data-observer-field="consumer_variation_ns">0 ns</b></div>