浏览器端反馈控制
This commit is contained in:
@@ -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_{};
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user