帧策略展示优化
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
* `Scene::Private` 是输入事件流的唯一所有者:外部转移事件对象所有权,并发提交到 Def 注册的 MPMC 三缓冲;收集、内部渲染、外部查询分别占用一份队列,Prepare 入口只在交换锁下轮换三者并清空过期查询队列。事件不得独立触发帧。2D 按 Renderable 区域与 Paint 顺序形成接受链,区域默认整个 viewport;3D 无等待提交渲染域,满载时保留当前事件供下一次 Prepare 重试。
|
||||
* 帧由 Scene 外部创建和持有;每次 `render(frame*)` 只借用该帧并在完成回调返回同一地址。Scene、异步后端和 Web 层只向帧写入固定语义的单调时间点与原始耗时,不保存平均值、分位数或波动等衍生统计。
|
||||
* Plot 使用服务端帧时钟调用 `Scene::render(frame*)`;`fixed_rate` 只由 Frame_Scheduler 周期时钟驱动,完成回调不得改变其 deadline。`maximum_rate` 是唯一允许完成回调在释放 Plot 准入后异步投递下一帧请求的特殊模式;回调不得直接重入 `render()`、不得同步等待,并且物理帧槽耗尽时只能由帧退役事件解除背压。系统不存在浏览器逐帧 request/ack。Scene 回调返回完成帧后,Web 层在 Render Domain 之外把 2D 原生 BGRA 或 3D 原生 RGBA 编码为 H.264,经 LibDataChannel WebRTC 视频轨直接发布;WebSocket 只承载 SDP/ICE、输入事件和诊断 JSON。多个订阅者共享同一编码结果,关闭视频时 3D 必须使用 diagnostics 输出并跳过 GPU 像素回读。
|
||||
* 帧策略及其时钟实现统一归属 `kernel/src/kernel/Frame_Policy`。配置变更和运行事实通过 MPMC 事件流进入唯一 consumer,由 `double_buffer/model.hpp` 发布 `Frame_Policy::State`;查询只读取已发布 State。手动帧捕获必须把帧创建边界的策略 State 绑定到该帧并沿现有帧捕获协议返回,禁止另建策略历史、轮询累加器或重复诊断字段。
|
||||
* 相机是 3D Scene 组件,只允许定义在 `render_3D/camera`;Kernel 和 2D 不得依赖相机类型。Web 层只为已有 `Camera_3D` 增加协议描述,不复制相机配置。Gallery 服务同一时刻只允许一个页面实例持有;该页面的所有 Plot 连接共享页面令牌,其他标签页或浏览器实例必须被拒绝。
|
||||
* `Time_Axis` 是时间与 tick 的唯一权威来源;使用层先推进时间轴,再把同一 tick 分发给所有相关数据图元。时间窗口从第一条数据起始终锚定最新 tick,未产生数据的槽位保持背景。
|
||||
* 图表选区保存两根轴上的数据范围,绘制时才映射为像素;选区作为独立 Renderable 在使用层与图元组合,禁止在各图元内复制选区状态。
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
#include "Frame_Pacing_Policy.hpp"
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace aethera {
|
||||
|
||||
Frame_Pacing_Policy::Frame_Pacing_Policy() = default;
|
||||
|
||||
Frame_Pacing_Properties Frame_Pacing_Policy::read() const {
|
||||
return {
|
||||
render_enabled_.load(std::memory_order_acquire),
|
||||
video_enabled_.load(std::memory_order_acquire),
|
||||
mode_.load(std::memory_order_acquire),
|
||||
fixed_rate_fps_.load(std::memory_order_acquire)};
|
||||
}
|
||||
|
||||
void Frame_Pacing_Policy::set_render_enabled(bool enabled) {
|
||||
render_enabled_.store(enabled, std::memory_order_release);
|
||||
}
|
||||
|
||||
void Frame_Pacing_Policy::set_video_enabled(bool enabled) {
|
||||
video_enabled_.store(enabled, std::memory_order_release);
|
||||
}
|
||||
|
||||
void Frame_Pacing_Policy::set_mode(Frame_Pacing_Mode mode) {
|
||||
mode_.store(mode, std::memory_order_release);
|
||||
}
|
||||
|
||||
void Frame_Pacing_Policy::set_fixed_rate(double fps) {
|
||||
if (!std::isfinite(fps) || fps <= 0.0)
|
||||
throw std::invalid_argument("frame pacing fps must be positive");
|
||||
fixed_rate_fps_.store(fps, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool Frame_Pacing_Policy::request_immediate() const noexcept {
|
||||
return render_enabled_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
namespace aethera {
|
||||
|
||||
enum struct Frame_Pacing_Mode : std::uint8_t {
|
||||
manual,
|
||||
fixed_rate,
|
||||
maximum_rate
|
||||
};
|
||||
|
||||
struct Frame_Pacing_Properties {
|
||||
bool render_enabled{true}; /* 查询时 Plot 是否接受任何帧请求。 */
|
||||
bool video_enabled{true}; /* 查询时完成帧是否需要生成视频像素。 */
|
||||
Frame_Pacing_Mode mode{Frame_Pacing_Mode::fixed_rate}; /* 查询时服务端帧驱动模式。 */
|
||||
double fixed_rate_fps{30.0}; /* fixed_rate 模式目标帧率。 */
|
||||
};
|
||||
|
||||
/*
|
||||
* 每个 Scene/Plot 独立持有的帧策略。
|
||||
* Scheduler 只负责 WHEN;这里负责 WHETHER。
|
||||
*/
|
||||
struct Frame_Pacing_Policy final {
|
||||
Frame_Pacing_Policy();
|
||||
|
||||
[[nodiscard]] Frame_Pacing_Properties read() const;
|
||||
void set_render_enabled(bool enabled);
|
||||
void set_video_enabled(bool enabled);
|
||||
void set_mode(Frame_Pacing_Mode mode);
|
||||
void set_fixed_rate(double fps);
|
||||
|
||||
[[nodiscard]] bool request_immediate() const noexcept;
|
||||
|
||||
private:
|
||||
std::atomic_bool render_enabled_{true}; /* 当前 Plot 是否接受任何帧请求。 */
|
||||
std::atomic_bool video_enabled_{true}; /* 完成帧是否需要生成视频像素。 */
|
||||
std::atomic<Frame_Pacing_Mode> mode_{Frame_Pacing_Mode::fixed_rate}; /* 当前服务端帧驱动模式。 */
|
||||
std::atomic<double> fixed_rate_fps_{30.0}; /* fixed_rate 模式目标帧率。 */
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
#include "Frame_Policy.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace aethera {
|
||||
namespace {
|
||||
|
||||
std::uint64_t monotonic_nanoseconds(
|
||||
std::chrono::steady_clock::time_point value =
|
||||
std::chrono::steady_clock::now()) noexcept {
|
||||
return static_cast<std::uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
value.time_since_epoch()).count());
|
||||
}
|
||||
|
||||
std::uint64_t duration_nanoseconds(
|
||||
std::chrono::steady_clock::duration value) noexcept {
|
||||
return static_cast<std::uint64_t>(std::max<std::int64_t>(
|
||||
0, std::chrono::duration_cast<std::chrono::nanoseconds>(value).count()));
|
||||
}
|
||||
|
||||
void observe_event(Frame_Policy::State& state,
|
||||
const detail::Frame_Policy_Event& event) noexcept {
|
||||
if (event.occurred_ns != 0) {
|
||||
if (state.observation_started_ns == 0)
|
||||
state.observation_started_ns = event.occurred_ns;
|
||||
state.observed_until_ns = std::max(
|
||||
state.observed_until_ns, event.occurred_ns);
|
||||
}
|
||||
}
|
||||
|
||||
void reset_counters(Frame_Policy::State& state,
|
||||
std::uint64_t occurred_ns) noexcept {
|
||||
const auto generation = state.generation + 1;
|
||||
const auto render_enabled = state.render_enabled;
|
||||
const auto video_enabled = state.video_enabled;
|
||||
const auto mode = state.mode;
|
||||
const auto fixed_rate_fps = state.fixed_rate_fps;
|
||||
state = {};
|
||||
state.generation = generation;
|
||||
state.render_enabled = render_enabled;
|
||||
state.video_enabled = video_enabled;
|
||||
state.mode = mode;
|
||||
state.fixed_rate_fps = fixed_rate_fps;
|
||||
state.observation_started_ns = occurred_ns;
|
||||
state.observed_until_ns = occurred_ns;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Frame_Policy::submit(detail::Frame_Policy_Event event) {
|
||||
if (event.occurred_ns == 0) event.occurred_ns = monotonic_nanoseconds();
|
||||
submit_stream<detail::Frame_Policy_Event_Stream>(std::move(event));
|
||||
}
|
||||
|
||||
void Frame_Policy::set_render_enabled(bool enabled) {
|
||||
submit({.type = detail::Frame_Policy_Event_Type::render_enabled,
|
||||
.enabled = enabled});
|
||||
}
|
||||
|
||||
void Frame_Policy::set_video_enabled(bool enabled) {
|
||||
submit({.type = detail::Frame_Policy_Event_Type::video_enabled,
|
||||
.enabled = enabled});
|
||||
}
|
||||
|
||||
void Frame_Policy::set_mode(Frame_Pacing_Mode mode) {
|
||||
submit({.type = detail::Frame_Policy_Event_Type::mode, .mode = mode});
|
||||
}
|
||||
|
||||
void Frame_Policy::set_fixed_rate(double fps) {
|
||||
if (!std::isfinite(fps) || fps <= 0.0)
|
||||
throw std::invalid_argument("frame pacing fps must be positive");
|
||||
submit({.type = detail::Frame_Policy_Event_Type::fixed_rate,
|
||||
.number = fps});
|
||||
}
|
||||
|
||||
void Frame_Policy::reset_statistics() {
|
||||
submit({.type = detail::Frame_Policy_Event_Type::reset});
|
||||
}
|
||||
|
||||
void Frame_Policy::record_request(
|
||||
Frame_Request_Source source,
|
||||
std::chrono::steady_clock::time_point occurred_at) {
|
||||
submit({.type = detail::Frame_Policy_Event_Type::request_received,
|
||||
.source = source,
|
||||
.occurred_ns = monotonic_nanoseconds(occurred_at)});
|
||||
}
|
||||
|
||||
void Frame_Policy::record_request_accepted(Frame_Request_Source source) {
|
||||
submit({.type = detail::Frame_Policy_Event_Type::request_accepted,
|
||||
.source = source});
|
||||
}
|
||||
|
||||
void Frame_Policy::record_request_coalesced() {
|
||||
submit({.type = detail::Frame_Policy_Event_Type::request_coalesced});
|
||||
}
|
||||
|
||||
void Frame_Policy::record_policy_rejection() {
|
||||
submit({.type = detail::Frame_Policy_Event_Type::policy_rejected});
|
||||
}
|
||||
|
||||
void Frame_Policy::record_frame_slot_backpressure() {
|
||||
submit({.type = detail::Frame_Policy_Event_Type::frame_slot_backpressure});
|
||||
}
|
||||
|
||||
void Frame_Policy::record_scene_rejection() {
|
||||
submit({.type = detail::Frame_Policy_Event_Type::scene_rejected});
|
||||
}
|
||||
|
||||
void Frame_Policy::record_frame_submitted(
|
||||
std::uint64_t sequence, Frame_Request_Source source,
|
||||
std::chrono::steady_clock::duration tick_queue_duration) {
|
||||
submit({.type = detail::Frame_Policy_Event_Type::frame_submitted,
|
||||
.source = source,
|
||||
.sequence = sequence,
|
||||
.duration_ns = duration_nanoseconds(tick_queue_duration)});
|
||||
}
|
||||
|
||||
void Frame_Policy::record_frame_completed(
|
||||
std::uint64_t sequence,
|
||||
std::chrono::steady_clock::duration completion_latency) {
|
||||
submit({.type = detail::Frame_Policy_Event_Type::frame_completed,
|
||||
.sequence = sequence,
|
||||
.duration_ns = duration_nanoseconds(completion_latency)});
|
||||
}
|
||||
|
||||
bool Frame_Policy::consume_events() {
|
||||
exchange_stream<detail::Frame_Policy_Event_Stream>();
|
||||
bool consumed{};
|
||||
bool configuration_changed{};
|
||||
auto& state = double_buffer::detail::Internal_Access::pending_state(this);
|
||||
access_rendering_stream<detail::Frame_Policy_Event_Stream>(
|
||||
[&](std::span<detail::Frame_Policy_Event> events) {
|
||||
for (const auto& event : events) {
|
||||
consumed = true;
|
||||
observe_event(state, event);
|
||||
switch (event.type) {
|
||||
case detail::Frame_Policy_Event_Type::render_enabled:
|
||||
configuration_changed |= state.render_enabled != event.enabled;
|
||||
state.render_enabled = event.enabled;
|
||||
++state.generation;
|
||||
break;
|
||||
case detail::Frame_Policy_Event_Type::video_enabled:
|
||||
state.video_enabled = event.enabled;
|
||||
++state.generation;
|
||||
break;
|
||||
case detail::Frame_Policy_Event_Type::mode:
|
||||
configuration_changed |= state.mode != event.mode;
|
||||
state.mode = event.mode;
|
||||
++state.generation;
|
||||
break;
|
||||
case detail::Frame_Policy_Event_Type::fixed_rate:
|
||||
configuration_changed |= state.fixed_rate_fps != event.number;
|
||||
state.fixed_rate_fps = event.number;
|
||||
++state.generation;
|
||||
break;
|
||||
case detail::Frame_Policy_Event_Type::request_received:
|
||||
++state.request_count;
|
||||
if (event.source == Frame_Request_Source::periodic)
|
||||
++state.periodic_request_count;
|
||||
else if (event.source == Frame_Request_Source::immediate)
|
||||
++state.immediate_request_count;
|
||||
else if (event.source == Frame_Request_Source::maximum_rate)
|
||||
++state.maximum_rate_request_count;
|
||||
break;
|
||||
case detail::Frame_Policy_Event_Type::request_accepted:
|
||||
++state.accepted_request_count;
|
||||
break;
|
||||
case detail::Frame_Policy_Event_Type::request_coalesced:
|
||||
++state.coalesced_request_count;
|
||||
break;
|
||||
case detail::Frame_Policy_Event_Type::policy_rejected:
|
||||
++state.policy_rejection_count;
|
||||
break;
|
||||
case detail::Frame_Policy_Event_Type::frame_slot_backpressure:
|
||||
++state.frame_slot_backpressure_count;
|
||||
break;
|
||||
case detail::Frame_Policy_Event_Type::scene_rejected:
|
||||
++state.scene_rejection_count;
|
||||
break;
|
||||
case detail::Frame_Policy_Event_Type::frame_submitted:
|
||||
++state.submitted_frame_count;
|
||||
++state.active_frame_count;
|
||||
state.last_frame_sequence = event.sequence;
|
||||
state.last_request_source = event.source;
|
||||
state.latest_tick_queue_ns = event.duration_ns;
|
||||
state.tick_queue_total_ns += event.duration_ns;
|
||||
state.maximum_tick_queue_ns = std::max(
|
||||
state.maximum_tick_queue_ns, event.duration_ns);
|
||||
break;
|
||||
case detail::Frame_Policy_Event_Type::frame_completed: {
|
||||
++state.completed_frame_count;
|
||||
if (state.active_frame_count != 0) --state.active_frame_count;
|
||||
state.last_frame_sequence = event.sequence;
|
||||
state.latest_completion_latency_ns = event.duration_ns;
|
||||
state.completion_latency_total_ns += event.duration_ns;
|
||||
state.maximum_completion_latency_ns = std::max(
|
||||
state.maximum_completion_latency_ns, event.duration_ns);
|
||||
if (state.first_completion_ns == 0)
|
||||
state.first_completion_ns = event.occurred_ns;
|
||||
if (state.last_completion_ns != 0) {
|
||||
const auto interval = event.occurred_ns -
|
||||
state.last_completion_ns;
|
||||
++state.completion_interval_count;
|
||||
state.latest_completion_interval_ns = interval;
|
||||
state.completion_interval_total_ns += interval;
|
||||
const auto value = static_cast<double>(interval);
|
||||
state.completion_interval_squared_total_ns2 +=
|
||||
value * value;
|
||||
}
|
||||
state.last_completion_ns = event.occurred_ns;
|
||||
break;
|
||||
}
|
||||
case detail::Frame_Policy_Event_Type::reset:
|
||||
reset_counters(state, event.occurred_ns);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (consumed) publish_state<Base_Tag>();
|
||||
return configuration_changed;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
#pragma once
|
||||
#include "double_buffer/model.hpp"
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
namespace aethera {
|
||||
|
||||
enum struct Frame_Pacing_Mode : std::uint8_t {
|
||||
manual,
|
||||
fixed_rate,
|
||||
maximum_rate
|
||||
};
|
||||
|
||||
enum struct Frame_Request_Source : std::uint8_t {
|
||||
unspecified,
|
||||
periodic,
|
||||
immediate,
|
||||
maximum_rate
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
struct Frame_Policy_Event_Stream;
|
||||
enum struct Frame_Policy_Event_Type : std::uint8_t {
|
||||
render_enabled,
|
||||
video_enabled,
|
||||
mode,
|
||||
fixed_rate,
|
||||
request_received,
|
||||
request_accepted,
|
||||
request_coalesced,
|
||||
policy_rejected,
|
||||
frame_slot_backpressure,
|
||||
scene_rejected,
|
||||
frame_submitted,
|
||||
frame_completed,
|
||||
reset
|
||||
};
|
||||
struct Frame_Policy_Event {
|
||||
Frame_Policy_Event_Type type{};
|
||||
Frame_Request_Source source{};
|
||||
Frame_Pacing_Mode mode{};
|
||||
std::uint64_t sequence{};
|
||||
std::uint64_t occurred_ns{};
|
||||
std::uint64_t duration_ns{};
|
||||
double number{};
|
||||
bool enabled{};
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* 帧策略的配置、累计事实和发布状态均归属同一个 double-buffer 对象。
|
||||
* 任意线程只向 MPMC 事件流提交事实;Plot tick consumer 是唯一归并写者。
|
||||
* 查询方只读取 current State,不参与事件归并,也不获取 Plot/Scene 锁。
|
||||
*/
|
||||
struct Frame_Policy final : double_buffer::Def<
|
||||
Frame_Policy, double_buffer::Root,
|
||||
double_buffer::Mpmc_Triple_Buffer<
|
||||
detail::Frame_Policy_Event_Stream, detail::Frame_Policy_Event>> {
|
||||
struct Prop : Prev_Prop {};
|
||||
struct State : Prev_State {
|
||||
std::uint64_t generation{};
|
||||
bool render_enabled{true};
|
||||
bool video_enabled{true};
|
||||
Frame_Pacing_Mode mode{Frame_Pacing_Mode::fixed_rate};
|
||||
double fixed_rate_fps{30.0};
|
||||
|
||||
std::uint64_t observation_started_ns{};
|
||||
std::uint64_t observed_until_ns{};
|
||||
std::uint64_t request_count{};
|
||||
std::uint64_t periodic_request_count{};
|
||||
std::uint64_t immediate_request_count{};
|
||||
std::uint64_t maximum_rate_request_count{};
|
||||
std::uint64_t accepted_request_count{};
|
||||
std::uint64_t coalesced_request_count{};
|
||||
std::uint64_t policy_rejection_count{};
|
||||
std::uint64_t frame_slot_backpressure_count{};
|
||||
std::uint64_t scene_rejection_count{};
|
||||
std::uint64_t submitted_frame_count{};
|
||||
std::uint64_t completed_frame_count{};
|
||||
std::uint64_t active_frame_count{};
|
||||
|
||||
std::uint64_t first_completion_ns{};
|
||||
std::uint64_t last_completion_ns{};
|
||||
std::uint64_t completion_interval_count{};
|
||||
std::uint64_t latest_completion_interval_ns{};
|
||||
std::uint64_t completion_interval_total_ns{};
|
||||
double completion_interval_squared_total_ns2{};
|
||||
std::uint64_t latest_tick_queue_ns{};
|
||||
std::uint64_t tick_queue_total_ns{};
|
||||
std::uint64_t maximum_tick_queue_ns{};
|
||||
std::uint64_t latest_completion_latency_ns{};
|
||||
std::uint64_t completion_latency_total_ns{};
|
||||
std::uint64_t maximum_completion_latency_ns{};
|
||||
std::uint64_t last_frame_sequence{};
|
||||
Frame_Request_Source last_request_source{};
|
||||
bool operator==(const State&) const = default;
|
||||
};
|
||||
struct Private : Prev_Private {};
|
||||
|
||||
void set_render_enabled(bool enabled);
|
||||
void set_video_enabled(bool enabled);
|
||||
void set_mode(Frame_Pacing_Mode mode);
|
||||
void set_fixed_rate(double fps);
|
||||
void reset_statistics();
|
||||
|
||||
void record_request(Frame_Request_Source source,
|
||||
std::chrono::steady_clock::time_point occurred_at);
|
||||
void record_request_accepted(Frame_Request_Source source);
|
||||
void record_request_coalesced();
|
||||
void record_policy_rejection();
|
||||
void record_frame_slot_backpressure();
|
||||
void record_scene_rejection();
|
||||
void record_frame_submitted(
|
||||
std::uint64_t sequence, Frame_Request_Source source,
|
||||
std::chrono::steady_clock::duration tick_queue_duration);
|
||||
void record_frame_completed(
|
||||
std::uint64_t sequence,
|
||||
std::chrono::steady_clock::duration completion_latency);
|
||||
|
||||
/* 唯一消费者在短任务中归并全部待处理事件并发布 State。 */
|
||||
[[nodiscard]] bool consume_events();
|
||||
|
||||
private:
|
||||
void submit(detail::Frame_Policy_Event event);
|
||||
};
|
||||
|
||||
}
|
||||
@@ -25,6 +25,7 @@ struct Render_Frame::Private {
|
||||
};
|
||||
Frame_Identity identity{}; /* 外部帧管理器提供且终生不变的帧身份。 */
|
||||
Frame_Request_Source request_source{}; /* 当前逻辑帧的策略触发来源。 */
|
||||
Frame_Policy::State frame_policy{}; /* 创建逻辑帧时读取的已发布策略状态。 */
|
||||
std::chrono::steady_clock::time_point created_at{}; /* 所有 elapsed_ns 使用的单调时钟原点。 */
|
||||
std::uint64_t created_time_unix_ns{}; /* 用于跨进程展示的创建 Unix 时间,单位为纳秒。 */
|
||||
std::array<std::atomic_uint64_t, marker_count> markers{}; /* 每种时间点首次出现时的 elapsed_ns 加一编码。 */
|
||||
@@ -37,9 +38,11 @@ Render_Frame::Render_Frame(Frame_Identity identity) : d(std::make_unique<Private
|
||||
begin(identity);
|
||||
}
|
||||
void Render_Frame::begin(Frame_Identity identity,
|
||||
Frame_Request_Source source) noexcept {
|
||||
Frame_Request_Source source,
|
||||
const Frame_Policy::State& frame_policy) noexcept {
|
||||
d->identity = identity;
|
||||
d->request_source = source;
|
||||
d->frame_policy = frame_policy;
|
||||
d->created_at = std::chrono::steady_clock::now();
|
||||
const auto system_now = std::chrono::system_clock::now().time_since_epoch();
|
||||
d->created_time_unix_ns = static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(system_now).count());
|
||||
@@ -158,6 +161,7 @@ Taskflow_Frame_Trace Render_Frame::take_taskflow_trace() {
|
||||
Taskflow_Frame_Trace result{};
|
||||
result.identity = d->identity;
|
||||
result.request_source = d->request_source;
|
||||
result.frame_policy = d->frame_policy;
|
||||
result.created_time_unix_ns = d->created_time_unix_ns;
|
||||
result.worker_count = d->taskflow_workers.size();
|
||||
for (std::size_t index = 0; index < marker_count; ++index) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "Frame_Policy/Frame_Policy.hpp"
|
||||
#include "double_buffer/mechanism.hpp"
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
@@ -9,12 +10,6 @@ namespace detail {
|
||||
struct Taskflow_Frame_Access;
|
||||
}
|
||||
struct Frame_Statistics_Sample;
|
||||
enum struct Frame_Request_Source : std::uint8_t {
|
||||
unspecified,
|
||||
periodic,
|
||||
immediate,
|
||||
maximum_rate
|
||||
};
|
||||
enum struct Frame_Trace_Marker : std::uint8_t {
|
||||
created,
|
||||
plot_update_started,
|
||||
@@ -128,6 +123,7 @@ struct Taskflow_Task_Trace {
|
||||
struct Taskflow_Frame_Trace {
|
||||
Frame_Identity identity{}; /* 该执行图所属逻辑渲染帧。 */
|
||||
Frame_Request_Source request_source{}; /* 该逻辑帧由周期、手动或最大吞吐策略触发。 */
|
||||
Frame_Policy::State frame_policy{}; /* 帧创建边界读取的已发布策略状态。 */
|
||||
std::uint64_t created_time_unix_ns{}; /* 帧创建 Unix 时间,单位纳秒。 */
|
||||
std::size_t worker_count{}; /* 捕获时全局 Executor 的 worker 数。 */
|
||||
std::vector<Frame_Trace_Point> markers{}; /* 与该帧 DAG 共用时间原点的原始流水线时间点。 */
|
||||
@@ -150,7 +146,8 @@ public:
|
||||
protected:
|
||||
/* 物理帧槽再次承载新逻辑帧时,重建其唯一身份和诊断时间原点。 */
|
||||
void begin(Frame_Identity identity,
|
||||
Frame_Request_Source source = Frame_Request_Source::unspecified) noexcept;
|
||||
Frame_Request_Source source = Frame_Request_Source::unspecified,
|
||||
const Frame_Policy::State& frame_policy = {}) noexcept;
|
||||
private:
|
||||
struct Private;
|
||||
friend struct detail::Taskflow_Frame_Access;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "scene.hpp"
|
||||
#include "Frame_Pacing_Policy.hpp"
|
||||
#include "Frame_Policy/Frame_Policy.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <atomic>
|
||||
#include <unordered_set>
|
||||
@@ -52,22 +52,31 @@ inline Direct_Renderable::Private& Direct_Renderable::data_for_test() {
|
||||
return static_cast<Private&>(*d);
|
||||
}
|
||||
|
||||
TEST(frame_pacing_policy, independent_properties_do_not_publish_copied_versions) {
|
||||
aethera::Frame_Pacing_Policy policy;
|
||||
policy.set_mode(aethera::Frame_Pacing_Mode::maximum_rate);
|
||||
policy.set_fixed_rate(47.5);
|
||||
policy.set_video_enabled(false);
|
||||
TEST(frame_policy, mpmc_events_publish_one_double_buffer_state) {
|
||||
auto built = aethera::Frame_Policy::Builder<aethera::Frame_Policy>{}.build();
|
||||
ASSERT_TRUE(built.has_value());
|
||||
auto policy = std::move(*built);
|
||||
policy->set_mode(aethera::Frame_Pacing_Mode::maximum_rate);
|
||||
policy->set_fixed_rate(47.5);
|
||||
policy->set_video_enabled(false);
|
||||
policy->record_request(aethera::Frame_Request_Source::maximum_rate,
|
||||
std::chrono::steady_clock::now());
|
||||
policy->record_request_accepted(aethera::Frame_Request_Source::maximum_rate);
|
||||
policy->record_frame_submitted(
|
||||
7, aethera::Frame_Request_Source::maximum_rate,
|
||||
std::chrono::microseconds{250});
|
||||
EXPECT_TRUE(policy->consume_events());
|
||||
|
||||
const auto properties = policy.read();
|
||||
EXPECT_TRUE(properties.render_enabled);
|
||||
EXPECT_FALSE(properties.video_enabled);
|
||||
EXPECT_EQ(properties.mode, aethera::Frame_Pacing_Mode::maximum_rate);
|
||||
EXPECT_DOUBLE_EQ(properties.fixed_rate_fps, 47.5);
|
||||
EXPECT_TRUE(policy.request_immediate());
|
||||
|
||||
policy.set_render_enabled(false);
|
||||
EXPECT_FALSE(policy.request_immediate());
|
||||
EXPECT_THROW(policy.set_fixed_rate(0.0), std::invalid_argument);
|
||||
const auto& state = policy->read_state<aethera::Frame_Policy::Base_Tag>();
|
||||
EXPECT_TRUE(state.render_enabled);
|
||||
EXPECT_FALSE(state.video_enabled);
|
||||
EXPECT_EQ(state.mode, aethera::Frame_Pacing_Mode::maximum_rate);
|
||||
EXPECT_DOUBLE_EQ(state.fixed_rate_fps, 47.5);
|
||||
EXPECT_EQ(state.request_count, 1);
|
||||
EXPECT_EQ(state.accepted_request_count, 1);
|
||||
EXPECT_EQ(state.submitted_frame_count, 1);
|
||||
EXPECT_EQ(state.latest_tick_queue_ns, 250'000);
|
||||
EXPECT_THROW(policy->set_fixed_rate(0.0), std::invalid_argument);
|
||||
}
|
||||
inline Graph_Renderable::Private& Graph_Renderable::data_for_test() {
|
||||
return static_cast<Private&>(*d);
|
||||
|
||||
@@ -11,11 +11,12 @@ Frame_2D::Frame_2D(Frame_Identity identity, Pixel_Format output_format)
|
||||
begin(identity, output_format);
|
||||
}
|
||||
void Frame_2D::begin(Frame_Identity identity, Pixel_Format output_format,
|
||||
Frame_Request_Source source) {
|
||||
Frame_Request_Source source,
|
||||
const Frame_Policy::State& frame_policy) {
|
||||
if (std::ranges::find(supported_pixel_formats, output_format) ==
|
||||
supported_pixel_formats.end())
|
||||
throw std::invalid_argument("Frame_2D output pixel format is unsupported");
|
||||
Render_Frame::begin(identity, source);
|
||||
Render_Frame::begin(identity, source, frame_policy);
|
||||
d->output_format = output_format;
|
||||
}
|
||||
Frame_2D::~Frame_2D() = default;
|
||||
|
||||
@@ -24,7 +24,8 @@ public:
|
||||
/* 保留二维后端图像分配,只重启物理槽承载的新逻辑帧。 */
|
||||
void begin(Frame_Identity identity,
|
||||
Pixel_Format output_format = native_pixel_format,
|
||||
Frame_Request_Source source = Frame_Request_Source::unspecified);
|
||||
Frame_Request_Source source = Frame_Request_Source::unspecified,
|
||||
const Frame_Policy::State& frame_policy = {});
|
||||
[[nodiscard]] Pixel_Format output_format() const noexcept;
|
||||
[[nodiscard]] Image_View image() const;
|
||||
/* 按本帧声明的输出格式生成连续像素;原生格式只做必要的行打包。 */
|
||||
|
||||
@@ -14,10 +14,11 @@ Frame_3D::Frame_3D(Frame_Identity identity, Frame_3D_Output output,
|
||||
begin(identity, output, output_format);
|
||||
}
|
||||
void Frame_3D::begin(Frame_Identity identity, Frame_3D_Output output,
|
||||
Pixel_Format output_format, Frame_Request_Source source) {
|
||||
Pixel_Format output_format, Frame_Request_Source source,
|
||||
const Frame_Policy::State& frame_policy) {
|
||||
if (output_format != native_pixel_format)
|
||||
throw std::invalid_argument("Frame_3D output pixel format is unsupported");
|
||||
Render_Frame::begin(identity, source);
|
||||
Render_Frame::begin(identity, source, frame_policy);
|
||||
d->output = output;
|
||||
d->output_format = output_format;
|
||||
d->extent = {};
|
||||
|
||||
@@ -31,7 +31,8 @@ public:
|
||||
void begin(Frame_Identity identity,
|
||||
Frame_3D_Output output = Frame_3D_Output::pixels,
|
||||
Pixel_Format output_format = native_pixel_format,
|
||||
Frame_Request_Source source = Frame_Request_Source::unspecified);
|
||||
Frame_Request_Source source = Frame_Request_Source::unspecified,
|
||||
const Frame_Policy::State& frame_policy = {});
|
||||
[[nodiscard]] Extent extent() const noexcept;
|
||||
[[nodiscard]] std::span<const std::byte> pixels() const noexcept;
|
||||
[[nodiscard]] std::shared_ptr<const std::vector<std::byte>> share_pixels() const noexcept;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
#include <Frame_Scheduler.hpp>
|
||||
#include <Frame_Policy/Frame_Scheduler.hpp>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
|
||||
+174
-71
@@ -1,8 +1,8 @@
|
||||
#include "Plot.hpp"
|
||||
#include "Renderable_Adapter.hpp"
|
||||
#include "Taskflow_Trace_Json.hpp"
|
||||
#include <Frame_Pacing_Policy.hpp>
|
||||
#include <Frame_Scheduler.hpp>
|
||||
#include <Frame_Policy/Frame_Policy.hpp>
|
||||
#include <Frame_Policy/Frame_Scheduler.hpp>
|
||||
#include <concurrentqueue-1.0.5/concurrentqueue.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <magic_enum/magic_enum.hpp>
|
||||
@@ -76,8 +76,8 @@ std::string_view pixel_format_name(render_3d::Pixel_Format format) {
|
||||
throw std::logic_error("unknown 3D pixel format");
|
||||
}
|
||||
|
||||
nlohmann::json frame_policy_schema(const Frame_Pacing_Policy& pacing) {
|
||||
const auto current = pacing.read();
|
||||
nlohmann::json frame_policy_schema(const Frame_Policy& policy) {
|
||||
const auto& current = policy.read_state<Frame_Policy::Base_Tag>();
|
||||
return {
|
||||
{"id", "frame-analysis"}, {"label", "渲染与媒体流水线"}, {"kind", "analysis"},
|
||||
{"fields", nlohmann::json::array({
|
||||
@@ -107,7 +107,7 @@ nlohmann::json frame_policy_schema(const Frame_Pacing_Policy& pacing) {
|
||||
};
|
||||
}
|
||||
|
||||
nlohmann::json write_frame_policy_prop(Frame_Pacing_Policy& pacing,
|
||||
nlohmann::json write_frame_policy_prop(Frame_Policy& pacing,
|
||||
std::string_view key,
|
||||
const nlohmann::json& value) {
|
||||
if (key == "render_enabled" || key == "video_enabled") {
|
||||
@@ -142,6 +142,101 @@ nlohmann::json write_frame_policy_prop(Frame_Pacing_Policy& pacing,
|
||||
return {{"success", false}, {"error", "unknown frame runtime property"}};
|
||||
}
|
||||
|
||||
nlohmann::json frame_policy_state_json(const Frame_Policy::State& state) {
|
||||
const auto milliseconds = [](std::uint64_t nanoseconds) {
|
||||
return static_cast<double>(nanoseconds) / 1'000'000.0;
|
||||
};
|
||||
const auto ratio = [](std::uint64_t numerator, std::uint64_t denominator) {
|
||||
return denominator == 0 ? 0.0 :
|
||||
static_cast<double>(numerator) / static_cast<double>(denominator);
|
||||
};
|
||||
const auto observed_ns = state.observed_until_ns > state.observation_started_ns
|
||||
? state.observed_until_ns - state.observation_started_ns : 0U;
|
||||
const auto observed_seconds = static_cast<double>(observed_ns) / 1'000'000'000.0;
|
||||
const auto rate = [observed_seconds](std::uint64_t count) {
|
||||
return observed_seconds > 0.0
|
||||
? static_cast<double>(count) / observed_seconds : 0.0;
|
||||
};
|
||||
const auto completion_span = state.last_completion_ns > state.first_completion_ns
|
||||
? state.last_completion_ns - state.first_completion_ns : 0U;
|
||||
const auto effective_fps = completion_span != 0 && state.completed_frame_count > 1
|
||||
? static_cast<double>(state.completed_frame_count - 1U) *
|
||||
1'000'000'000.0 / static_cast<double>(completion_span)
|
||||
: 0.0;
|
||||
const auto interval_mean = state.completion_interval_count == 0 ? 0.0 :
|
||||
static_cast<double>(state.completion_interval_total_ns) /
|
||||
static_cast<double>(state.completion_interval_count);
|
||||
const auto interval_variance = state.completion_interval_count == 0 ? 0.0 :
|
||||
std::max(0.0,
|
||||
state.completion_interval_squared_total_ns2 /
|
||||
static_cast<double>(state.completion_interval_count) -
|
||||
interval_mean * interval_mean);
|
||||
const auto target_achievement =
|
||||
state.mode == Frame_Pacing_Mode::fixed_rate && state.fixed_rate_fps > 0.0
|
||||
? effective_fps / state.fixed_rate_fps : 0.0;
|
||||
return {
|
||||
{"generation", state.generation},
|
||||
{"configuration", {
|
||||
{"mode", pacing_mode_name(state.mode)},
|
||||
{"render_enabled", state.render_enabled},
|
||||
{"video_enabled", state.video_enabled},
|
||||
{"fixed_rate_fps", state.fixed_rate_fps}}},
|
||||
{"observation", {
|
||||
{"duration_ms", milliseconds(observed_ns)},
|
||||
{"request_count", state.request_count},
|
||||
{"submitted_frame_count", state.submitted_frame_count},
|
||||
{"completed_frame_count", state.completed_frame_count},
|
||||
{"active_frame_count", state.active_frame_count}}},
|
||||
{"throughput", {
|
||||
{"request_rate_fps", rate(state.request_count)},
|
||||
{"submission_rate_fps", rate(state.submitted_frame_count)},
|
||||
{"completion_rate_fps", effective_fps},
|
||||
{"target_achievement_ratio", target_achievement},
|
||||
{"latest_frame_interval_ms",
|
||||
milliseconds(state.latest_completion_interval_ns)},
|
||||
{"average_frame_interval_ms", milliseconds(
|
||||
state.completion_interval_count == 0 ? 0U :
|
||||
state.completion_interval_total_ns /
|
||||
state.completion_interval_count)},
|
||||
{"frame_interval_jitter_ms",
|
||||
std::sqrt(interval_variance) / 1'000'000.0}}},
|
||||
{"requests", {
|
||||
{"periodic", state.periodic_request_count},
|
||||
{"immediate", state.immediate_request_count},
|
||||
{"maximum_rate", state.maximum_rate_request_count},
|
||||
{"accepted", state.accepted_request_count},
|
||||
{"coalesced", state.coalesced_request_count},
|
||||
{"policy_rejected", state.policy_rejection_count},
|
||||
{"frame_slot_backpressure", state.frame_slot_backpressure_count},
|
||||
{"scene_rejected", state.scene_rejection_count},
|
||||
{"acceptance_ratio", ratio(
|
||||
state.accepted_request_count, state.request_count)},
|
||||
{"coalescing_ratio", ratio(
|
||||
state.coalesced_request_count, state.request_count)},
|
||||
{"backpressure_ratio", ratio(
|
||||
state.frame_slot_backpressure_count,
|
||||
state.accepted_request_count)}}},
|
||||
{"latency", {
|
||||
{"latest_tick_queue_ms", milliseconds(state.latest_tick_queue_ns)},
|
||||
{"average_tick_queue_ms", milliseconds(
|
||||
state.submitted_frame_count == 0 ? 0U :
|
||||
state.tick_queue_total_ns / state.submitted_frame_count)},
|
||||
{"maximum_tick_queue_ms", milliseconds(state.maximum_tick_queue_ns)},
|
||||
{"latest_completion_ms", milliseconds(
|
||||
state.latest_completion_latency_ns)},
|
||||
{"average_completion_ms", milliseconds(
|
||||
state.completed_frame_count == 0 ? 0U :
|
||||
state.completion_latency_total_ns /
|
||||
state.completed_frame_count)},
|
||||
{"maximum_completion_ms", milliseconds(
|
||||
state.maximum_completion_latency_ns)}}},
|
||||
{"last_frame", {
|
||||
{"sequence", state.last_frame_sequence},
|
||||
{"request_source", magic_enum::enum_name(
|
||||
state.last_request_source)}}}
|
||||
};
|
||||
}
|
||||
|
||||
void append_statistic_json(nlohmann::json& output,
|
||||
const Frame_Statistics_State& state) {
|
||||
for (const auto statistic : magic_enum::enum_values<Frame_Statistic>()) {
|
||||
@@ -260,6 +355,7 @@ nlohmann::json taskflow_trace_json(
|
||||
{"sequence", trace.identity.sequence},
|
||||
{"correlation_id", trace.identity.correlation_id},
|
||||
{"request_source", magic_enum::enum_name(trace.request_source)},
|
||||
{"frame_policy", frame_policy_state_json(trace.frame_policy)},
|
||||
{"created_time_unix_ns", trace.created_time_unix_ns},
|
||||
{"worker_count", trace.worker_count},
|
||||
{"markers", std::move(markers)},
|
||||
@@ -337,6 +433,8 @@ struct Plot::Private {
|
||||
|
||||
struct Managed_Frame {
|
||||
std::chrono::microseconds presentation_time{}; /* 共享页面时钟产生的媒体时间戳。 */
|
||||
std::chrono::steady_clock::time_point tick_issued_at{}; /* 本逻辑帧请求进入 Plot 的时刻。 */
|
||||
std::chrono::steady_clock::time_point submitted_at{}; /* Scene 接受本逻辑帧的时刻。 */
|
||||
Frame frame{}; /* 三缓冲物理槽拥有且反复承载逻辑帧。 */
|
||||
std::atomic<Frame_State> state{Frame_State::available}; /* 本槽唯一生命周期状态。 */
|
||||
std::atomic_size_t diagnostic_readers{}; /* 无锁诊断读取认领;非零时该槽不可复用。 */
|
||||
@@ -366,7 +464,7 @@ struct Plot::Private {
|
||||
std::atomic_uint64_t next_stream_id{1};
|
||||
std::atomic<std::shared_ptr<const std::string>> terminal_failure{}; /* 首次 Plot Unknown Failure 的唯一终止状态。 */
|
||||
std::uint64_t next_frame_sequence{1};
|
||||
Frame_Pacing_Policy frame_policy{};
|
||||
std::unique_ptr<Frame_Policy> frame_policy{};
|
||||
Frame_Scheduler::Timer frame_timer{}; /* 每 Plot/Scene 只有轻量时间轮节点,不持有线程。 */
|
||||
static constexpr std::size_t scene_frame_capacity{3};
|
||||
std::array<Managed_Frame, scene_frame_capacity> frame_slots{}; /* Scene 与外接消费者共享生命周期的稳定三缓冲。 */
|
||||
@@ -380,12 +478,6 @@ struct Plot::Private {
|
||||
std::atomic_bool tick_task_scheduled{}; /* 唯一短任务准入;不占用 Worker 等待。 */
|
||||
std::atomic<Render_Admission_State> render_admission{Render_Admission_State::ready}; /* Plot 渲染准入及物理槽背压的唯一状态源。 */
|
||||
std::chrono::steady_clock::time_point clock_origin{std::chrono::steady_clock::now()};
|
||||
std::atomic_uint64_t received_tick_count{}; /* 页面时钟交付给本 Plot 的 tick 总数。 */
|
||||
std::atomic_uint64_t coalesced_tick_count{}; /* 尚未消费时被更新 tick 替换的旧 tick 总数。 */
|
||||
std::atomic_uint64_t policy_skip_count{}; /* 帧策略拒绝的 tick 总数。 */
|
||||
std::atomic_uint64_t frame_slot_busy_count{}; /* 三个物理帧槽均被占用的提交次数。 */
|
||||
std::atomic_uint64_t scene_rejection_count{}; /* Scene 单帧准入拒绝的提交次数。 */
|
||||
std::atomic_uint64_t submitted_frame_count{}; /* 成功提交给 Scene 的帧总数。 */
|
||||
std::atomic_size_t taskflow_trace_remaining{}; /* 尚待标记的实际渲染帧数。 */
|
||||
static constexpr std::size_t maximum_taskflow_trace_frames{120};
|
||||
/* 高 32 位 requested,低 32 位 captured。每槽只发布一次不可变 Trace,
|
||||
@@ -411,6 +503,10 @@ struct Plot::Private {
|
||||
Private(std::unique_ptr<Scene_Object> value_scene,
|
||||
std::unique_ptr<Scene_View> value_view)
|
||||
: view(std::move(value_view)), scene(std::move(value_scene)) {
|
||||
auto built_policy = Frame_Policy::Builder<Frame_Policy>{}.build();
|
||||
if (!built_policy)
|
||||
throw std::logic_error("Frame Policy dependency graph is invalid");
|
||||
frame_policy = std::move(*built_policy);
|
||||
for (auto& slot : frame_slots) {
|
||||
if constexpr (std::same_as<Scene_Object, Scene_2D>)
|
||||
slot.frame = std::make_unique<Frame_2D>(Frame_Identity{});
|
||||
@@ -483,7 +579,7 @@ void Plot::Private::fail(std::exception_ptr failure) noexcept {
|
||||
|
||||
nlohmann::json Plot::Private::schema() const {
|
||||
auto result = view->schema();
|
||||
auto analysis = frame_policy_schema(frame_policy);
|
||||
auto analysis = frame_policy_schema(*frame_policy);
|
||||
const auto generator = view->data_generator_schema();
|
||||
if (!generator.is_null()) analysis["data_generator"] = generator;
|
||||
result["frame_analysis"] = std::move(analysis);
|
||||
@@ -537,7 +633,7 @@ void Plot::Private::publish(
|
||||
void Plot::Private::refresh_schedule() {
|
||||
if (!frame_timer.valid()) return;
|
||||
const auto current_consumers = consumers.load(std::memory_order_acquire);
|
||||
const auto pacing = frame_policy.read();
|
||||
const auto& pacing = frame_policy->read_state<Frame_Policy::Base_Tag>();
|
||||
if (!pacing.render_enabled || current_consumers->empty()) {
|
||||
frame_timer.cancel();
|
||||
return;
|
||||
@@ -553,29 +649,33 @@ void Plot::Private::refresh_schedule() {
|
||||
.issued_at = now,
|
||||
.time_milliseconds = std::chrono::duration<double, std::milli>(
|
||||
now - clock_origin).count(),
|
||||
.source = Plot_Render_Tick_Source::maximum_rate});
|
||||
.source = Frame_Request_Source::maximum_rate});
|
||||
arm_tick_consumer(lifetime);
|
||||
}
|
||||
|
||||
void Plot::Private::submit_tick_request(Plot_Render_Tick tick) {
|
||||
const auto source = tick.source;
|
||||
const auto issued_at = tick.issued_at;
|
||||
if (!tick_requests.enqueue(std::move(tick)))
|
||||
throw std::bad_alloc{};
|
||||
frame_policy->record_request(source, issued_at);
|
||||
tick_request_generation.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
void Plot::Private::keep_latest_tick(const Plot_Render_Tick& tick) {
|
||||
const auto priority = [](Plot_Render_Tick_Source source) {
|
||||
const auto priority = [](Frame_Request_Source source) {
|
||||
switch (source) {
|
||||
case Plot_Render_Tick_Source::periodic: return 0;
|
||||
case Plot_Render_Tick_Source::maximum_rate: return 1;
|
||||
case Plot_Render_Tick_Source::immediate: return 2;
|
||||
case Frame_Request_Source::unspecified: return 0;
|
||||
case Frame_Request_Source::periodic: return 0;
|
||||
case Frame_Request_Source::maximum_rate: return 1;
|
||||
case Frame_Request_Source::immediate: return 2;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
if (deferred_tick) {
|
||||
const auto current_priority = priority(deferred_tick->source);
|
||||
const auto next_priority = priority(tick.source);
|
||||
coalesced_tick_count.fetch_add(1, std::memory_order_relaxed);
|
||||
frame_policy->record_request_coalesced();
|
||||
if (current_priority > next_priority ||
|
||||
(current_priority == next_priority &&
|
||||
deferred_tick->issued_at >= tick.issued_at))
|
||||
@@ -601,7 +701,7 @@ void Plot::Private::release_render_admission(std::weak_ptr<Plot> lifetime) {
|
||||
expected, Render_Admission_State::ready,
|
||||
std::memory_order_acq_rel, std::memory_order_acquire))
|
||||
return;
|
||||
const auto pacing = frame_policy.read();
|
||||
const auto& pacing = frame_policy->read_state<Frame_Policy::Base_Tag>();
|
||||
if (pacing.render_enabled && pacing.mode == Frame_Pacing_Mode::maximum_rate) {
|
||||
const auto current_consumers = consumers.load(std::memory_order_acquire);
|
||||
if (!current_consumers->empty()) {
|
||||
@@ -610,13 +710,14 @@ void Plot::Private::release_render_admission(std::weak_ptr<Plot> lifetime) {
|
||||
.issued_at = now,
|
||||
.time_milliseconds = std::chrono::duration<double, std::milli>(
|
||||
now - clock_origin).count(),
|
||||
.source = Plot_Render_Tick_Source::maximum_rate});
|
||||
.source = Frame_Request_Source::maximum_rate});
|
||||
}
|
||||
}
|
||||
arm_tick_consumer(std::move(lifetime));
|
||||
}
|
||||
|
||||
void Plot::Private::consume_tick(std::weak_ptr<Plot> lifetime) {
|
||||
if (frame_policy->consume_events()) refresh_schedule();
|
||||
const auto observed_generation =
|
||||
tick_request_generation.load(std::memory_order_acquire);
|
||||
Plot_Render_Tick requested;
|
||||
@@ -627,6 +728,7 @@ void Plot::Private::consume_tick(std::weak_ptr<Plot> lifetime) {
|
||||
auto tick = std::exchange(deferred_tick, {});
|
||||
if (tick) clock_tick(*tick);
|
||||
}
|
||||
if (frame_policy->consume_events()) refresh_schedule();
|
||||
tick_task_scheduled.store(false, std::memory_order_release);
|
||||
if (tick_request_generation.load(std::memory_order_acquire) !=
|
||||
observed_generation ||
|
||||
@@ -637,17 +739,18 @@ void Plot::Private::consume_tick(std::weak_ptr<Plot> lifetime) {
|
||||
|
||||
void Plot::Private::clock_tick(const Plot_Render_Tick& tick) {
|
||||
if (terminal_failure.load(std::memory_order_acquire)) return;
|
||||
const auto pacing = frame_policy.read();
|
||||
const auto& pacing = frame_policy->read_state<Frame_Policy::Base_Tag>();
|
||||
const bool accepted = pacing.render_enabled &&
|
||||
(tick.source == Plot_Render_Tick_Source::immediate ||
|
||||
(tick.source == Plot_Render_Tick_Source::periodic &&
|
||||
(tick.source == Frame_Request_Source::immediate ||
|
||||
(tick.source == Frame_Request_Source::periodic &&
|
||||
pacing.mode == Frame_Pacing_Mode::fixed_rate) ||
|
||||
(tick.source == Plot_Render_Tick_Source::maximum_rate &&
|
||||
(tick.source == Frame_Request_Source::maximum_rate &&
|
||||
pacing.mode == Frame_Pacing_Mode::maximum_rate));
|
||||
if (!accepted) {
|
||||
policy_skip_count.fetch_add(1, std::memory_order_relaxed);
|
||||
frame_policy->record_policy_rejection();
|
||||
return;
|
||||
}
|
||||
frame_policy->record_request_accepted(tick.source);
|
||||
render_frame(tick);
|
||||
}
|
||||
|
||||
@@ -722,7 +825,7 @@ nlohmann::json Plot::Private::trace_response(
|
||||
void Plot::Private::render_frame(Plot_Render_Tick tick) {
|
||||
if (terminal_failure.load(std::memory_order_acquire)) return;
|
||||
const auto streams = stream_snapshot();
|
||||
const auto pacing = frame_policy.read();
|
||||
const auto& pacing = frame_policy->read_state<Frame_Policy::Base_Tag>();
|
||||
if (!pacing.render_enabled || streams.consumers->empty()) return;
|
||||
|
||||
auto admission_expected = Render_Admission_State::ready;
|
||||
@@ -773,7 +876,7 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
|
||||
break;
|
||||
}
|
||||
if (!managed) {
|
||||
frame_slot_busy_count.fetch_add(1, std::memory_order_relaxed);
|
||||
frame_policy->record_frame_slot_backpressure();
|
||||
keep_latest_tick(tick);
|
||||
/*
|
||||
* 三个槽都仍被外接消费者持有时,只保留 latest pending。这里绝不能
|
||||
@@ -789,6 +892,8 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
|
||||
managed->presentation_time =
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::duration<double, std::milli>(tick.time_milliseconds));
|
||||
managed->tick_issued_at = tick.issued_at;
|
||||
managed->submitted_at = {};
|
||||
const auto rollback_unsubmitted = [this, slot_index] {
|
||||
auto& slot = frame_slots[slot_index];
|
||||
auto expected = Frame_State::rendering;
|
||||
@@ -808,28 +913,21 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
|
||||
const std::uint64_t sequence = next_frame_sequence++;
|
||||
const Frame_Identity identity{
|
||||
sequence, tick.sequence == 0 ? sequence : tick.sequence};
|
||||
const auto request_source = [&] {
|
||||
switch (tick.source) {
|
||||
case Plot_Render_Tick_Source::periodic:
|
||||
return Frame_Request_Source::periodic;
|
||||
case Plot_Render_Tick_Source::immediate:
|
||||
return Frame_Request_Source::immediate;
|
||||
case Plot_Render_Tick_Source::maximum_rate:
|
||||
return Frame_Request_Source::maximum_rate;
|
||||
}
|
||||
return Frame_Request_Source::unspecified;
|
||||
}();
|
||||
const auto request_source = tick.source;
|
||||
const auto policy_state =
|
||||
frame_policy->read_state<Frame_Policy::Base_Tag>();
|
||||
Render_Frame* logical_frame{};
|
||||
if (auto* frame_2d = std::get_if<std::unique_ptr<Frame_2D>>(&managed->frame)) {
|
||||
(*frame_2d)->begin(identity, Frame_2D::native_pixel_format,
|
||||
request_source);
|
||||
request_source, policy_state);
|
||||
logical_frame = frame_2d->get();
|
||||
} else {
|
||||
auto& frame_3d = std::get<std::unique_ptr<Frame_3D>>(managed->frame);
|
||||
frame_3d->begin(identity,
|
||||
pacing.video_enabled ? Frame_3D_Output::pixels
|
||||
: Frame_3D_Output::diagnostics,
|
||||
Frame_3D::native_pixel_format, request_source);
|
||||
Frame_3D::native_pixel_format, request_source,
|
||||
policy_state);
|
||||
logical_frame = frame_3d.get();
|
||||
}
|
||||
taskflow_trace_claimed = mark_taskflow_trace(*logical_frame);
|
||||
@@ -864,13 +962,16 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
|
||||
Size{static_cast<int>(tick.width), static_cast<int>(tick.height)});
|
||||
const auto result = (*scene_2d)->render(&output);
|
||||
if (!result) {
|
||||
scene_rejection_count.fetch_add(1, std::memory_order_relaxed);
|
||||
frame_policy->record_scene_rejection();
|
||||
rollback_unsubmitted();
|
||||
restore_taskflow_trace_claim();
|
||||
release_render_admission(lifetime);
|
||||
} else {
|
||||
taskflow_trace_claimed = false;
|
||||
submitted_frame_count.fetch_add(1, std::memory_order_relaxed);
|
||||
managed->submitted_at = std::chrono::steady_clock::now();
|
||||
frame_policy->record_frame_submitted(
|
||||
sequence, request_source,
|
||||
managed->submitted_at - managed->tick_issued_at);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -881,10 +982,13 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
|
||||
const auto result = scene_3d->render(&output);
|
||||
if (result == Render_Scene_3D::Render_Result::submitted) {
|
||||
taskflow_trace_claimed = false;
|
||||
submitted_frame_count.fetch_add(1, std::memory_order_relaxed);
|
||||
managed->submitted_at = std::chrono::steady_clock::now();
|
||||
frame_policy->record_frame_submitted(
|
||||
sequence, request_source,
|
||||
managed->submitted_at - managed->tick_issued_at);
|
||||
return;
|
||||
}
|
||||
scene_rejection_count.fetch_add(1, std::memory_order_relaxed);
|
||||
frame_policy->record_scene_rejection();
|
||||
rollback_unsubmitted();
|
||||
restore_taskflow_trace_claim();
|
||||
release_render_admission(lifetime);
|
||||
@@ -918,7 +1022,8 @@ void Plot::Private::publish_completed_frame() {
|
||||
throw std::logic_error("Scene completion graph has no rendering Plot frame");
|
||||
|
||||
try {
|
||||
const auto pacing = frame_policy.read();
|
||||
const auto& pacing =
|
||||
frame_policy->read_state<Frame_Policy::Base_Tag>();
|
||||
const auto identity = frame->identity();
|
||||
Frame_Identity rendered_identity = identity;
|
||||
std::shared_ptr<const std::vector<std::byte>> pixel_storage;
|
||||
@@ -1132,6 +1237,12 @@ void Plot::Private::finalize_retired_frame(Render_Frame* frame) {
|
||||
if (!managed)
|
||||
throw std::logic_error("retired frame has no owned Plot slot");
|
||||
|
||||
frame_policy->record_frame_completed(
|
||||
frame->identity().sequence,
|
||||
managed->submitted_at.time_since_epoch().count() == 0
|
||||
? std::chrono::steady_clock::duration::zero()
|
||||
: std::chrono::steady_clock::now() - managed->submitted_at);
|
||||
|
||||
const auto statistics_generation_value =
|
||||
statistics_generation.load(std::memory_order_acquire);
|
||||
if (applied_statistics_generation != statistics_generation_value) {
|
||||
@@ -1173,10 +1284,10 @@ void Plot::Private::finalize_retired_frame(Render_Frame* frame) {
|
||||
|
||||
/* 物理槽是唯一背压原因;归还任意槽后只解除一次耗尽状态。 */
|
||||
auto admission = Render_Admission_State::frame_slots_exhausted;
|
||||
if (render_admission.compare_exchange_strong(
|
||||
admission, Render_Admission_State::ready,
|
||||
std::memory_order_acq_rel, std::memory_order_acquire))
|
||||
arm_tick_consumer(lifetime);
|
||||
static_cast<void>(render_admission.compare_exchange_strong(
|
||||
admission, Render_Admission_State::ready,
|
||||
std::memory_order_acq_rel, std::memory_order_acquire));
|
||||
arm_tick_consumer(lifetime);
|
||||
|
||||
if (captured_trace) {
|
||||
auto owner = lifetime;
|
||||
@@ -1229,7 +1340,7 @@ void Plot::ensure_started() {
|
||||
.issued_at = tick.issued_at,
|
||||
.sequence = tick.sequence,
|
||||
.time_milliseconds = tick.time_milliseconds,
|
||||
.source = Plot_Render_Tick_Source::periodic});
|
||||
.source = Frame_Request_Source::periodic});
|
||||
}
|
||||
});
|
||||
/*
|
||||
@@ -1337,21 +1448,19 @@ void Plot::configure_stream(Stream_Id stream, std::uint32_t width,
|
||||
void Plot::schedule_render(Plot_Render_Tick tick) {
|
||||
ensure_started();
|
||||
if (d->terminal_failure.load(std::memory_order_acquire)) return;
|
||||
d->received_tick_count.fetch_add(1, std::memory_order_relaxed);
|
||||
d->submit_tick_request(std::move(tick));
|
||||
d->arm_tick_consumer(weak_from_this());
|
||||
}
|
||||
|
||||
void Plot::render_once() {
|
||||
ensure_started();
|
||||
if (!d->frame_policy.request_immediate()) return;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto elapsed = now - d->clock_origin;
|
||||
schedule_render(Plot_Render_Tick{
|
||||
.issued_at = now,
|
||||
.time_milliseconds =
|
||||
std::chrono::duration<double, std::milli>(elapsed).count(),
|
||||
.source = Plot_Render_Tick_Source::immediate});
|
||||
.source = Frame_Request_Source::immediate});
|
||||
}
|
||||
|
||||
void Plot::submit_input(Plot_Input_Event event) {
|
||||
@@ -1385,8 +1494,9 @@ nlohmann::json Plot::write_prop(std::string_view component,
|
||||
ensure_started();
|
||||
if (component != "frame-analysis")
|
||||
return d->view->write_prop(component, key, value);
|
||||
auto result = write_frame_policy_prop(d->frame_policy, key, value);
|
||||
if (result.value("success", false)) d->refresh_schedule();
|
||||
auto result = write_frame_policy_prop(*d->frame_policy, key, value);
|
||||
if (result.value("success", false))
|
||||
d->arm_tick_consumer(weak_from_this());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1450,7 +1560,8 @@ nlohmann::json Plot::diagnostics() const {
|
||||
? 1'000.0 / interval.trimmed_average : 0.0;
|
||||
}
|
||||
|
||||
const auto pacing = d->frame_policy.read();
|
||||
const auto& pacing =
|
||||
d->frame_policy->read_state<Frame_Policy::Base_Tag>();
|
||||
const auto stream = d->stream_snapshot();
|
||||
const auto admission = d->render_admission.load(std::memory_order_acquire);
|
||||
const auto admission_name = [&] {
|
||||
@@ -1479,7 +1590,7 @@ nlohmann::json Plot::diagnostics() const {
|
||||
const std::size_t byte_length = pacing.video_enabled
|
||||
? static_cast<std::size_t>(stream.width) * stream.height * 4U : 0U;
|
||||
nlohmann::json output{
|
||||
{"protocol", "aethera.plot.diagnostics"}, {"version", 3},
|
||||
{"protocol", "aethera.plot.diagnostics"}, {"version", 4},
|
||||
{"dimension", is_3d ? "3D" : "2D"},
|
||||
{"sequence", identity.sequence},
|
||||
{"correlation_id", identity.correlation_id},
|
||||
@@ -1495,18 +1606,8 @@ nlohmann::json Plot::diagnostics() const {
|
||||
{"format", format}, {"native_format", native_format},
|
||||
{"supported_formats", std::move(supported_formats)},
|
||||
{"byte_length", byte_length}}},
|
||||
{"pacing", {{"mode", pacing_mode_name(pacing.mode)},
|
||||
{"fixed_rate_fps", pacing.fixed_rate_fps},
|
||||
{"render_enabled", pacing.render_enabled},
|
||||
{"video_enabled", pacing.video_enabled}}},
|
||||
{"plot_scheduler", {
|
||||
{"received_ticks", d->received_tick_count.load(std::memory_order_relaxed)},
|
||||
{"coalesced_ticks", d->coalesced_tick_count.load(std::memory_order_relaxed)},
|
||||
{"policy_skips", d->policy_skip_count.load(std::memory_order_relaxed)},
|
||||
{"render_admission", admission_name},
|
||||
{"frame_slot_busy", d->frame_slot_busy_count.load(std::memory_order_relaxed)},
|
||||
{"scene_rejections", d->scene_rejection_count.load(std::memory_order_relaxed)},
|
||||
{"submitted_frames", d->submitted_frame_count.load(std::memory_order_relaxed)}}},
|
||||
{"frame_policy", frame_policy_state_json(pacing)},
|
||||
{"render_admission", admission_name},
|
||||
{"frame_statistics", std::move(frame_statistics)},
|
||||
{"input_statistics", std::move(input_statistics)}};
|
||||
if (is_3d) {
|
||||
@@ -1601,5 +1702,7 @@ nlohmann::json Plot::post_publish_taskflow_trace() const {
|
||||
void Plot::reset_diagnostics() {
|
||||
std::visit([](auto& scene) { scene->reset_diagnostics(); }, d->scene);
|
||||
d->statistics_generation.fetch_add(1, std::memory_order_acq_rel);
|
||||
d->frame_policy->reset_statistics();
|
||||
d->arm_tick_consumer(weak_from_this());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,19 +27,13 @@ struct Plot_Input_Event {
|
||||
std::uint32_t native_key{}; /* 浏览器原生按键码。 */
|
||||
bool auto_repeat{}; /* 是否为系统重复按键。 */
|
||||
};
|
||||
enum struct Plot_Render_Tick_Source : std::uint8_t {
|
||||
periodic,
|
||||
immediate,
|
||||
maximum_rate
|
||||
};
|
||||
|
||||
struct Plot_Render_Tick {
|
||||
std::chrono::steady_clock::time_point issued_at{}; /* 页面帧时钟发布本 tick 的单调时刻;手动帧在提交时填写。 */
|
||||
std::uint64_t sequence{}; /* Kernel 全局 Frame_Scheduler 时间轴上的关联序号。 */
|
||||
double time_milliseconds{}; /* 页面级单调时间线,所有图共享同一个动画时刻。 */
|
||||
std::uint32_t width{320}; /* 当前图在媒体图集中的固定像素宽度。 */
|
||||
std::uint32_t height{192}; /* 当前图在媒体图集中的固定像素高度。 */
|
||||
Plot_Render_Tick_Source source{Plot_Render_Tick_Source::immediate}; /* 本次请求来自周期时钟、手动操作或最大吞吐自驱动。 */
|
||||
Frame_Request_Source source{Frame_Request_Source::immediate}; /* 本次请求来自周期时钟、手动操作或最大吞吐自驱动。 */
|
||||
};
|
||||
enum struct Plot_Pixel_Layout : std::uint8_t {
|
||||
bgra8,
|
||||
|
||||
@@ -39,12 +39,12 @@ type Browser_Input_Statistic = {count: number; latest_ms: number; average_ms: nu
|
||||
maximum_ms: number; sent_count: number; disconnected_count: number;
|
||||
websocket_buffered_bytes: number};
|
||||
type Browser_Input_Statistics = Record<string, Browser_Input_Statistic>;
|
||||
type Plot_Diagnostics = {protocol: "aethera.plot.diagnostics"; version: 3; dimension: "2D" | "3D";
|
||||
type Plot_Diagnostics = {protocol: "aethera.plot.diagnostics"; version: 4; dimension: "2D" | "3D";
|
||||
sequence: number; correlation_id: number; rendered_sequence: number; rendered_correlation_id: number;
|
||||
generated_time_unix_ms: number; delivery: Frame_Delivery; frame_rate_fps: number; dropped_sequence_count: number;
|
||||
window_capacity: number; pixel: {width: number; height: number; format: Pixel_Format; native_format: Pixel_Format;
|
||||
supported_formats: Pixel_Format[]; byte_length: number};
|
||||
pacing: {mode: Frame_Pacing_Mode; fixed_rate_fps: number; render_enabled: boolean; video_enabled: boolean};
|
||||
frame_policy: Frame_Policy_State;
|
||||
frame_statistics: Record<string, Statistic_Snapshot>; input_statistics: Event_Statistics};
|
||||
type Frame_Sample = {sequence: number; generated_at_ms: number; received_at_ms: number; values: Frame_Stage_Values};
|
||||
type Video_Playback_Metrics = {frame_rate_fps: number; presented_frames: number; dropped_frames: number;
|
||||
@@ -84,8 +84,21 @@ type Taskflow_Execution_Trace = {native_id: string; node_id: string; worker_id:
|
||||
completed_ms: number; duration_ms: number; cpu_duration_ms: number; cpu_cycles: number; cooperative_wait_ms: number;
|
||||
cpu_time_coarse: boolean; observer_entry_ms: number; observer_exit_ms: number;
|
||||
observer_entry_cpu_ms: number; observer_exit_cpu_ms: number; queue_wait_ms: number};
|
||||
type Frame_Policy_State = {
|
||||
generation: number;
|
||||
configuration: {mode: Frame_Pacing_Mode; render_enabled: boolean; video_enabled: boolean; fixed_rate_fps: number};
|
||||
observation: {duration_ms: number; request_count: number; submitted_frame_count: number; completed_frame_count: number; active_frame_count: number};
|
||||
throughput: {request_rate_fps: number; submission_rate_fps: number; completion_rate_fps: number; target_achievement_ratio: number;
|
||||
latest_frame_interval_ms: number; average_frame_interval_ms: number; frame_interval_jitter_ms: number};
|
||||
requests: {periodic: number; immediate: number; maximum_rate: number; accepted: number; coalesced: number; policy_rejected: number;
|
||||
frame_slot_backpressure: number; scene_rejected: number; acceptance_ratio: number; coalescing_ratio: number; backpressure_ratio: number};
|
||||
latency: {latest_tick_queue_ms: number; average_tick_queue_ms: number; maximum_tick_queue_ms: number;
|
||||
latest_completion_ms: number; average_completion_ms: number; maximum_completion_ms: number};
|
||||
last_frame: {sequence: number; request_source: "unspecified" | "periodic" | "immediate" | "maximum_rate"};
|
||||
};
|
||||
type Taskflow_Frame_Trace = {sequence: number; correlation_id: number; created_time_unix_ns: number; worker_count: number;
|
||||
request_source?: "unspecified" | "periodic" | "immediate" | "maximum_rate";
|
||||
frame_policy?: Frame_Policy_State;
|
||||
markers?: Record<string, number>; measurements?: Record<string, number>;
|
||||
graphs: Taskflow_Graph_Trace[]; executions: Taskflow_Execution_Trace[]};
|
||||
type Taskflow_Frame_Response = {protocol: "aethera.taskflow.frames"; version: 1; requested: number; remaining: number;
|
||||
@@ -125,9 +138,9 @@ function gallery_socket_url(path: string, transport: Gallery_Transport_Mode) {
|
||||
function valid_plot_diagnostics(value: unknown): value is Plot_Diagnostics {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const diagnostics = value as Partial<Plot_Diagnostics>;
|
||||
return diagnostics.protocol === "aethera.plot.diagnostics" && diagnostics.version === 3 &&
|
||||
return diagnostics.protocol === "aethera.plot.diagnostics" && diagnostics.version === 4 &&
|
||||
typeof diagnostics.sequence === "number" && Boolean(diagnostics.frame_statistics) &&
|
||||
Boolean(diagnostics.input_statistics) && Boolean(diagnostics.pacing);
|
||||
Boolean(diagnostics.input_statistics) && Boolean(diagnostics.frame_policy);
|
||||
}
|
||||
|
||||
function valid_gallery_layout(value: unknown): value is Gallery_Layout {
|
||||
@@ -661,8 +674,8 @@ function use_plot_stream(plot: Plot,
|
||||
p99_server_completion_ms: completion?.p99 ?? 0,
|
||||
frame_rate_fps: server.frame_rate_fps,
|
||||
p95_frame_interval_jitter_ms: interval?.p95 ?? 0,
|
||||
pacing_mode: server.pacing.mode,
|
||||
fixed_rate_fps: server.pacing.fixed_rate_fps,
|
||||
pacing_mode: server.frame_policy.configuration.mode,
|
||||
fixed_rate_fps: server.frame_policy.configuration.fixed_rate_fps,
|
||||
delivery: server.delivery,
|
||||
video_playback: diagnostics.video_playback
|
||||
});
|
||||
@@ -1098,10 +1111,78 @@ function Frame_Policy_Pane({plot, analysis, busy, on_refresh, on_update, on_manu
|
||||
on_manual_frame: () => void; on_reset: () => void;
|
||||
}) {
|
||||
const fields = analysis?.fields.filter(field => field.editable) ?? [];
|
||||
const [frame_count, set_frame_count] = useState(8);
|
||||
const [response, set_response] = useState<Taskflow_Frame_Response | null>(null);
|
||||
const [frame_index, set_frame_index] = useState(0);
|
||||
const [capture_busy, set_capture_busy] = useState(false);
|
||||
const [capture_error, set_capture_error] = useState("");
|
||||
const load_samples = useCallback(async () => {
|
||||
const request = await fetch(plot.taskflow, {cache: "no-store"});
|
||||
const value = await request.json() as Taskflow_Frame_Response & {error?: string};
|
||||
if (!request.ok) throw new Error(value.error ?? "读取帧策略状态失败");
|
||||
set_response(value);
|
||||
set_frame_index(current => Math.min(current, Math.max(0, value.frames.length - 1)));
|
||||
}, [plot.taskflow]);
|
||||
useEffect(() => {
|
||||
set_response(null); set_frame_index(0); set_capture_error("");
|
||||
void load_samples().catch(failure => set_capture_error(
|
||||
failure instanceof Error ? failure.message : "读取帧策略状态失败"));
|
||||
}, [load_samples]);
|
||||
useEffect(() => {
|
||||
if (!response || response.requested === 0 || response.complete) return;
|
||||
const timer = window.setInterval(() => void load_samples().catch(failure =>
|
||||
set_capture_error(failure instanceof Error ? failure.message : "读取帧策略状态失败")), 400);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [load_samples, response?.requested, response?.complete]);
|
||||
const capture = async () => {
|
||||
set_capture_busy(true); set_capture_error(""); set_frame_index(0);
|
||||
try {
|
||||
const request = await fetch(plot.taskflow, {method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({frame_count})});
|
||||
const value = await request.json() as Taskflow_Frame_Response & {error?: string};
|
||||
if (!request.ok) throw new Error(value.error ?? "请求帧策略状态失败");
|
||||
set_response(value);
|
||||
} catch (failure) {
|
||||
set_capture_error(failure instanceof Error ? failure.message : "请求帧策略状态失败");
|
||||
} finally { set_capture_busy(false); }
|
||||
};
|
||||
const frame = response?.frames[frame_index];
|
||||
const state = frame?.frame_policy;
|
||||
const percent = (value: number) => `${(value * 100).toFixed(1)}%`;
|
||||
const rate = (value: number) => `${value.toFixed(2)} FPS`;
|
||||
const duration = (value: number) => `${value.toFixed(value < 10 ? 3 : 2)} ms`;
|
||||
return <section className="workspacePane"><Workspace_Header plot={plot} label="采样与帧策略" count={fields.length} busy={busy} on_refresh={on_refresh}/>
|
||||
<div className="workspaceBody"><section className="analysisSection"><header><div><strong>{plot.dimension} 后端帧策略</strong><span>Scene 按当前后端策略渲染;媒体层独立按 30 FPS 读取 latest,并选择 FFmpeg/WebRTC 或 WebSocket 原始像素。浏览器隐藏画面不会停采样。</span></div>
|
||||
<div className="workspaceBody framePolicyWorkspace"><section className="analysisSection"><header><div><strong>{plot.dimension} 后端帧策略</strong><span>配置和运行统计由 Kernel Frame_Policy 双缓冲状态发布;查询只读取已发布状态,不扫描任务图,也不锁住渲染线程。</span></div>
|
||||
<div className="framePolicyActions"><button onClick={on_manual_frame}>手动生成一帧</button><button onClick={on_reset}>清空并从头统计</button></div></header>
|
||||
{analysis ? <div className="propGrid">{fields.map(field => <Property_Control key={field.key} field={field} on_commit={value => on_update(analysis, field, value)}/>)}</div> : <p className="muted">正在读取帧策略…</p>}
|
||||
</section>
|
||||
<section className="analysisSection framePolicySampling"><header><div><strong>按完成帧采样策略状态</strong><span>策略状态绑定在对应帧上,与 Taskflow 帧捕获使用同一请求、帧槽和序列号。每个样本表示该帧创建时已经发布的策略状态。</span></div></header>
|
||||
<div className="taskflowCaptureBar"><label>采样帧数<input type="number" min={1} max={120} value={frame_count}
|
||||
onChange={event => set_frame_count(Math.max(1, Math.min(120, Number(event.target.value) || 1)))}/></label>
|
||||
<button disabled={capture_busy || Boolean(response && response.requested > 0 && !response.complete)} onClick={() => void capture()}>{capture_busy ? "提交中…" : "采样帧策略状态"}</button>
|
||||
<span>{response?.requested ? `${response.captured}/${response.requested} 帧${response.complete ? " · 已完成" : ` · 还需 ${response.remaining} 帧`}` : "只在手动请求后采集;manual 模式需要手动生成相应帧数。"}</span></div>
|
||||
{capture_error ? <p className="taskflowError">{capture_error}</p> : null}
|
||||
{state && frame ? <><div className="framePolicySampleSelector"><label>状态样本<select value={frame_index} onChange={event => set_frame_index(Number(event.target.value))}>
|
||||
{response?.frames.map((value, index) => <option key={`${value.sequence}:${index}`} value={index}>#{value.sequence} · {value.request_source ?? "unspecified"}</option>)}</select></label>
|
||||
<span>策略代次 {state.generation} · 观察 {duration(state.observation.duration_ms)}</span></div>
|
||||
<div className="framePolicyMetricGrid">
|
||||
<article><span>实际完成帧率</span><strong>{rate(state.throughput.completion_rate_fps)}</strong><small>由完成帧间隔计算;评价最终吞吐</small></article>
|
||||
<article><span>提交速率</span><strong>{rate(state.throughput.submission_rate_fps)}</strong><small>进入 Scene 的帧速率</small></article>
|
||||
<article><span>目标达成率</span><strong>{state.configuration.mode === "fixed_rate" ? percent(state.throughput.target_achievement_ratio) : "不适用"}</strong><small>实际完成 FPS ÷ fixed_rate 目标</small></article>
|
||||
<article><span>帧间隔抖动</span><strong>{duration(state.throughput.frame_interval_jitter_ms)}</strong><small>完成间隔标准差,越低越稳定</small></article>
|
||||
<article><span>请求接受率</span><strong>{percent(state.requests.acceptance_ratio)}</strong><small>{state.requests.accepted}/{state.observation.request_count} 个请求进入准入</small></article>
|
||||
<article><span>请求合并率</span><strong>{percent(state.requests.coalescing_ratio)}</strong><small>忙碌期间被 latest 合并的比例</small></article>
|
||||
<article><span>帧槽背压率</span><strong>{percent(state.requests.backpressure_ratio)}</strong><small>{state.requests.frame_slot_backpressure} 次物理帧槽耗尽</small></article>
|
||||
<article><span>平均 tick 排队</span><strong>{duration(state.latency.average_tick_queue_ms)}</strong><small>请求发出至 Scene 提交</small></article>
|
||||
<article><span>平均完成延迟</span><strong>{duration(state.latency.average_completion_ms)}</strong><small>Scene 提交至完成帧退役</small></article>
|
||||
<article><span>在途帧</span><strong>{state.observation.active_frame_count}</strong><small>已提交但尚未完成退役</small></article>
|
||||
</div>
|
||||
<div className="framePolicySampleTable"><table><thead><tr><th>帧</th><th>来源</th><th>完成 FPS</th><th>间隔</th><th>抖动</th><th>排队</th><th>完成延迟</th><th>合并 / 背压</th></tr></thead><tbody>
|
||||
{response?.frames.map((sample, index) => { const policy = sample.frame_policy; return policy ? <tr key={`${sample.sequence}:${index}`} className={index === frame_index ? "active" : ""} onClick={() => set_frame_index(index)}><td>#{sample.sequence}</td><td>{sample.request_source ?? "unspecified"}</td><td>{rate(policy.throughput.completion_rate_fps)}</td><td>{duration(policy.throughput.latest_frame_interval_ms)}</td><td>{duration(policy.throughput.frame_interval_jitter_ms)}</td><td>{duration(policy.latency.latest_tick_queue_ms)}</td><td>{duration(policy.latency.latest_completion_ms)}</td><td>{policy.requests.coalesced} / {policy.requests.frame_slot_backpressure}</td></tr> : null;})}
|
||||
</tbody></table></div>
|
||||
<details className="rawState"><summary>查看该帧绑定的完整 Frame_Policy State</summary><pre className="stateJson">{JSON.stringify(state, null, 2)}</pre></details>
|
||||
</> : <p className="muted">尚未采样帧策略状态。</p>}
|
||||
</section></div></section>;
|
||||
}
|
||||
|
||||
@@ -1841,6 +1922,7 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_
|
||||
sequence: frame?.sequence,
|
||||
correlation_id: frame?.correlation_id,
|
||||
request_source: frame?.request_source,
|
||||
frame_policy: frame?.frame_policy,
|
||||
markers: frame?.markers ?? {},
|
||||
measurements: frame?.measurements ?? {},
|
||||
graph,
|
||||
@@ -2116,6 +2198,7 @@ function Taskflow_Timeline({graph, executions, frame, components, gallery_state,
|
||||
sequence: frame?.sequence,
|
||||
correlation_id: frame?.correlation_id,
|
||||
request_source: frame?.request_source,
|
||||
frame_policy: frame?.frame_policy,
|
||||
markers: frame?.markers ?? {},
|
||||
measurements: frame?.measurements ?? {},
|
||||
graph,
|
||||
@@ -2707,7 +2790,7 @@ export function App() {
|
||||
if (category === "2D" || category === "3D") return plots.filter(plot => plot.dimension === category);
|
||||
return plots;
|
||||
}, [category, plots]);
|
||||
const gallery = <section className="galleryPanel"><header className="topbar"><div><span className="eyebrow">AETHERA 渲染实验室</span><h1>实时图形组件库</h1></div><div className="topbarActions">
|
||||
const gallery = <section className="galleryPanel"><header className="topbar"><nav aria-label="图形维度筛选">{categories.map(value => <button key={value} className={category === value ? "active" : ""} onClick={() => set_category(value)}>{value}</button>)}</nav><div className="topbarActions">
|
||||
{selected ? <span className="selectionName">当前图形 <strong>{plot_labels[selected.id] ?? selected.title}</strong></span> : <span className="muted">点击任意图形后,属性和 DAG 节点状态会自动同步。</span>}
|
||||
<label className="transportSelector">媒体传输<select value={gallery_transport} onChange={event => {
|
||||
const value = event.target.value as Gallery_Transport_Mode;
|
||||
@@ -2715,7 +2798,6 @@ export function App() {
|
||||
}}><option value="websocket_pixels">WebSocket 原始像素</option><option value="ffmpeg">FFmpeg H.264 / WebRTC</option></select></label>
|
||||
<button onClick={() => { localStorage.removeItem(workspace_layout_key); localStorage.removeItem(gallery_layout_key);
|
||||
set_layout_model(Model.fromJson(default_workspace_layout)); set_gallery_layout_revision(value => value + 1); }}>恢复默认布局</button></div></header>
|
||||
<nav>{categories.map(value => <button key={value} className={category === value ? "active" : ""} onClick={() => set_category(value)}>{value}</button>)}</nav>
|
||||
<Gallery_Grid key={`${gallery_layout_revision}:${category}`} plots={visible} selected={selected} policies={execution_policies} layout_scope={category} galleries={gallery_videos} on_policy={update_execution_policy} on_select={set_selected}/></section>;
|
||||
const factory = (node: TabNode) => {
|
||||
if (node.getComponent() === "gallery") return gallery;
|
||||
|
||||
@@ -22,12 +22,13 @@ button { color: inherit; }
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
min-height: 92px;
|
||||
min-height: 66px;
|
||||
padding: 16px 4px;
|
||||
border-bottom: 1px solid #24324a;
|
||||
background: #07101bea;
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
.topbar > nav { flex: none; padding: 0; }
|
||||
|
||||
h1 { margin: 3px 0 0; font-size: clamp(25px, 3vw, 40px); line-height: 1; letter-spacing: -.045em; }
|
||||
h2 { margin: 4px 0 0; font-size: 23px; letter-spacing: -.03em; }
|
||||
@@ -54,6 +55,25 @@ h2 { margin: 4px 0 0; font-size: 23px; letter-spacing: -.03em; }
|
||||
.selectionName { margin-right: 4px; color: #8296b2; white-space: nowrap; }
|
||||
.selectionName strong { color: #e8f0ff; }
|
||||
|
||||
.framePolicyWorkspace { display: grid; align-content: start; gap: 14px; }
|
||||
.framePolicySampling { display: grid; gap: 14px; }
|
||||
.framePolicySampleSelector { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: #8296b2; font-size: 11px; }
|
||||
.framePolicySampleSelector label { display: flex; align-items: center; gap: 8px; }
|
||||
.framePolicySampleSelector select { min-width: 180px; padding: 7px 9px; color: #d7e4f5; border: 1px solid #2a3b55; border-radius: 7px; background: #091321; }
|
||||
.framePolicyMetricGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 9px; }
|
||||
.framePolicyMetricGrid article { min-width: 0; padding: 12px; border: 1px solid #213653; border-radius: 10px; background: #0a1422; }
|
||||
.framePolicyMetricGrid article > span { display: block; color: #8ca0ba; font-size: 11px; }
|
||||
.framePolicyMetricGrid article > strong { display: block; margin: 8px 0 7px; color: #5ce4c2; font: 700 19px/1.1 ui-monospace, monospace; }
|
||||
.framePolicyMetricGrid article > small { display: block; color: #657994; font-size: 10px; line-height: 1.4; }
|
||||
.framePolicySampleTable { overflow: auto; border: 1px solid #213653; border-radius: 10px; background: #08121f; }
|
||||
.framePolicySampleTable table { width: 100%; border-collapse: collapse; font-size: 11px; white-space: nowrap; }
|
||||
.framePolicySampleTable th, .framePolicySampleTable td { padding: 9px 11px; border-bottom: 1px solid #192d46; text-align: right; }
|
||||
.framePolicySampleTable th:first-child, .framePolicySampleTable td:first-child,
|
||||
.framePolicySampleTable th:nth-child(2), .framePolicySampleTable td:nth-child(2) { text-align: left; }
|
||||
.framePolicySampleTable th { position: sticky; top: 0; color: #7890ad; background: #0d1929; }
|
||||
.framePolicySampleTable tbody tr { color: #b9c9dc; cursor: pointer; }
|
||||
.framePolicySampleTable tbody tr:hover, .framePolicySampleTable tbody tr.active { color: #e9fff9; background: #14352f; }
|
||||
|
||||
nav { display: flex; flex-wrap: wrap; gap: 8px; padding: 18px 0; }
|
||||
.plotGridHost { width: 100%; min-height: 360px; }
|
||||
.react-grid-layout { position: relative; transition: height .2s ease; }
|
||||
|
||||
Reference in New Issue
Block a user