This commit is contained in:
2026-08-24 10:45:26 +08:00
parent 4f21169ade
commit cb9822074b
31 changed files with 363 additions and 536 deletions
+5 -3
View File
@@ -309,9 +309,10 @@ nlohmann::json generate_2d_data(Object& object, const Json& input) {
std::ranges::copy_n(complete.begin() + block * width, width, blocks[block].begin());
object.template set<&Sweep_Spectrum::Prop::bins_per_block>(width);
object.template set<&Sweep_Spectrum::Prop::block_count>(block_count);
for (auto& block : blocks)
for (std::size_t block_index = 0; block_index < blocks.size(); ++block_index)
object.template submit_stream<Sweep_Spectrum_Stream_Tag>(
std::make_shared<const std::vector<Plot_Value>>(std::move(block)));
std::make_shared<const Sweep_Spectrum_Block>(
Sweep_Spectrum_Block{block_index, std::move(blocks[block_index])}));
object.template mark_dirty<Prepare_Data_Tag>();
generated_count = block_count * width;
} else if constexpr (std::same_as<Definition, Afterglow>) {
@@ -675,7 +676,8 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
values[i] = -90.0 + 35.0 * std::sin(sweep_index * 0.08 + event.time_milliseconds * 0.002);
}
raw->template submit_stream<Sweep_Spectrum_Stream_Tag>(
std::make_shared<const std::vector<Plot_Value>>(std::move(values)));
std::make_shared<const Sweep_Spectrum_Block>(
Sweep_Spectrum_Block{block_index, std::move(values)}));
raw->template mark_dirty<Prepare_Data_Tag>();
};
auto view = make_scene_view<
+1 -28
View File
@@ -69,25 +69,6 @@ void Graph_WebSocket::deliver_frame(
}
}
void Graph_WebSocket::deliver_input_completion(
Plot_Input_Observation observation) {
if (!d->attached.load(std::memory_order_acquire)) return;
const auto connection = d->connection.lock();
if (!connection || !connection->connected()) return;
const nlohmann::json message{
{"kind", "input_completion"},
{"protocol", "aethera.input.latency"},
{"version", 2},
{"sequence", observation.sequence},
{"rendered_frame_sequence", observation.rendered_frame_sequence},
{"coalesced_event_count", observation.coalesced_event_count}
};
try {
connection->send(message.dump(), drogon::WebSocketMessageType::Text);
}
catch (...) {}
}
void Graph_WebSocket::receive(std::string_view message) {
const auto received_time = std::chrono::steady_clock::now();
const auto json = nlohmann::json::parse(message, nullptr, false);
@@ -126,8 +107,6 @@ void Graph_WebSocket::receive(std::string_view message) {
height = d->height;
}
Plot_Input_Event decoded;
decoded.sequence = input->value("sequence", std::uint64_t{});
if (decoded.sequence == 0) return;
decoded.coalesced_event_count = std::clamp(
input->value("coalesced_event_count", std::size_t{1}),
std::size_t{1}, std::size_t{1'000});
@@ -167,13 +146,7 @@ void Graph_WebSocket::receive(std::string_view message) {
input->value("key", std::string{"unknown"})).value_or(Key::unknown);
decoded.native_key = input->value("native_key", 0U);
decoded.auto_repeat = input->value("auto_repeat", false);
const auto weak = weak_from_this();
d->plot->submit_input(
std::move(decoded),
[weak](Plot_Input_Observation observation) {
if (const auto socket = weak.lock())
socket->deliver_input_completion(std::move(observation));
});
d->plot->submit_input(std::move(decoded));
}
catch (const nlohmann::json::exception&) {}
catch (...) {}
-1
View File
@@ -19,7 +19,6 @@ public:
void close() noexcept;
private:
void deliver_frame(std::shared_ptr<const Plot_Stream_Frame> frame);
void deliver_input_completion(Plot_Input_Observation observation);
struct Private;
std::unique_ptr<Private> d;
};
+30 -51
View File
@@ -62,9 +62,15 @@ double elapsed_milliseconds(std::chrono::steady_clock::time_point start,
struct Web_Input_Metadata {
Plot_Input_Event input;
Plot::Input_Handler handler;
std::chrono::steady_clock::time_point scene_dispatched;
};
struct Plot_Input_Observation {
double admission_ms{}; /* WebSocket 接收到提交 Scene 事件流的耗时。 */
double scene_wait_ms{}; /* Scene 收到事件到 Prepare 消费事件的等待。 */
double dispatch_ms{}; /* Scene 开始分发到 Renderable 完成消费的耗时。 */
double server_consume_ms{}; /* WebSocket 接收到 Renderable 完成消费的服务端总耗时。 */
std::size_t coalesced_event_count{}; /* 本次服务端事件代表的浏览器原始事件数量。 */
};
struct Web_Input_Event {
virtual ~Web_Input_Event() = default;
[[nodiscard]] virtual const Web_Input_Metadata& web_input_metadata() const noexcept = 0;
@@ -413,12 +419,11 @@ void Plot_Diagnostics::submit(
}
void Plot_Diagnostics::submit_input(const Plot_Input_Observation& observation) {
const std::array<std::pair<std::string_view, double>, 6> values{{
{"input_plot_queue_ms", observation.plot_queue_ms},
{"input_scene_queue_ms", observation.scene_queue_ms},
{"input_backend_queue_ms", observation.backend_queue_ms},
{"input_frame_completion_ms", observation.frame_completion_ms},
{"input_server_total_ms", observation.server_total_ms},
const std::array<std::pair<std::string_view, double>, 5> values{{
{"input_admission_ms", observation.admission_ms},
{"input_scene_wait_ms", observation.scene_wait_ms},
{"input_dispatch_ms", observation.dispatch_ms},
{"input_server_consume_ms", observation.server_consume_ms},
{"input_coalesced_event_count", static_cast<double>(observation.coalesced_event_count)}}};
input_values.submit(values);
}
@@ -466,10 +471,8 @@ nlohmann::json Plot_Diagnostics::snapshot() const {
}
template <typename Scene_Object>
void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input,
Plot::Input_Handler handler) {
Web_Input_Metadata metadata{input, std::move(handler),
std::chrono::steady_clock::now()};
void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) {
Web_Input_Metadata metadata{input, std::chrono::steady_clock::now()};
const auto dispatch = [&](auto event) {
scene.template submit_stream<aethera::Scene_Event_Stream_Tag>(std::move(event));
};
@@ -568,7 +571,6 @@ struct Plot::Private {
bool tick_task_scheduled{}; /* Plot strand 上是否已有唯一消费任务。 */
double last_clock_render_time_ms{-std::numeric_limits<double>::infinity()};
std::chrono::steady_clock::time_point clock_origin{std::chrono::steady_clock::now()};
std::vector<aethera::Scene::Event_Report_Pointer> pending_event_reports; /* Scene 已处理、等待目标画面完成的报告。 */
template <typename Scene_Object>
Private(asio::any_io_executor executor, std::unique_ptr<Scene_Object> value_scene,
@@ -590,7 +592,7 @@ struct Plot::Private {
void clock_tick(const Plot_Render_Tick& tick);
void render_frame(Plot_Render_Tick tick);
void queue_completed_frame(Render_Frame* frame);
void complete_event_reports(Frame_Identity rendered_identity);
void collect_consumed_input_statistics();
void fail(std::exception_ptr failure) noexcept;
};
@@ -746,6 +748,7 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
(*scene_2d)->set<&Render_Scene_2D::Prop::viewport>(
Size{static_cast<int>(tick.width), static_cast<int>(tick.height)});
const auto result = (*scene_2d)->render(&output);
collect_consumed_input_statistics();
if (result != Render_Scene_2D::Render_Result::completed)
rollback_unsubmitted();
return;
@@ -757,6 +760,7 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
auto& scene_3d = std::get<std::unique_ptr<Scene_3D>>(scene);
scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{tick.width, tick.height});
const auto result = scene_3d->render(&output);
collect_consumed_input_statistics();
if (result == Render_Scene_3D::Render_Result::submitted) return;
rollback_unsubmitted();
if (result == Render_Scene_3D::Render_Result::backend_unavailable)
@@ -768,7 +772,7 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
}
}
void Plot::Private::complete_event_reports(Frame_Identity rendered_identity) {
void Plot::Private::collect_consumed_input_statistics() {
aethera::Scene::Event_Report_Batch reports = std::visit(
[](auto& value) {
return value->template access_query_stream<aethera::Scene_Event_Stream_Tag>(
@@ -779,50 +783,28 @@ void Plot::Private::complete_event_reports(Frame_Identity rendered_identity) {
return result;
});
}, scene);
pending_event_reports.insert(pending_event_reports.end(),
std::make_move_iterator(reports.begin()),
std::make_move_iterator(reports.end()));
const auto now = std::chrono::steady_clock::now();
const auto first_complete = std::stable_partition(
pending_event_reports.begin(), pending_event_reports.end(),
[&](const aethera::Scene::Event_Report_Pointer& event) {
const auto timing = event->dispatch_timing();
return timing.completed_steady_ns == 0 ||
timing.frame_sequence > rendered_identity.sequence;
});
for (auto iterator = first_complete;
iterator != pending_event_reports.end(); ++iterator) {
const auto* web_event = dynamic_cast<const Web_Input_Event*>(iterator->get());
for (const auto& event : reports) {
const auto* web_event = dynamic_cast<const Web_Input_Event*>(event.get());
if (!web_event) continue;
const auto& metadata = web_event->web_input_metadata();
const auto timing = (*iterator)->dispatch_timing();
const auto timing = event->dispatch_timing();
if (timing.completed_steady_ns == 0) continue;
const auto started = std::chrono::steady_clock::time_point{
std::chrono::nanoseconds(timing.started_steady_ns)};
const auto completed = std::chrono::steady_clock::time_point{
std::chrono::nanoseconds(timing.completed_steady_ns)};
Plot_Input_Observation observation{};
observation.sequence = metadata.input.sequence;
observation.type = metadata.input.type;
observation.stage = Plot_Input_Stage::frame_completed;
observation.plot_queue_ms = elapsed_milliseconds(
observation.admission_ms = elapsed_milliseconds(
metadata.input.received_time, metadata.scene_dispatched);
observation.scene_queue_ms = elapsed_milliseconds(
observation.scene_wait_ms = elapsed_milliseconds(
metadata.scene_dispatched, started);
observation.backend_queue_ms = elapsed_milliseconds(started, completed);
observation.frame_completion_ms = elapsed_milliseconds(completed, now);
observation.server_total_ms = elapsed_milliseconds(
metadata.input.received_time, now);
observation.target_frame_sequence = timing.frame_sequence;
observation.rendered_frame_sequence = rendered_identity.sequence;
observation.dispatch_ms = elapsed_milliseconds(started, completed);
observation.server_consume_ms = elapsed_milliseconds(
metadata.input.received_time, completed);
observation.coalesced_event_count =
metadata.input.coalesced_event_count;
diagnostics.submit_input(observation);
try {
if (metadata.handler) metadata.handler(std::move(observation));
}
catch (...) {}
}
pending_event_reports.erase(first_complete, pending_event_reports.end());
}
void Plot::Private::queue_completed_frame(Render_Frame* frame) {
@@ -906,8 +888,6 @@ void Plot::Private::queue_completed_frame(Render_Frame* frame) {
pixel_storage = frame_3d->share_pixels();
}
complete_event_reports(rendered_identity);
auto pixels = std::make_shared<const Plot_Pixel_Frame>(Plot_Pixel_Frame{
std::move(pixel_storage), managed->presentation_time,
identity.sequence, identity.correlation_id,
@@ -1066,7 +1046,7 @@ void Plot::render_once() {
});
}
void Plot::submit_input(Plot_Input_Event event, Input_Handler handler) {
void Plot::submit_input(Plot_Input_Event event) {
ensure_started();
if (d->terminal_failure.load(std::memory_order_acquire)) return;
if (event.received_time == std::chrono::steady_clock::time_point{})
@@ -1078,10 +1058,9 @@ void Plot::submit_input(Plot_Input_Event event, Input_Handler handler) {
*/
try {
if (auto* scene_2d = std::get_if<std::unique_ptr<Scene_2D>>(&d->scene))
dispatch_plot_input(**scene_2d, event, std::move(handler));
dispatch_plot_input(**scene_2d, event);
else
dispatch_plot_input(*std::get<std::unique_ptr<Scene_3D>>(d->scene),
event, std::move(handler));
dispatch_plot_input(*std::get<std::unique_ptr<Scene_3D>>(d->scene), event);
}
catch (...) {
d->fail(std::current_exception());
+1 -21
View File
@@ -15,7 +15,6 @@
namespace aethera::web {
struct Plot_Input_Event {
std::size_t coalesced_event_count{1}; /* 浏览器显示周期内合并的原始输入数量。 */
std::uint64_t sequence{}; /* 页面为本次原始输入分配的单调身份。 */
Event_Type type{Event_Type::pointer_move}; /* 输入事件业务类型。 */
render_2d::Point_F position{}; /* Plot 像素坐标。 */
render_2d::Point_F global_position{}; /* 浏览器屏幕像素坐标。 */
@@ -32,24 +31,6 @@ struct Plot_Input_Event {
std::chrono::steady_clock::time_point received_time{}; /* WebSocket 完成解析的服务端单调时刻。 */
};
enum class Plot_Input_Stage : std::uint8_t {
frame_completed
};
struct Plot_Input_Observation {
std::uint64_t sequence{}; /* 对应页面原始输入身份。 */
Event_Type type{Event_Type::pointer_move}; /* 对应输入事件业务类型。 */
Plot_Input_Stage stage{Plot_Input_Stage::frame_completed}; /* 对外只发布完整画面闭环。 */
double plot_queue_ms{}; /* WebSocket 接收到 Scene 接收的等待。 */
double scene_queue_ms{}; /* Scene 接收事件到下一次 Render 消费事件的等待。 */
double backend_queue_ms{}; /* 事件进入渲染后端到实际应用的等待。 */
double frame_completion_ms{}; /* 后端应用事件到包含该状态的画面完成的等待。 */
double server_total_ms{}; /* WebSocket 接收到画面完成或当前阶段的总耗时。 */
std::uint64_t target_frame_sequence{}; /* 首个应包含本事件状态的逻辑帧。 */
std::uint64_t rendered_frame_sequence{}; /* 实际包含本事件状态的物理画面帧。 */
std::size_t coalesced_event_count{}; /* 合并到本事件的连续输入数量,包含自身。 */
};
struct Plot_Render_Tick {
std::uint64_t sequence{}; /* 页面级帧时钟分配的关联序号。 */
double time_milliseconds{}; /* 页面级单调时间线,所有图共享同一个动画时刻。 */
@@ -78,7 +59,6 @@ public:
using Stream_Id = std::uint64_t;
using Stream_Handler = std::function<void(std::shared_ptr<const Plot_Stream_Frame>)>;
using Json_Handler = std::function<void(nlohmann::json)>;
using Input_Handler = std::function<void(Plot_Input_Observation)>;
class Scene_View {
public:
@@ -107,7 +87,7 @@ public:
void configure_stream(Stream_Id stream, std::uint32_t width, std::uint32_t height);
void schedule_render(Plot_Render_Tick tick);
void render_once();
void submit_input(Plot_Input_Event event, Input_Handler handler);
void submit_input(Plot_Input_Event event);
void async_schema(Json_Handler handler);
void async_write_prop(std::string component, std::string key,
nlohmann::json value, Json_Handler handler);