From f463a756fc4604b76bb53c07796abaa80b80042f Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Mon, 24 Aug 2026 16:27:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=8D=E6=9C=89bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kernel/src/kernel/double_buffer/model.hpp | 20 + kernel/src/kernel/frame.cpp | 160 ++++- kernel/src/kernel/frame.hpp | 6 +- kernel/src/kernel/frame_statistics.cpp | 227 +++++++ kernel/src/kernel/frame_statistics.hpp | 168 +++++ kernel/src/kernel/render_common.cpp | 5 +- render_2D/render_2D/scene/Render_Scene_2D.cpp | 8 +- render_2D/render_2D/scene/Render_Scene_2D.hpp | 8 +- render_2D/render_2D/scene/Render_Scene_2D.ipp | 37 +- render_2D/tests/Axis_Test.cpp | 2 +- render_2D/tests/Plottable_Migration_Test.cpp | 2 +- render_2D/tests/Spectrum_Test.cpp | 58 +- render_3D/render_3D/scene/Render_Scene_3D.cpp | 3 + render_3D/render_3D/scene/Render_Scene_3D.hpp | 3 + render_3D/render_3D/scene/Render_Scene_3D.ipp | 16 +- web_server/src/Gallery_Plots_2D.cpp | 53 +- web_server/src/Gallery_Video_Stream.cpp | 58 +- web_server/src/H264_Encoder.cpp | 71 +- web_server/src/Plot.cpp | 624 +++++------------- web_server/src/Plot.hpp | 9 +- web_server/src/Sliding_Statistics.cpp | 143 ---- web_server/src/Sliding_Statistics.hpp | 63 -- web_server/src/WebRtc_Video_Session.cpp | 93 +-- web_server/src/Web_Server.cpp | 32 +- web_server/tests/Sliding_Statistics_Tests.cpp | 65 +- webapp_gallery/src/app.tsx | 157 +++-- webapp_gallery/src/styles.css | 8 +- 27 files changed, 1201 insertions(+), 898 deletions(-) create mode 100644 kernel/src/kernel/frame_statistics.cpp create mode 100644 kernel/src/kernel/frame_statistics.hpp delete mode 100644 web_server/src/Sliding_Statistics.cpp delete mode 100644 web_server/src/Sliding_Statistics.hpp diff --git a/kernel/src/kernel/double_buffer/model.hpp b/kernel/src/kernel/double_buffer/model.hpp index 42a27a8..5eba354 100644 --- a/kernel/src/kernel/double_buffer/model.hpp +++ b/kernel/src/kernel/double_buffer/model.hpp @@ -411,6 +411,26 @@ public: std::lock_guard guard(lock); advance_unlocked(); } + template Tag> + void publish_state() { + std::lock_guard guard(lock); + data().state.advance(); + data().state_callbacks.template notify(*data().state.current); + } + template Tag, auto... Members, typename Callback> + requires (sizeof...(Members) > 0) && + (detail::State_Member && ...) && + std::invocable> + void publish_state(Callback&& callback) { + std::lock_guard guard(lock); + (before_state_set(Members), ...); + std::invoke(std::forward(callback), + State_Access{*data().state.pending}); + (after_state_set(Members), ...); + (emit_state_dependencies(), ...); + data().state.advance(); + data().state_callbacks.template notify(*data().state.current); + } template Callback> void advance(Callback&& callback) { std::lock_guard guard(lock); diff --git a/kernel/src/kernel/frame.cpp b/kernel/src/kernel/frame.cpp index 9017924..399ee11 100644 --- a/kernel/src/kernel/frame.cpp +++ b/kernel/src/kernel/frame.cpp @@ -1,9 +1,11 @@ #include "frame.hpp" +#include "frame_statistics.hpp" #include #include #include #include #include +#include namespace aethera { namespace { constexpr std::size_t marker_count = static_cast(Frame_Trace_Marker::count); @@ -49,16 +51,154 @@ void Render_Frame::record(Frame_Trace_Measurement measurement, std::uint64_t val std::uint64_t expected{}; d->measurements[index].compare_exchange_strong(expected, encode_present_value(value_ns), std::memory_order_release, std::memory_order_relaxed); } -std::vector Render_Frame::trace_points() const { - std::vector result; - result.reserve(marker_count); - for (std::size_t index = 0; index < marker_count; ++index) if (const auto encoded = d->markers[index].load(std::memory_order_acquire)) result.push_back({static_cast(index), decode_present_value(encoded)}); - return result; -} -std::vector Render_Frame::trace_values() const { - std::vector result; - result.reserve(measurement_count); - for (std::size_t index = 0; index < measurement_count; ++index) if (const auto encoded = d->measurements[index].load(std::memory_order_acquire)) result.push_back({static_cast(index), decode_present_value(encoded)}); +Frame_Statistics_Sample Render_Frame::statistics(Frame_Dimension dimension) const { + std::array, marker_count> markers{}; + for (std::size_t index = 0; index < marker_count; ++index) { + const auto encoded = d->markers[index].load(std::memory_order_acquire); + if (encoded) + markers[index] = static_cast(decode_present_value(encoded)) / + 1'000'000.0; + } + std::array measurements{}; + for (std::size_t index = 0; index < measurement_count; ++index) { + const auto encoded = d->measurements[index].load(std::memory_order_acquire); + if (encoded) + measurements[index] = static_cast(decode_present_value(encoded)) / + 1'000'000.0; + } + const auto marker = [&](Frame_Trace_Marker value) { + return markers[static_cast(value)].value_or(0.0); + }; + const auto interval = [&](Frame_Trace_Marker first, Frame_Trace_Marker last) { + const auto start = markers[static_cast(first)]; + const auto finish = markers[static_cast(last)]; + return start && finish ? std::max(0.0, *finish - *start) : 0.0; + }; + const auto measurement = [&](Frame_Trace_Measurement value) { + return measurements[static_cast(value)]; + }; + + Frame_Statistics_Sample result{}; + result.set(Frame_Statistic::server_completion_ms, + marker(Frame_Trace_Marker::frame_ready)); + result.set(Frame_Statistic::scene_render_ms, interval( + Frame_Trace_Marker::scene_render_started, + Frame_Trace_Marker::scene_render_finished)); + result.set(Frame_Statistic::event_dispatch_ms, interval( + Frame_Trace_Marker::event_dispatch_started, + Frame_Trace_Marker::event_dispatch_finished)); + result.set(Frame_Statistic::prepare_ms, interval( + Frame_Trace_Marker::prepare_started, Frame_Trace_Marker::prepare_finished)); + result.set(Frame_Statistic::paint_ms, interval( + Frame_Trace_Marker::paint_started, Frame_Trace_Marker::paint_finished)); + result.set(Frame_Statistic::backend_queue_ms, interval( + Frame_Trace_Marker::backend_queue_entered, Frame_Trace_Marker::backend_queue_left)); + result.set(Frame_Statistic::gpu_submission_ms, interval( + Frame_Trace_Marker::gpu_submitted, Frame_Trace_Marker::gpu_completed)); + result.set(Frame_Statistic::readback_stage_ms, interval( + Frame_Trace_Marker::readback_started, Frame_Trace_Marker::readback_finished)); + result.set(Frame_Statistic::callback_ms, interval( + Frame_Trace_Marker::callback_started, Frame_Trace_Marker::callback_finished)); + + constexpr std::array measurement_statistics{ + Frame_Statistic::backend_apply_ms, + Frame_Statistic::backend_plan_ms, + Frame_Statistic::backend_execute_ms, + Frame_Statistic::backend_submit_ms, + Frame_Statistic::gpu_fence_wait_ms, + Frame_Statistic::gpu_render_ms, + Frame_Statistic::gpu_transition_ms, + Frame_Statistic::gpu_copy_ms, + Frame_Statistic::gpu_total_ms, + Frame_Statistic::readback_ms}; + for (std::size_t index = 0; index < measurement_statistics.size(); ++index) + result.set(measurement_statistics[index], measurements[index]); + + double remaining = marker(Frame_Trace_Marker::frame_ready); + const auto take = [&](double requested) { + const auto value = std::min(remaining, std::max(0.0, requested)); + remaining -= value; + return value; + }; + const double scene_time = interval(Frame_Trace_Marker::scene_render_started, + Frame_Trace_Marker::scene_render_finished); + const double event_time = std::min(scene_time, interval( + Frame_Trace_Marker::event_dispatch_started, + Frame_Trace_Marker::event_dispatch_finished)); + const double prepare_time = std::min(std::max(0.0, scene_time - event_time), interval( + Frame_Trace_Marker::prepare_started, Frame_Trace_Marker::prepare_finished)); + const double paint_time = std::min( + std::max(0.0, scene_time - event_time - prepare_time), interval( + Frame_Trace_Marker::paint_started, Frame_Trace_Marker::paint_finished)); + + if (dimension == Frame_Dimension::two_dimensional) { + result.set(Frame_Statistic::pipeline_2d_event_ms, take(event_time)); + result.set(Frame_Statistic::pipeline_2d_prepare_ms, take(prepare_time)); + result.set(Frame_Statistic::pipeline_2d_paint_ms, take(paint_time)); + result.set(Frame_Statistic::pipeline_2d_scene_coordination_ms, + take(std::max(0.0, scene_time - event_time - prepare_time - paint_time))); + result.set(Frame_Statistic::pipeline_2d_callback_ms, take(interval( + Frame_Trace_Marker::callback_started, Frame_Trace_Marker::callback_finished))); + result.set(Frame_Statistic::pipeline_2d_frame_handoff_ms, remaining); + return result; + } + + result.set(Frame_Statistic::pipeline_3d_event_ms, take(event_time)); + result.set(Frame_Statistic::pipeline_3d_prepare_ms, take(prepare_time)); + result.set(Frame_Statistic::pipeline_3d_submit_graph_ms, take(paint_time)); + result.set(Frame_Statistic::pipeline_3d_scene_coordination_ms, + take(std::max(0.0, scene_time - event_time - prepare_time - paint_time))); + const double scene_finished = marker(Frame_Trace_Marker::scene_render_finished); + const double queue_entered = marker(Frame_Trace_Marker::backend_queue_entered); + const double backend_prepare_started = marker(Frame_Trace_Marker::backend_prepare_started); + const double backend_prepare_finished = marker(Frame_Trace_Marker::backend_prepare_finished); + const double submit_queued = marker(Frame_Trace_Marker::backend_submit_queued); + const double queue_left = marker(Frame_Trace_Marker::backend_queue_left); + result.set(Frame_Statistic::pipeline_3d_prepare_queue_ms, take(std::max( + 0.0, backend_prepare_started - std::max(scene_finished, queue_entered)))); + double preparation_window = std::max(0.0, + backend_prepare_finished - backend_prepare_started); + const auto take_preparation = [&](Frame_Trace_Measurement key) { + const double value = std::min(preparation_window, + std::max(0.0, measurement(key))); + preparation_window -= value; + return take(value); + }; + result.set(Frame_Statistic::pipeline_3d_backend_apply_ms, + take_preparation(Frame_Trace_Measurement::backend_apply_ns)); + result.set(Frame_Statistic::pipeline_3d_backend_plan_ms, + take_preparation(Frame_Trace_Measurement::backend_plan_ns)); + result.set(Frame_Statistic::pipeline_3d_backend_execute_ms, + take_preparation(Frame_Trace_Measurement::backend_execute_ns)); + result.set(Frame_Statistic::pipeline_3d_backend_commands_ms, take(preparation_window)); + result.set(Frame_Statistic::pipeline_3d_backend_queue_ms, + take(std::max(0.0, queue_left - submit_queued))); + const double gpu_submitted = marker(Frame_Trace_Marker::gpu_submitted); + double submit_window = std::max(0.0, gpu_submitted - queue_left); + const double measured_submit = std::min(submit_window, std::max( + 0.0, measurement(Frame_Trace_Measurement::backend_submit_ns))); + result.set(Frame_Statistic::pipeline_3d_backend_submit_ms, take(measured_submit)); + submit_window -= measured_submit; + result.set(Frame_Statistic::pipeline_3d_submit_handoff_ms, take(submit_window)); + double gpu_window = interval(Frame_Trace_Marker::gpu_submitted, + Frame_Trace_Marker::gpu_completed); + const auto take_gpu = [&](Frame_Trace_Measurement key) { + const double value = std::min(gpu_window, std::max(0.0, measurement(key))); + gpu_window -= value; + return take(value); + }; + result.set(Frame_Statistic::pipeline_3d_gpu_render_ms, + take_gpu(Frame_Trace_Measurement::gpu_render_ns)); + result.set(Frame_Statistic::pipeline_3d_gpu_transition_ms, + take_gpu(Frame_Trace_Measurement::gpu_transition_ns)); + result.set(Frame_Statistic::pipeline_3d_gpu_copy_ms, + take_gpu(Frame_Trace_Measurement::gpu_copy_ns)); + result.set(Frame_Statistic::pipeline_3d_gpu_sync_ms, take(gpu_window)); + result.set(Frame_Statistic::pipeline_3d_readback_ms, take(interval( + Frame_Trace_Marker::readback_started, Frame_Trace_Marker::readback_finished))); + result.set(Frame_Statistic::pipeline_3d_callback_ms, take(interval( + Frame_Trace_Marker::callback_started, Frame_Trace_Marker::callback_finished))); + result.set(Frame_Statistic::pipeline_3d_completion_handoff_ms, remaining); return result; } } diff --git a/kernel/src/kernel/frame.hpp b/kernel/src/kernel/frame.hpp index 54075cb..889565e 100644 --- a/kernel/src/kernel/frame.hpp +++ b/kernel/src/kernel/frame.hpp @@ -4,6 +4,8 @@ #include #include namespace aethera { +enum class Frame_Dimension : std::uint8_t; +struct Frame_Statistics_Sample; enum class Frame_Trace_Marker : std::uint8_t { created, scene_render_requested, @@ -43,6 +45,7 @@ enum class Frame_Trace_Measurement : std::uint8_t { count }; struct Frame_Identity { + friend bool operator==(const Frame_Identity&, const Frame_Identity&) = default; std::uint64_t sequence{}; /* 外部帧管理器分配的单调帧序号。 */ std::uint64_t correlation_id{}; /* 与调用方请求关联的标识;零值表示未关联。 */ }; @@ -62,8 +65,7 @@ public: [[nodiscard]] std::uint64_t created_time_unix_ns() const noexcept; void mark(Frame_Trace_Marker marker) noexcept; void record(Frame_Trace_Measurement measurement, std::uint64_t value_ns) noexcept; - [[nodiscard]] std::vector trace_points() const; - [[nodiscard]] std::vector trace_values() const; + [[nodiscard]] Frame_Statistics_Sample statistics(Frame_Dimension dimension) const; protected: /* 物理帧槽再次承载新逻辑帧时,重建其唯一身份和诊断时间原点。 */ void begin(Frame_Identity identity) noexcept; diff --git a/kernel/src/kernel/frame_statistics.cpp b/kernel/src/kernel/frame_statistics.cpp new file mode 100644 index 0000000..f6a1188 --- /dev/null +++ b/kernel/src/kernel/frame_statistics.cpp @@ -0,0 +1,227 @@ +#include "frame_statistics.hpp" +#include +#include +#include + +namespace aethera { +void Frame_Statistics_Sample::set(Frame_Statistic statistic, double value) noexcept { + const auto index = static_cast(statistic); + if (index >= values.size() || !std::isfinite(value)) return; + values[index] = value; + present.set(index); +} + +Sliding_Statistics::Sliding_Statistics() : Sliding_Statistics(600) {} + +Sliding_Statistics::Sliding_Statistics(std::size_t capacity) + : values_(capacity), trimmed_values_(capacity), + minimum_(capacity, true), maximum_(capacity, false) { + if (capacity == 0) + throw std::invalid_argument("statistics capacity must be positive"); +} + +Sliding_Statistics::Extremum_Queue::Extremum_Queue( + std::size_t capacity, bool minimum) + : nodes_(capacity + 1U), minimum_(minimum) {} + +void Sliding_Statistics::Extremum_Queue::submit( + double sample, std::uint64_t sequence, std::size_t window) noexcept { + const auto count = nodes_.size(); + while (head_ != tail_ && + nodes_[head_].sequence + window <= sequence) + head_ = (head_ + 1U) % count; + while (head_ != tail_) { + const auto previous = (tail_ + count - 1U) % count; + const bool superseded = minimum_ ? nodes_[previous].value >= sample + : nodes_[previous].value <= sample; + if (!superseded) break; + tail_ = previous; + } + nodes_[tail_] = Node{sample, sequence}; + tail_ = (tail_ + 1U) % count; +} + +double Sliding_Statistics::Extremum_Queue::value() const noexcept { + return head_ == tail_ ? 0.0 : nodes_[head_].value; +} + +void Sliding_Statistics::Extremum_Queue::reset() noexcept { + head_ = 0; + tail_ = 0; +} + +Sliding_Statistics::Quantile_Estimator::Quantile_Estimator( + double probability) noexcept : probability_(probability) {} + +void Sliding_Statistics::Quantile_Estimator::submit(double sample) noexcept { + if (count_ < initial_.size()) { + initial_[count_++] = sample; + if (count_ != initial_.size()) return; + std::ranges::sort(initial_); + heights_ = initial_; + positions_ = {1.0, 2.0, 3.0, 4.0, 5.0}; + desired_ = {1.0, 1.0 + 2.0 * probability_, + 1.0 + 4.0 * probability_, + 3.0 + 2.0 * probability_, 5.0}; + increments_ = {0.0, probability_ / 2.0, probability_, + (1.0 + probability_) / 2.0, 1.0}; + return; + } + ++count_; + std::size_t bucket{}; + if (sample < heights_[0]) { + heights_[0] = sample; + } else if (sample >= heights_[4]) { + heights_[4] = sample; + bucket = 3; + } else { + while (bucket < 3 && sample >= heights_[bucket + 1]) ++bucket; + } + for (std::size_t index = bucket + 1; index < positions_.size(); ++index) + positions_[index] += 1.0; + for (std::size_t index = 0; index < desired_.size(); ++index) + desired_[index] += increments_[index]; + for (std::size_t index = 1; index < 4; ++index) { + const double distance = desired_[index] - positions_[index]; + const double direction = distance >= 1.0 ? 1.0 : distance <= -1.0 ? -1.0 : 0.0; + if (direction == 0.0 || + (direction > 0.0 && positions_[index + 1] - positions_[index] <= 1.0) || + (direction < 0.0 && positions_[index - 1] - positions_[index] >= -1.0)) + continue; + const double left = positions_[index] - positions_[index - 1]; + const double right = positions_[index + 1] - positions_[index]; + const double estimate = heights_[index] + direction / (left + right) * + ((left + direction) * (heights_[index + 1] - heights_[index]) / right + + (right - direction) * (heights_[index] - heights_[index - 1]) / left); + if (heights_[index - 1] < estimate && estimate < heights_[index + 1]) + heights_[index] = estimate; + else { + const auto neighbor = static_cast( + static_cast(index) + + static_cast(direction)); + heights_[index] += direction * + (heights_[neighbor] - heights_[index]) / + (positions_[neighbor] - positions_[index]); + } + positions_[index] += direction; + } +} + +double Sliding_Statistics::Quantile_Estimator::value() const noexcept { + if (count_ == 0) return 0.0; + if (count_ >= initial_.size()) return heights_[2]; + auto ordered = initial_; + std::sort(ordered.begin(), ordered.begin() + static_cast(count_)); + const auto index = std::min(count_ - 1U, static_cast( + std::ceil(probability_ * static_cast(count_))) - 1U); + return ordered[index]; +} + +void Sliding_Statistics::Quantile_Estimator::reset() noexcept { + initial_ = {}; + heights_ = {}; + positions_ = {}; + desired_ = {}; + increments_ = {}; + count_ = 0; +} + +Statistic_State Sliding_Statistics::submit(double value) { + if (!std::isfinite(value)) return state_; + if (size_ == values_.size()) { + if (!trimmed_window_initialized_) { + const double lower = p05_.value(); + const double upper = p95_.value(); + trimmed_sum_ = 0.0; + for (std::size_t index = 0; index < size_; ++index) { + trimmed_values_[index] = std::clamp(values_[index], lower, upper); + trimmed_sum_ += trimmed_values_[index]; + } + trimmed_window_initialized_ = true; + } + const double discarded = values_[next_]; + sum_ -= discarded; + squared_sum_ -= discarded * discarded; + trimmed_sum_ -= trimmed_values_[next_]; + } + p05_.submit(value); + p50_.submit(value); + p95_.submit(value); + p99_.submit(value); + minimum_.submit(value, sequence_, values_.size()); + maximum_.submit(value, sequence_, values_.size()); + ++sequence_; + const double trimmed = trimmed_window_initialized_ + ? std::clamp(value, p05_.value(), p95_.value()) : value; + values_[next_] = value; + trimmed_values_[next_] = trimmed; + sum_ += value; + squared_sum_ += value * value; + trimmed_sum_ += trimmed; + next_ = (next_ + 1U) % values_.size(); + size_ = std::min(size_ + 1U, values_.size()); + const double mean = sum_ / static_cast(size_); + state_ = Statistic_State{ + size_, value, minimum_.value(), maximum_.value(), mean, + trimmed_sum_ / static_cast(size_), + std::sqrt(std::max(0.0, + squared_sum_ / static_cast(size_) - mean * mean)), + p50_.value(), p95_.value(), p99_.value()}; + return state_; +} + +const Statistic_State& Sliding_Statistics::state() const noexcept { return state_; } + +void Sliding_Statistics::reset() noexcept { + size_ = 0; + next_ = 0; + sum_ = 0.0; + squared_sum_ = 0.0; + trimmed_sum_ = 0.0; + trimmed_window_initialized_ = false; + sequence_ = 0; + minimum_.reset(); + maximum_.reset(); + p05_.reset(); + p50_.reset(); + p95_.reset(); + p99_.reset(); + state_ = {}; +} + +Frame_Statistics_Accumulator::Frame_Statistics_Accumulator(std::size_t capacity) { + values_.reserve(frame_statistic_count); + for (std::size_t index = 0; index < frame_statistic_count; ++index) + values_.emplace_back(capacity); +} + +const Frame_Statistics_State& Frame_Statistics_Accumulator::submit( + const Render_Frame& frame, Frame_Dimension dimension) { + auto sample = frame.statistics(dimension); + const auto now = std::chrono::steady_clock::now(); + const auto frame_identity = frame.identity(); + if (previous_completion_ != std::chrono::steady_clock::time_point{}) { + sample.set(Frame_Statistic::frame_interval_ms, + std::chrono::duration( + now - previous_completion_).count()); + if (frame_identity.sequence > previous_sequence_ + 1U) + state_.dropped_sequences += frame_identity.sequence - previous_sequence_ - 1U; + } + previous_completion_ = now; + previous_sequence_ = frame_identity.sequence; + state_.identity = frame_identity; + state_.created_time_unix_ns = frame.created_time_unix_ns(); + for (std::size_t index = 0; index < sample.values.size(); ++index) { + if (!sample.present.test(index)) continue; + state_.values[index] = values_[index].submit(sample.values[index]); + } + return state_; +} + +void Frame_Statistics_Accumulator::reset() noexcept { + for (auto& value : values_) value.reset(); + state_ = {}; + previous_completion_ = {}; + previous_sequence_ = 0; +} +} diff --git a/kernel/src/kernel/frame_statistics.hpp b/kernel/src/kernel/frame_statistics.hpp new file mode 100644 index 0000000..d7d0231 --- /dev/null +++ b/kernel/src/kernel/frame_statistics.hpp @@ -0,0 +1,168 @@ +#pragma once +#include "frame.hpp" +#include +#include +#include +#include +#include +#include + +namespace aethera { +enum class Frame_Dimension : std::uint8_t { two_dimensional, three_dimensional }; + +enum class Frame_Statistic : std::uint8_t { + server_completion_ms, + scene_render_ms, + event_dispatch_ms, + prepare_ms, + paint_ms, + backend_queue_ms, + gpu_submission_ms, + readback_stage_ms, + callback_ms, + backend_apply_ms, + backend_plan_ms, + backend_execute_ms, + backend_submit_ms, + gpu_fence_wait_ms, + gpu_render_ms, + gpu_transition_ms, + gpu_copy_ms, + gpu_total_ms, + readback_ms, + pipeline_2d_event_ms, + pipeline_2d_prepare_ms, + pipeline_2d_paint_ms, + pipeline_2d_scene_coordination_ms, + pipeline_2d_callback_ms, + pipeline_2d_frame_handoff_ms, + pipeline_3d_event_ms, + pipeline_3d_prepare_ms, + pipeline_3d_submit_graph_ms, + pipeline_3d_scene_coordination_ms, + pipeline_3d_prepare_queue_ms, + pipeline_3d_backend_apply_ms, + pipeline_3d_backend_plan_ms, + pipeline_3d_backend_execute_ms, + pipeline_3d_backend_commands_ms, + pipeline_3d_backend_queue_ms, + pipeline_3d_backend_submit_ms, + pipeline_3d_submit_handoff_ms, + pipeline_3d_gpu_render_ms, + pipeline_3d_gpu_transition_ms, + pipeline_3d_gpu_copy_ms, + pipeline_3d_gpu_sync_ms, + pipeline_3d_readback_ms, + pipeline_3d_callback_ms, + pipeline_3d_completion_handoff_ms, + frame_interval_ms, + count +}; + +inline constexpr std::size_t frame_statistic_count = + static_cast(Frame_Statistic::count); + +struct Frame_Statistics_Sample { + std::array values{}; + std::bitset present{}; + + void set(Frame_Statistic statistic, double value) noexcept; +}; + +struct Statistic_State { + std::size_t count{}; + double latest{}; + double minimum{}; + double maximum{}; + double average{}; + double trimmed_average{}; + double variability{}; + double p50{}; + double p95{}; + double p99{}; + bool operator==(const Statistic_State&) const = default; +}; + +class Sliding_Statistics final { +public: + Sliding_Statistics(); + explicit Sliding_Statistics(std::size_t capacity); + [[nodiscard]] Statistic_State submit(double value); + [[nodiscard]] const Statistic_State& state() const noexcept; + void reset() noexcept; + +private: + /* P² 只维护五个标记点,分位数是从 reset 起的在线估计,不保存样本。 */ + class Quantile_Estimator final { + public: + explicit Quantile_Estimator(double probability) noexcept; + void submit(double value) noexcept; + [[nodiscard]] double value() const noexcept; + void reset() noexcept; + private: + double probability_{}; + std::array initial_{}; + std::array heights_{}; + std::array positions_{}; + std::array desired_{}; + std::array increments_{}; + std::size_t count_{}; + }; + + /* 预分配单调队列,O(1) 摊还维护精确滑动窗口极值。 */ + class Extremum_Queue final { + public: + Extremum_Queue(std::size_t capacity, bool minimum); + void submit(double value, std::uint64_t sequence, + std::size_t window) noexcept; + [[nodiscard]] double value() const noexcept; + void reset() noexcept; + private: + struct Node { double value{}; std::uint64_t sequence{}; }; + std::vector nodes_; + std::size_t head_{}; + std::size_t tail_{}; + bool minimum_{}; + }; + + /* 原始值和截尾值只为精确滑动均值/方差服务,构造后不再分配。 */ + std::vector values_; + std::vector trimmed_values_; + std::size_t size_{}; + std::size_t next_{}; + double sum_{}; + double squared_sum_{}; + double trimmed_sum_{}; + bool trimmed_window_initialized_{}; + std::uint64_t sequence_{}; + Extremum_Queue minimum_; + Extremum_Queue maximum_; + Quantile_Estimator p05_{0.05}; + Quantile_Estimator p50_{0.50}; + Quantile_Estimator p95_{0.95}; + Quantile_Estimator p99_{0.99}; + Statistic_State state_{}; +}; + +struct Frame_Statistics_State { + /* 可直接通过 Scene State 双缓冲发布的定长结果;不含统计器内部样本。 */ + std::array values{}; + Frame_Identity identity{}; + std::uint64_t created_time_unix_ns{}; + std::uint64_t dropped_sequences{}; + bool operator==(const Frame_Statistics_State&) const = default; +}; + +class Frame_Statistics_Accumulator final { +public: + explicit Frame_Statistics_Accumulator(std::size_t capacity = 600); + [[nodiscard]] const Frame_Statistics_State& submit( + const Render_Frame& frame, Frame_Dimension dimension); + void reset() noexcept; +private: + std::vector values_; + Frame_Statistics_State state_{}; + std::chrono::steady_clock::time_point previous_completion_{}; + std::uint64_t previous_sequence_{}; +}; +} diff --git a/kernel/src/kernel/render_common.cpp b/kernel/src/kernel/render_common.cpp index 704a030..53f8244 100644 --- a/kernel/src/kernel/render_common.cpp +++ b/kernel/src/kernel/render_common.cpp @@ -202,14 +202,13 @@ private: std::atomic_bool state_callback_enabled{}; void create_executor(std::size_t workers, std::shared_ptr worker_interface) { executor = std::make_unique(workers, std::move(worker_interface)); - observer = executor->make_observer(); + observer.reset(); state = {}; } void ensure_executor() { std::lock_guard guard(state_mutex); if (executor) return; executor = std::make_unique(); - observer = executor->make_observer(); } void publish_state() { if (!state_callback_enabled.load(std::memory_order_acquire)) return; @@ -283,7 +282,9 @@ public: return memory; } void set_state_callback(std::function callback) { + ensure_executor(); std::lock_guard guard(state_mutex); + if (!observer) observer = executor->make_observer(); state_callbacks.template set(std::move(callback)); state_callback_enabled.store(true, std::memory_order_release); } diff --git a/render_2D/render_2D/scene/Render_Scene_2D.cpp b/render_2D/render_2D/scene/Render_Scene_2D.cpp index f46ea7c..25f96fd 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.cpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.cpp @@ -4,7 +4,10 @@ Render_Scene_2D::Private::~Private() = default; bool Render_Scene_2D::State::operator==(const State&) const = default; bool Render_Scene_2D::Prop::operator==(const Prop&) const = default; -Render_Scene_2D::Render_Result Render_Scene_2D::render(Frame_2D* frame) { return static_cast(*d).dispatch->render(this, frame); } +std::expected +Render_Scene_2D::render(Frame_2D* frame) { + return static_cast(*d).dispatch->render(this, frame); +} void Render_Scene_2D::set_frame_callback(Frame_Callback callback) { static_cast(*d).dispatch->set_frame_callback(this, std::move(callback)); } tf::Taskflow& Render_Scene_2D::completion_taskflow() { return static_cast(*d).completion_graph; @@ -15,4 +18,7 @@ void Render_Scene_2D::activate_view() { void Render_Scene_2D::deactivate_view() { static_cast(*d).dispatch->set_active(this, false); } +void Render_Scene_2D::reset_frame_statistics() { + static_cast(*d).dispatch->reset_statistics(this); +} } diff --git a/render_2D/render_2D/scene/Render_Scene_2D.hpp b/render_2D/render_2D/scene/Render_Scene_2D.hpp index da2aa6b..945e7ad 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.hpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.hpp @@ -3,6 +3,8 @@ #include "../base/Renderable_2D.hpp" #include "../render/Blend2D_Cache.hpp" #include +#include +#include #include #include namespace aethera::render_2d { @@ -17,6 +19,7 @@ struct Render_Scene_2D : Def(Object*)>> attachments{}; /* 仅在 build 期间绑定已构造 Renderable。 */ }; - enum class Render_Result { completed, frame_in_flight, view_inactive, empty_viewport }; + enum class Render_Result { frame_in_flight, view_inactive, empty_viewport }; using Frame_Callback = std::function; /* 向调用方拥有的帧合成一次;必须先安装回调。回调返回前的并发请求返回 frame_in_flight。 */ - [[nodiscard]] Render_Result render(Frame_2D* frame); + [[nodiscard]] std::expected render(Frame_2D* frame); /* 安装完成帧回调;回调收到对应 render(frame) 的对象,返回时 Scene 才释放下一帧准入。 */ void set_frame_callback(Frame_Callback callback); /* @@ -50,6 +53,7 @@ struct Render_Scene_2D : Def, Dependency_Graph_Error> Render_Scene_2D:: return scene; } struct Render_Scene_2D::Private : Prev_Private { - using Render_Run = Render_Result (*)(Root*, Frame_2D*); + using Render_Run = std::expected (*)(Root*, Frame_2D*); using Callback_Run = void (*)(Root*, Frame_Callback); using Active_Run = void (*)(Root*, bool); struct Paint_Node { @@ -54,6 +54,7 @@ struct Render_Scene_2D::Private : Prev_Private { Renderable_2D_Base::Private* private_data{}; /* 候选对象的二维能力层;仅在本次 Prepare 分发期间有效。 */ }; struct Dispatch { + void (*reset_statistics)(Root*); Render_Run render; /* 向外部帧执行最终 Scene 并合成颜色层。 */ Callback_Run set_frame_callback; /* 安装最终完成帧回调。 */ Active_Run set_active; /* 修改最终 Scene 的视图活动状态。 */ @@ -62,6 +63,7 @@ struct Render_Scene_2D::Private : Prev_Private { std::mutex render_mutex{}; /* 只保护完成回调和单帧准入。 */ bool frame_in_flight{}; /* render 准入到完成回调返回的唯一状态源。 */ Frame_Callback frame_callback{}; /* 合成完成后的唯一像素发布出口。 */ + Frame_Statistics_Accumulator frame_statistics{}; /* Scene 内部增量计算;State 只发布定长统计结果。 */ tf::Taskflow completion_graph{}; /* 最终像素完成后、发布回调前执行的外部续写图。 */ std::unique_ptr paint_taskflow{}; /* 仅由二维 Paint 图构建的执行图。 */ ~Private(); @@ -81,7 +83,9 @@ struct Render_Scene_2D::Private : Prev_Private { template void dispatch_events(Object* object, Size viewport, std::uint64_t frame_sequence); - template [[nodiscard]] Render_Result render(Object* object, Frame_2D* frame); + template + [[nodiscard]] std::expected render(Object* object, + Frame_2D* frame); template [[nodiscard]] static const Dispatch& dispatch_for(); /* CRTP 覆盖:Builder 挂接最终 Private 后安装二维 Scene 的无虚函数业务分派。 */ @@ -297,14 +301,16 @@ void Render_Scene_2D::Private::process(Object* object, Callback&& callback) std::invoke(std::forward(callback)); } template -Render_Scene_2D::Render_Result Render_Scene_2D::Private::render(Object* object, Frame_2D* frame) { +std::expected +Render_Scene_2D::Private::render(Object* object, Frame_2D* frame) { static_cast(detail::Frame_2D_Access::render_target(frame)); Frame_Callback callback; { std::lock_guard lock(render_mutex); if (!frame_callback) throw std::logic_error("Render_Scene_2D requires a frame callback before render"); - if (frame_in_flight) return Render_Result::frame_in_flight; + if (frame_in_flight) + return std::unexpected(Render_Result::frame_in_flight); frame_in_flight = true; callback = frame_callback; } @@ -331,10 +337,18 @@ Render_Scene_2D::Render_Result Render_Scene_2D::Private::render(Object* object, frame->mark(Frame_Trace_Marker::callback_started); callback(frame); frame->mark(Frame_Trace_Marker::callback_finished); + frame->mark(Frame_Trace_Marker::frame_ready); + auto& private_data = static_cast(*this); + auto& scene_state = static_cast(*private_data.state.pending); + scene_state.frame_statistics = frame_statistics.submit( + *frame, Frame_Dimension::two_dimensional); + private_data.state.advance(); + object->template notify_state(); }); - if (completed) return Render_Result::completed; + if (completed) return {}; const auto& prop = object->template read_prop(); - return prop.view_active ? Render_Result::empty_viewport : Render_Result::view_inactive; + return std::unexpected(prop.view_active ? Render_Result::empty_viewport + : Render_Result::view_inactive); } template void Render_Scene_2D::Private::dispatch_events(Object* object, Size viewport, @@ -367,6 +381,17 @@ void Render_Scene_2D::Private::dispatch_events(Object* object, Size viewport, template const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for() { static const Dispatch value{ + [](Root* root) { + auto* object = static_cast(root); + auto& data = static_cast(*object->d); + object->template publish_state( + [&data](State_Access states) { + data.frame_statistics.reset(); + states.template get() + .frame_statistics = {}; + }); + }, [](Root* root, Frame_2D* frame) { auto* object = static_cast(root); return static_cast(*object->d).render(object, frame); diff --git a/render_2D/tests/Axis_Test.cpp b/render_2D/tests/Axis_Test.cpp index c50d842..04ad208 100644 --- a/render_2D/tests/Axis_Test.cpp +++ b/render_2D/tests/Axis_Test.cpp @@ -18,7 +18,7 @@ std::unique_ptr render_frame(Scene* scene) { static std::uint64_t sequence{1}; auto frame = std::make_unique(Frame_Identity{sequence++, 0}); scene->set_frame_callback([](Frame_2D*) {}); - EXPECT_EQ(scene->render(frame.get()), Render_Scene_2D::Render_Result::completed); + EXPECT_TRUE(scene->render(frame.get()).has_value()); return frame; } template diff --git a/render_2D/tests/Plottable_Migration_Test.cpp b/render_2D/tests/Plottable_Migration_Test.cpp index c74b7f3..bc5ffb4 100644 --- a/render_2D/tests/Plottable_Migration_Test.cpp +++ b/render_2D/tests/Plottable_Migration_Test.cpp @@ -36,7 +36,7 @@ void render_once(Scene* scene) { static std::uint64_t sequence{1}; Frame_2D frame{Frame_Identity{sequence++, 0}}; scene->set_frame_callback([](Frame_2D*) {}); - EXPECT_EQ(scene->render(&frame), Render_Scene_2D::Render_Result::completed); + EXPECT_TRUE(scene->render(&frame).has_value()); } } diff --git a/render_2D/tests/Spectrum_Test.cpp b/render_2D/tests/Spectrum_Test.cpp index c919429..3d30664 100644 --- a/render_2D/tests/Spectrum_Test.cpp +++ b/render_2D/tests/Spectrum_Test.cpp @@ -28,12 +28,26 @@ bool contains_color(Image_View view) { } return false; } +std::uint64_t image_hash(Image_View view) { + std::uint64_t value{1469598103934665603ULL}; + for (int y = 0; y < view.height; ++y) { + const auto* row = reinterpret_cast(view.data) + + static_cast(y) * view.stride; + for (std::ptrdiff_t index = 0; + index < static_cast(view.width * sizeof(Pixel)); + ++index) { + value ^= static_cast(row[index]); + value *= 1099511628211ULL; + } + } + return value; +} template std::unique_ptr render_frame(Scene* scene) { static std::uint64_t sequence{1}; auto frame = std::make_unique(Frame_Identity{sequence++, 0}); scene->set_frame_callback([](Frame_2D*) {}); - EXPECT_EQ(scene->render(frame.get()), Render_Scene_2D::Render_Result::completed); + EXPECT_TRUE(scene->render(frame.get()).has_value()); return frame; } } @@ -182,3 +196,45 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) { EXPECT_EQ(resized_frame.height, resized_canvas.height); EXPECT_TRUE(contains_color(resized_frame)); } + +TEST(spectrum_data, repeatedly_publishes_new_samples_during_long_running_render) { + using Frequency = Impl; + using Power = Impl; + using Spectrum_Object = Impl; + using Scene_Object = Impl; + initialize_runtime(4); + auto frequency = build_object(); + auto power = build_object(); + auto spectrum = build_object(frequency.get(), power.get()); + auto scene = build_scene(spectrum.get()); + frequency->set<&Abs_Axis::Prop::position>(Point_F{20.0, 100.0}); + frequency->set<&Abs_Axis::Prop::pixel_length>(120.0); + frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0}); + power->set<&Abs_Axis::Prop::position>(Point_F{20.0, 100.0}); + power->set<&Abs_Axis::Prop::pixel_length>(-80.0); + power->set<&Abs_Axis::Prop::orientation>(Axis_Orientation::vertical); + power->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-100.0, 0.0}); + spectrum->set<&Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0}); + scene->set<&Render_Scene_2D::Prop::viewport>(Size{160, 120}); + scene->set<&Render_Scene_2D::Prop::background>(Color::transparent()); + scene->activate_view(); + + std::uint64_t previous_hash{}; + std::size_t changed_frames{}; + for (std::size_t frame_index = 0; frame_index < 720; ++frame_index) { + std::vector samples(128, -95.0); + const std::size_t peak = 8 + frame_index % 112; + samples[peak] = -15.0; + spectrum->pending_buffer() = + Spectrum_Frame{std::move(samples)}; + spectrum->mark_dirty(); + auto frame = render_frame(scene.get()); + const auto hash = image_hash(frame->image()); + if (frame_index != 0 && hash != previous_hash) ++changed_frames; + previous_hash = hash; + EXPECT_TRUE(spectrum->read_state().prepare_executed); + EXPECT_TRUE(spectrum->read_state().paint_executed); + EXPECT_EQ(spectrum->read_state().sample_count, 128u); + } + EXPECT_GT(changed_frames, 700u); +} diff --git a/render_3D/render_3D/scene/Render_Scene_3D.cpp b/render_3D/render_3D/scene/Render_Scene_3D.cpp index ba52075..84568af 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.cpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.cpp @@ -13,6 +13,9 @@ void Render_Scene_3D::set_frame_callback(Frame_Callback callback) { static_cast< tf::Taskflow& Render_Scene_3D::completion_taskflow() { return static_cast(*d).completion_graph; } +void Render_Scene_3D::reset_frame_statistics() { + static_cast(*d).dispatch->reset_statistics(this); +} void Render_Scene_3D::activate_view() { static_cast(*d).dispatch->set_active(this, true); } void Render_Scene_3D::deactivate_view() { static_cast(*d).dispatch->set_active(this, false); } } diff --git a/render_3D/render_3D/scene/Render_Scene_3D.hpp b/render_3D/render_3D/scene/Render_Scene_3D.hpp index 98e3bc0..81144aa 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.hpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.hpp @@ -5,6 +5,7 @@ #include "../detail/Backend_Types.hpp" #include "../visual/Visuals.hpp" #include +#include #include #include #include @@ -22,6 +23,7 @@ struct Render_Scene_3D : Def(gpu_index, validation_enabled, std::move(visuals), initial); - backend->set_frame_callback([this](Frame_3D* frame) { + backend->set_frame_callback([this, object](Frame_3D* frame) { Frame_Callback callback; { std::lock_guard lock(render_mutex); callback = frame_callback; } - auto finish = [this, frame, callback = std::move(callback)]() mutable { + auto finish = [this, object, frame, callback = std::move(callback)]() mutable { frame->mark(Frame_Trace_Marker::callback_started); if (callback) callback(frame); frame->mark(Frame_Trace_Marker::callback_finished); + frame->mark(Frame_Trace_Marker::frame_ready); + object->template publish_state( + [this, frame](State_Access states) { + states.template get() + .frame_statistics = frame_statistics.submit( + *frame, Frame_Dimension::three_dimensional); + }); { std::lock_guard lock(render_mutex); frame_in_flight = false; @@ -338,6 +348,6 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Object* object, if (prop.viewport.empty()) return Render_Result::empty_viewport; return Render_Result::backend_unavailable; } -template const Render_Scene_3D::Private::Dispatch& Render_Scene_3D::Private::dispatch_for() { static const Dispatch value{[](Root* root, Frame_3D* frame) { auto* object = static_cast(root); return static_cast(*object->d).render(object, frame); }, [](Root* root, Frame_Callback callback) { auto* object = static_cast(root); auto& data = static_cast(*object->d); std::lock_guard lock(data.render_mutex); data.frame_callback = std::move(callback); }, [](Root* root, bool active) { static_cast(root)->template set<&Prop::view_active>(active); }}; return value; } +template const Render_Scene_3D::Private::Dispatch& Render_Scene_3D::Private::dispatch_for() { static const Dispatch value{[](Root* root) { auto* object = static_cast(root); auto& data = static_cast(*object->d); object->template publish_state([&data](State_Access states) { data.frame_statistics.reset(); states.template get().frame_statistics = {}; }); }, [](Root* root, Frame_3D* frame) { auto* object = static_cast(root); return static_cast(*object->d).render(object, frame); }, [](Root* root, Frame_Callback callback) { auto* object = static_cast(root); auto& data = static_cast(*object->d); std::lock_guard lock(data.render_mutex); data.frame_callback = std::move(callback); }, [](Root* root, bool active) { static_cast(root)->template set<&Prop::view_active>(active); }}; return value; } template void Render_Scene_3D::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } } diff --git a/web_server/src/Gallery_Plots_2D.cpp b/web_server/src/Gallery_Plots_2D.cpp index 5820e81..91cee0e 100644 --- a/web_server/src/Gallery_Plots_2D.cpp +++ b/web_server/src/Gallery_Plots_2D.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,7 @@ public: struct Data_Generator { nlohmann::json schema; std::function generate; + std::function advance; explicit operator bool() const noexcept { return static_cast(generate); } }; Scene_View_Model(std::vector> value_descriptors, @@ -57,17 +59,22 @@ public: nlohmann::json generate_data(const nlohmann::json& input) override { if (!data_generator) return {{"success", false}, {"error", "this plot has no raw data input"}}; auto result = data_generator.generate(input); - if (result.value("success", false)) generated_data_active = true; + if (result.value("success", false)) + generated_data.store(std::make_shared(input), + std::memory_order_release); return result; } void update(const Plot_Render_Tick& request) override { - update_scene(request, !generated_data_active); + const auto input = generated_data.load(std::memory_order_acquire); + if (input && data_generator.advance) + data_generator.advance(*input, request); + update_scene(request, !input); } private: std::vector> descriptors; std::function update_scene; Data_Generator data_generator; - bool generated_data_active{}; /* true 后保留用户生成的数据,不再用演示输入覆盖;viewport 更新仍持续。 */ + std::atomic> generated_data{}; /* 成功生成后发布不可变参数;渲染任务按同一配置持续产生压力数据。 */ std::tuple objects; }; template @@ -267,8 +274,13 @@ Json generator_2d_schema() { fields.push_back(generator_number_field("y_min", "Y 坐标下界", "矩形起点 Y 随机范围下界。", 0.0, -1'000'000.0, 1'000'000.0)); fields.push_back(generator_number_field("y_max", "Y 坐标上界", "矩形终点 Y 随机范围上界。", 100.0, -1'000'000.0, 1'000'000.0)); } - if (!fields.empty()) + if (!fields.empty()) { fields.push_back(generator_integer_field("seed", "随机种子", "固定种子可重现同一压力数据集,便于对比不同帧策略和像素传输模式。", 42, 4'294'967'295ULL)); + fields.push_back(generator_integer_field( + "update_every_n_frames", "更新帧间隔", + "每隔多少个渲染帧重新生成一次本图压力数据;1 表示每帧更新。", + 1, 100'000)); + } return {{"label", std::move(label)}, {"description", std::move(description) + " 可通过数据规模与坐标/数值范围构造可重复的压力负载。"}, {"fields", std::move(fields)}}; } template @@ -276,12 +288,13 @@ nlohmann::json generate_2d_data(Object& object, const Json& input) { using Definition = typename Object::Attached_Object; try { std::mt19937_64 engine{generator_count(input, "seed", 4'294'967'295ULL)}; + const auto animation_row = input.value("_animation_row", std::size_t{}); std::size_t generated_count{}; if constexpr (std::same_as) { const auto count = generator_count(input, "sample_count"); const auto [minimum, maximum] = generator_range(input, "power_min", "power_max"); std::vector values(count); - generate_spectral_row(values, 0, generator_count(input, "signal_count", 256), minimum, maximum, generator_number(input, "noise_stddev"), engine); + generate_spectral_row(values, animation_row, generator_count(input, "signal_count", 256), minimum, maximum, generator_number(input, "noise_stddev"), engine); object.template pending_buffer() = Spectrum_Frame{std::move(values)}; object.template mark_dirty(); generated_count = count; @@ -304,7 +317,7 @@ nlohmann::json generate_2d_data(Object& object, const Json& input) { const auto [minimum, maximum] = generator_range(input, "power_min", "power_max"); std::vector> blocks(block_count, std::vector(width)); std::vector complete(block_count * width); - generate_spectral_row(complete, 0, generator_count(input, "signal_count", 256), minimum, maximum, generator_number(input, "noise_stddev"), engine); + generate_spectral_row(complete, animation_row, generator_count(input, "signal_count", 256), minimum, maximum, generator_number(input, "noise_stddev"), engine); for (std::size_t block = 0; block < block_count; ++block) std::ranges::copy_n(complete.begin() + block * width, width, blocks[block].begin()); object.template set<&Sweep_Spectrum::Prop::bins_per_block>(width); @@ -324,7 +337,7 @@ nlohmann::json generate_2d_data(Object& object, const Json& input) { const auto signal_count = generator_count(input, "signal_count", 256); const auto noise_stddev = generator_number(input, "noise_stddev"); for (std::size_t row = 0; row < row_count; ++row) - generate_spectral_row(spectra[row], row, signal_count, minimum, maximum, noise_stddev, engine); + generate_spectral_row(spectra[row], animation_row + row, signal_count, minimum, maximum, noise_stddev, engine); for (auto& spectrum : spectra) object.template submit_stream( std::make_shared>(std::move(spectrum))); @@ -341,7 +354,7 @@ nlohmann::json generate_2d_data(Object& object, const Json& input) { const auto noise_stddev = generator_number(input, "noise_stddev"); for (std::size_t row = 0; row < row_count; ++row) { std::vector row_values(width); - generate_spectral_row(row_values, row, signal_count, minimum, maximum, noise_stddev, engine); + generate_spectral_row(row_values, animation_row + row, signal_count, minimum, maximum, noise_stddev, engine); rows.push_back({static_cast(row), std::move(row_values)}); } object.template set<&Waterfall::Prop::frequency_bin_count>(width); @@ -418,7 +431,27 @@ std::unique_ptr make_scene_view( std::move(components), std::move(update), typename Scene_View_Model...>::Data_Generator{ - generator_2d_schema(), [&object](const nlohmann::json& input) { return generate_2d_data(object, input); }}, + generator_2d_schema(), + [&object](const nlohmann::json& input) { + return generate_2d_data(object, input); + }, + [&object](const nlohmann::json& input, + const Plot_Render_Tick& tick) { + const auto interval = generator_count( + input, "update_every_n_frames", 100'000); + if (tick.sequence % interval != 0) return; + auto frame_input = input; + constexpr std::uint64_t maximum_seed{4'294'967'295ULL}; + const auto base_seed = generator_count( + input, "seed", maximum_seed); + frame_input["seed"] = + 1 + (base_seed - 1 + tick.sequence) % maximum_seed; + frame_input["_animation_row"] = tick.sequence; + const auto result = generate_2d_data(object, frame_input); + if (!result.value("success", false)) + throw std::runtime_error(result.value( + "error", "continuous 2D data generation failed")); + }}, std::forward(owned_objects)...); } std::unique_ptr make_frequency_axis() { @@ -542,7 +575,7 @@ std::shared_ptr make_axes_plot() { Json{{"label", "生成坐标轴压力数据"}, {"description", "按时间样本规模和三个业务坐标范围生成可重复的坐标轴压力负载。"}, {"fields", std::move(generator_fields)}}, - std::move(generate)}, + std::move(generate), {}}, std::move(frequency), std::move(numeric), std::move(time)); return std::make_shared(std::move(scene), std::move(view)); } diff --git a/web_server/src/Gallery_Video_Stream.cpp b/web_server/src/Gallery_Video_Stream.cpp index ea08c28..ed6fd84 100644 --- a/web_server/src/Gallery_Video_Stream.cpp +++ b/web_server/src/Gallery_Video_Stream.cpp @@ -1,5 +1,5 @@ #include "Gallery_Video_Stream.hpp" -#include "Sliding_Statistics.hpp" +#include #include "detail/Gallery_Frame_Atlas.hpp" #include "detail/Gallery_Frame_Clock.hpp" #include @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,15 @@ constexpr std::uint32_t atlas_columns{4}; constexpr double gallery_frame_rate{100.0}; constexpr auto metric_interval{std::chrono::seconds(1)}; +nlohmann::json statistic_json(const Statistic_State& value) { + return {{"count", value.count}, {"latest", value.latest}, + {"minimum", value.minimum}, {"maximum", value.maximum}, + {"average", value.average}, + {"trimmed_average", value.trimmed_average}, + {"variability", value.variability}, {"p50", value.p50}, + {"p95", value.p95}, {"p99", value.p99}}; +} + std::string exception_description(const std::exception_ptr& failure) { try { if (failure) std::rethrow_exception(failure); @@ -43,6 +53,13 @@ struct Gallery_Video_Stream::Private { Plot_Entry entry; /* 布局槽位关联的实际 Plot。 */ Plot::Stream_Id stream{}; /* Plot 完成帧的唯一订阅标识。 */ }; + struct State { + Sliding_Statistics compose_ms{600}; + Sliding_Statistics encode_ms{600}; + Sliding_Statistics publish_ms{600}; + mutable std::shared_mutex diagnostics_exchange_mutex; + double_buffer::Double_Buffer diagnostics{}; + } state{}; std::vector sources{}; /* 已按业务标识排序的稳定图集来源。 */ std::unique_ptr atlas{}; /* 最近完成帧与 RGBA 图集的唯一状态源。 */ @@ -72,11 +89,6 @@ struct Gallery_Video_Stream::Private { std::uint64_t metric_clock_start{}; /* 指标窗口起点的累计已分发 tick 数。 */ std::vector metric_completion_starts{}; /* 指标窗口起点各 Plot 逻辑完成回调数。 */ std::vector metric_rendered_starts{}; /* 指标窗口起点各 Plot 真实画面数。 */ - Sliding_Statistics metric_compose_samples{600}; /* 图集快照合成耗时滑动窗口,单位毫秒。 */ - Sliding_Statistics metric_encode_samples{600}; /* 硬件编码耗时滑动窗口,单位毫秒。 */ - Sliding_Statistics metric_publish_samples{600}; /* WebRTC 发布调用耗时滑动窗口,单位毫秒。 */ - mutable std::mutex diagnostics_mutex; - nlohmann::json latest_diagnostics{}; /* 最近一次低频聚合结果;HTTP 请求只复制该快照。 */ explicit Private(std::vector plots) { std::ranges::sort(plots, {}, &Plot_Entry::id); @@ -245,9 +257,9 @@ struct Gallery_Video_Stream::Private { metric_completion_starts[slot] = progress.completion_count; metric_rendered_starts[slot] = progress.rendered_frame_count; } - const auto compose = metric_compose_samples.snapshot(); - const auto encode = metric_encode_samples.snapshot(); - const auto publish_time = metric_publish_samples.snapshot(); + const auto& compose = state.compose_ms.state(); + const auto& encode = state.encode_ms.state(); + const auto& publish_time = state.publish_ms.state(); auto output = nlohmann::json{ {"kind", "gallery_metrics"}, {"protocol", "aethera.gallery.video"}, @@ -267,8 +279,9 @@ struct Gallery_Video_Stream::Private { {"encode_p95_ms", encode.p95}, {"publish_average_ms", publish_time.average}, {"publish_p95_ms", publish_time.p95}, - {"statistics", {{"compose_ms", compose}, {"encode_ms", encode}, - {"publish_ms", publish_time}}}, + {"statistics", {{"compose_ms", statistic_json(compose)}, + {"encode_ms", statistic_json(encode)}, + {"publish_ms", statistic_json(publish_time)}}}, {"encoded_bytes", encoded_bytes}, {"fresh_tiles", composition.fresh_tile_count}, {"missing_tiles", composition.missing_tile_count}, @@ -280,10 +293,9 @@ struct Gallery_Video_Stream::Private { metric_started = now; metric_encoded_start = encoded_frame_count; metric_clock_start = clock_total; - { - std::lock_guard lock(diagnostics_mutex); - latest_diagnostics = output; - } + *state.diagnostics.current = std::move(output); + std::unique_lock lock(state.diagnostics_exchange_mutex); + state.diagnostics.advance(); } void encode_latest(std::weak_ptr lifetime) { @@ -303,10 +315,10 @@ struct Gallery_Video_Stream::Private { try { const auto compose_started = std::chrono::steady_clock::now(); auto composition = atlas->compose(); - metric_compose_samples.submit( + static_cast(state.compose_ms.submit( std::chrono::duration( std::chrono::steady_clock::now() - compose_started) - .count()); + .count())); const auto started = std::chrono::steady_clock::now(); if (key_frame_requested.exchange(false, std::memory_order_acq_rel)) encoder.request_key_frame(); @@ -317,7 +329,7 @@ struct Gallery_Video_Stream::Private { std::llround(tick.time_milliseconds * 1'000.0))}); const double encode_time = std::chrono::duration( std::chrono::steady_clock::now() - started).count(); - metric_encode_samples.submit(encode_time); + static_cast(state.encode_ms.submit(encode_time)); if (video) { ++encoded_frame_count; const auto encoded_bytes = video->annex_b.size(); @@ -327,10 +339,10 @@ struct Gallery_Video_Stream::Private { const auto publish_started = std::chrono::steady_clock::now(); publish(std::move(*video), {}); - metric_publish_samples.submit( + static_cast(state.publish_ms.submit( std::chrono::duration( std::chrono::steady_clock::now() - publish_started) - .count()); + .count())); } } catch (...) { @@ -512,9 +524,9 @@ std::string Gallery_Video_Stream::layout_description() const { nlohmann::json Gallery_Video_Stream::diagnostics() const { nlohmann::json output; { - std::lock_guard lock(d->diagnostics_mutex); - output = !d->latest_diagnostics.is_null() - ? d->latest_diagnostics + std::shared_lock lock(d->state.diagnostics_exchange_mutex); + output = !d->state.diagnostics.pending->is_null() + ? *d->state.diagnostics.pending : nlohmann::json{{"kind", "gallery_metrics"}, {"protocol", "aethera.gallery.video"}, {"version", 2}, diff --git a/web_server/src/H264_Encoder.cpp b/web_server/src/H264_Encoder.cpp index 131b358..4e1ffde 100644 --- a/web_server/src/H264_Encoder.cpp +++ b/web_server/src/H264_Encoder.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,12 @@ extern "C" { namespace aethera::web { namespace { constexpr std::string_view high_profile_level_5_1{"640033"}; +/* + * Gallery 帧属于桌面/UI 内容:细线和小字比自然视频更怕量化块。 + * 固定质量避免 100 FPS CBR 在复杂图集帧上临时抬高 QP;18 在浏览器 + * H.264 4:2:0 兼容约束内保留足够的文字边缘,同时仍保持硬件编码。 + */ +constexpr std::int64_t gallery_constant_qp{18}; std::runtime_error ffmpeg_failure(std::string operation, int code) { std::array description{}; @@ -119,6 +126,17 @@ struct H264_Encoder::Private { Video_Encoder_Backend backend{Video_Encoder_Backend::nvenc}; }; + struct Nvenc_Probe_State { + std::once_flag once{}; + bool available{}; + std::string failure{}; + }; + + static Nvenc_Probe_State& nvenc_probe_state() { + static Nvenc_Probe_State state; + return state; + } + double frame_rate{}; /* 页面媒体时钟频率,也是 GOP 的计算基准。 */ AVCodecContext* codec_context{}; /* 当前图集尺寸对应的唯一硬件编码上下文。 */ AVFrame* software_frame{}; /* 当前硬件路径需要的复用 CPU 输入帧。 */ @@ -213,7 +231,7 @@ struct H264_Encoder::Private { configure_common(*result.codec_context, next_width, next_height, input_pixel_format(next_layout)); require_ffmpeg(av_opt_set(result.codec_context->priv_data, - "preset", "p1", 0), + "preset", "p4", 0), "setting NVENC preset"); require_ffmpeg(av_opt_set(result.codec_context->priv_data, "tune", "ull", 0), @@ -225,8 +243,11 @@ struct H264_Encoder::Private { "level", "5.1", 0), "setting NVENC H.264 level"); require_ffmpeg(av_opt_set(result.codec_context->priv_data, - "rc", "cbr", 0), + "rc", "constqp", 0), "setting NVENC rate control"); + require_ffmpeg(av_opt_set_int(result.codec_context->priv_data, + "qp", gallery_constant_qp, 0), + "setting NVENC constant quantizer"); require_ffmpeg(av_opt_set_int(result.codec_context->priv_data, "delay", 0, 0), "disabling NVENC output delay"); @@ -296,14 +317,17 @@ struct H264_Encoder::Private { "usage", "stream", 0), "setting Vulkan Video streaming usage"); require_ffmpeg(av_opt_set(result.codec_context->priv_data, - "content", "rendered", 0), - "setting Vulkan Video rendered content"); + "content", "desktop", 0), + "setting Vulkan Video desktop content"); require_ffmpeg(av_opt_set(result.codec_context->priv_data, "tune", "ull", 0), "setting Vulkan Video ultra-low latency"); require_ffmpeg(av_opt_set(result.codec_context->priv_data, - "rc_mode", "cbr", 0), + "rc_mode", "cqp", 0), "setting Vulkan Video rate control"); + require_ffmpeg(av_opt_set_int(result.codec_context->priv_data, + "qp", gallery_constant_qp, 0), + "setting Vulkan Video constant quantizer"); require_ffmpeg(av_opt_set_int(result.codec_context->priv_data, "async_depth", 3, 0), "setting Vulkan Video pipeline depth"); @@ -354,14 +378,37 @@ struct H264_Encoder::Private { width == next_width && height == next_height && layout == next_layout) return; + auto& probe = nvenc_probe_state(); + Configuration probed_configuration; + bool owns_probed_configuration{}; + std::call_once(probe.once, [&] { + try { + probed_configuration = configure_nvenc( + next_width, next_height, next_layout); + probe.available = true; + owns_probed_configuration = true; + } + catch (...) { + probe.failure = failure_description(std::current_exception()); + } + }); + std::exception_ptr nvenc_failure; - try { - adopt(configure_nvenc(next_width, next_height, next_layout), - next_width, next_height, next_layout); - return; - } - catch (...) { - nvenc_failure = std::current_exception(); + if (probe.available) { + try { + adopt(owns_probed_configuration + ? std::move(probed_configuration) + : configure_nvenc(next_width, next_height, next_layout), + next_width, next_height, next_layout); + return; + } + catch (...) { + nvenc_failure = std::current_exception(); + } + } else { + nvenc_failure = std::make_exception_ptr(std::runtime_error( + probe.failure.empty() ? "NVENC capability probe failed" + : probe.failure)); } try { adopt(configure_vulkan(next_width, next_height, next_layout), diff --git a/web_server/src/Plot.cpp b/web_server/src/Plot.cpp index a25a69c..9b71e9e 100644 --- a/web_server/src/Plot.cpp +++ b/web_server/src/Plot.cpp @@ -1,7 +1,7 @@ #include "Plot.hpp" #include "Renderable_Adapter.hpp" -#include "Sliding_Statistics.hpp" #include +#include #include #include #include @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -33,43 +34,11 @@ using Scene_3D = Impl; constexpr std::uint16_t plot_stream_protocol_version{9}; constexpr std::size_t diagnostic_window_capacity{600}; -class Statistic_Input_Batch final { -public: - Statistic_Input_Batch( - std::initializer_list> initial) { - for (const auto& value : initial) emplace_back(value.first, value.second); - } - void emplace_back(std::string_view key, double value) { - if (size == values.size()) - throw std::logic_error("frame statistic input capacity exceeded"); - values[size++] = {key, value}; - } - [[nodiscard]] std::span> view() const { - return {values.data(), size}; - } -private: - std::array, 48> values{}; /* 单帧全部瞬时统计的栈内存储。 */ - std::size_t size{}; /* 当前已写入的有效字段数量。 */ -}; - -double elapsed_milliseconds(std::chrono::steady_clock::time_point start, - std::chrono::steady_clock::time_point finish) { - if (start == std::chrono::steady_clock::time_point{} || finish < start) - return 0.0; - return std::chrono::duration(finish - start).count(); -} struct Web_Input_Metadata { Plot_Input_Event input; 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; @@ -128,29 +97,19 @@ private: }; std::string_view pacing_mode_name(Frame_Pacing_Mode mode) { - switch (mode) { - case Frame_Pacing_Mode::manual: return "manual"; - case Frame_Pacing_Mode::fixed_rate: return "fixed_rate"; - case Frame_Pacing_Mode::maximum_rate: return "maximum_rate"; - } - throw std::logic_error("unknown frame pacing mode"); + const auto name = magic_enum::enum_name(mode); + if (name.empty()) throw std::logic_error("unknown frame pacing mode"); + return name; } std::optional parse_pacing_mode(std::string_view value) { - if (value == "manual") return Frame_Pacing_Mode::manual; - if (value == "fixed_rate") return Frame_Pacing_Mode::fixed_rate; - if (value == "maximum_rate") return Frame_Pacing_Mode::maximum_rate; - return std::nullopt; + return magic_enum::enum_cast(value); } std::string_view pixel_format_name(render_2d::Pixel_Format format) { - switch (format) { - case render_2d::Pixel_Format::bgra8_premultiplied: - return "bgra8_premultiplied"; - case render_2d::Pixel_Format::rgba8: - return "rgba8"; - } - throw std::logic_error("unknown 2D pixel format"); + const auto name = magic_enum::enum_name(format); + if (name.empty()) throw std::logic_error("unknown 2D pixel format"); + return name; } std::string_view pixel_format_name(render_3d::Pixel_Format format) { @@ -226,288 +185,23 @@ nlohmann::json Frame_Policy::write_prop(std::string_view key, return {{"success", false}, {"error", "unknown frame runtime property"}}; } -class Plot_Diagnostics final { -public: - void submit(Render_Frame& frame, Frame_Identity rendered_identity, - std::uint32_t width, std::uint32_t height, - std::size_t pixel_bytes, std::string output_format, - std::string native_format, - std::vector supported_formats, - const Frame_Pacing_Properties& pacing, bool is_3d); - void submit_input(const Plot_Input_Observation& observation); - void reset(); - [[nodiscard]] nlohmann::json snapshot() const; - -private: - Sliding_Statistics_Set frame_values{diagnostic_window_capacity}; - Sliding_Statistics_Set input_values{diagnostic_window_capacity}; - mutable std::mutex state_mutex; - std::chrono::steady_clock::time_point previous_completion{}; - std::uint64_t previous_sequence{}; - std::uint64_t dropped_sequences{}; - std::uint64_t sequence{}; - std::uint64_t correlation_id{}; - std::uint64_t rendered_sequence{}; - std::uint64_t rendered_correlation_id{}; - std::uint64_t created_time_unix_ns{}; - std::uint32_t width{}; - std::uint32_t height{}; - std::size_t pixel_bytes{}; - std::string output_format{}; - std::string native_format{}; - std::vector supported_formats{}; - Frame_Pacing_Properties pacing{}; - bool is_3d{}; -}; - -std::string_view measurement_key(Frame_Trace_Measurement measurement) { - switch (measurement) { - case Frame_Trace_Measurement::backend_apply_ns: return "backend_apply_ms"; - case Frame_Trace_Measurement::backend_plan_ns: return "backend_plan_ms"; - case Frame_Trace_Measurement::backend_execute_ns: return "backend_execute_ms"; - case Frame_Trace_Measurement::backend_submit_ns: return "backend_submit_ms"; - case Frame_Trace_Measurement::gpu_fence_wait_ns: return "gpu_fence_wait_ms"; - case Frame_Trace_Measurement::gpu_render_ns: return "gpu_render_ms"; - case Frame_Trace_Measurement::gpu_transition_ns: return "gpu_transition_ms"; - case Frame_Trace_Measurement::gpu_copy_ns: return "gpu_copy_ms"; - case Frame_Trace_Measurement::gpu_total_ns: return "gpu_total_ms"; - case Frame_Trace_Measurement::readback_ns: return "readback_ms"; - case Frame_Trace_Measurement::count: break; +void append_statistic_json(nlohmann::json& output, + const Frame_Statistics_State& state) { + for (const auto statistic : magic_enum::enum_values()) { + if (statistic == Frame_Statistic::count) continue; + const auto& value = + state.values[static_cast(statistic)]; + if (value.count == 0) continue; + output[magic_enum::enum_name(statistic)] = { + {"count", value.count}, {"latest", value.latest}, + {"minimum", value.minimum}, {"maximum", value.maximum}, + {"average", value.average}, + {"trimmed_average", value.trimmed_average}, + {"variability", value.variability}, {"p50", value.p50}, + {"p95", value.p95}, {"p99", value.p99}}; } - throw std::logic_error("unknown frame trace measurement"); } -void Plot_Diagnostics::submit( - Render_Frame& frame, Frame_Identity rendered_identity, - std::uint32_t value_width, std::uint32_t value_height, - std::size_t value_pixel_bytes, std::string value_output_format, - std::string value_native_format, - std::vector value_supported_formats, - const Frame_Pacing_Properties& value_pacing, bool value_is_3d) { - constexpr auto marker_count = static_cast(Frame_Trace_Marker::count); - constexpr auto measurement_count = static_cast(Frame_Trace_Measurement::count); - std::array, marker_count> markers{}; - for (const auto& point : frame.trace_points()) - markers[static_cast(point.marker)] = - static_cast(point.elapsed_ns) / 1'000'000.0; - const auto marker = [&](Frame_Trace_Marker value) { - return markers[static_cast(value)].value_or(0.0); - }; - const auto interval = [&](Frame_Trace_Marker first, Frame_Trace_Marker last) { - const auto start = markers[static_cast(first)]; - const auto finish = markers[static_cast(last)]; - return start && finish ? std::max(0.0, *finish - *start) : 0.0; - }; - std::array measurements{}; - const auto trace_measurements = frame.trace_values(); - for (const auto& value : trace_measurements) - measurements[static_cast(value.measurement)] = - static_cast(value.value_ns) / 1'000'000.0; - const auto measurement = [&](Frame_Trace_Measurement value) { - return measurements[static_cast(value)]; - }; - Statistic_Input_Batch values{ - {"server_completion_ms", marker(Frame_Trace_Marker::frame_ready)}, - {"payload_megabytes", static_cast(value_pixel_bytes) / (1024.0 * 1024.0)}, - {"scene_render_ms", interval(Frame_Trace_Marker::scene_render_started, Frame_Trace_Marker::scene_render_finished)}, - {"event_dispatch_ms", interval(Frame_Trace_Marker::event_dispatch_started, Frame_Trace_Marker::event_dispatch_finished)}, - {"prepare_ms", interval(Frame_Trace_Marker::prepare_started, Frame_Trace_Marker::prepare_finished)}, - {"paint_ms", interval(Frame_Trace_Marker::paint_started, Frame_Trace_Marker::paint_finished)}, - {"backend_queue_ms", interval(Frame_Trace_Marker::backend_queue_entered, Frame_Trace_Marker::backend_queue_left)}, - {"gpu_submission_ms", interval(Frame_Trace_Marker::gpu_submitted, Frame_Trace_Marker::gpu_completed)}, - {"readback_stage_ms", interval(Frame_Trace_Marker::readback_started, Frame_Trace_Marker::readback_finished)}, - {"callback_ms", interval(Frame_Trace_Marker::callback_started, Frame_Trace_Marker::frame_ready)}}; - for (const auto& value : trace_measurements) - values.emplace_back(measurement_key(value.measurement), - static_cast(value.value_ns) / 1'000'000.0); - - double remaining = marker(Frame_Trace_Marker::frame_ready); - auto take = [&](double requested) { - const auto result = std::min(remaining, std::max(0.0, requested)); - remaining -= result; - return result; - }; - const double scene_time = interval(Frame_Trace_Marker::scene_render_started, - Frame_Trace_Marker::scene_render_finished); - const double event_time = std::min(scene_time, interval( - Frame_Trace_Marker::event_dispatch_started, - Frame_Trace_Marker::event_dispatch_finished)); - const double prepare_time = std::min(std::max(0.0, scene_time - event_time), - interval(Frame_Trace_Marker::prepare_started, Frame_Trace_Marker::prepare_finished)); - const double paint_time = std::min(std::max(0.0, scene_time - event_time - prepare_time), - interval(Frame_Trace_Marker::paint_started, Frame_Trace_Marker::paint_finished)); - values.emplace_back(value_is_3d ? "pipeline_3d_event_ms" : "pipeline_2d_event_ms", take(event_time)); - values.emplace_back(value_is_3d ? "pipeline_3d_prepare_ms" : "pipeline_2d_prepare_ms", take(prepare_time)); - values.emplace_back(value_is_3d ? "pipeline_3d_submit_graph_ms" : "pipeline_2d_paint_ms", take(paint_time)); - values.emplace_back(value_is_3d ? "pipeline_3d_scene_coordination_ms" : "pipeline_2d_scene_coordination_ms", - take(std::max(0.0, scene_time - event_time - prepare_time - paint_time))); - if (value_is_3d) { - const double scene_finished = marker(Frame_Trace_Marker::scene_render_finished); - const double queue_entered = marker(Frame_Trace_Marker::backend_queue_entered); - const double backend_prepare_started = marker(Frame_Trace_Marker::backend_prepare_started); - const double backend_prepare_finished = marker(Frame_Trace_Marker::backend_prepare_finished); - const double submit_queued = marker(Frame_Trace_Marker::backend_submit_queued); - const double queue_left = marker(Frame_Trace_Marker::backend_queue_left); - values.emplace_back("pipeline_3d_prepare_queue_ms", take(std::max( - 0.0, backend_prepare_started - std::max(scene_finished, queue_entered)))); - double preparation_window = std::max(0.0, backend_prepare_finished - backend_prepare_started); - const auto take_preparation = [&](Frame_Trace_Measurement key) { - const double value = std::min(preparation_window, std::max(0.0, measurement(key))); - preparation_window -= value; - return take(value); - }; - values.emplace_back("pipeline_3d_backend_apply_ms", take_preparation(Frame_Trace_Measurement::backend_apply_ns)); - values.emplace_back("pipeline_3d_backend_plan_ms", take_preparation(Frame_Trace_Measurement::backend_plan_ns)); - values.emplace_back("pipeline_3d_backend_execute_ms", take_preparation(Frame_Trace_Measurement::backend_execute_ns)); - values.emplace_back("pipeline_3d_backend_commands_ms", take(preparation_window)); - values.emplace_back("pipeline_3d_backend_queue_ms", take(std::max(0.0, queue_left - submit_queued))); - const double gpu_submitted = marker(Frame_Trace_Marker::gpu_submitted); - double submit_window = std::max(0.0, gpu_submitted - queue_left); - const double measured_submit = std::min(submit_window, std::max( - 0.0, measurement(Frame_Trace_Measurement::backend_submit_ns))); - values.emplace_back("pipeline_3d_backend_submit_ms", take(measured_submit)); - submit_window -= measured_submit; - values.emplace_back("pipeline_3d_submit_handoff_ms", take(submit_window)); - double gpu_window = interval(Frame_Trace_Marker::gpu_submitted, - Frame_Trace_Marker::gpu_completed); - const auto take_gpu = [&](Frame_Trace_Measurement key) { - const double value = std::min(gpu_window, std::max(0.0, measurement(key))); - gpu_window -= value; - return take(value); - }; - values.emplace_back("pipeline_3d_gpu_render_ms", take_gpu(Frame_Trace_Measurement::gpu_render_ns)); - values.emplace_back("pipeline_3d_gpu_transition_ms", take_gpu(Frame_Trace_Measurement::gpu_transition_ns)); - values.emplace_back("pipeline_3d_gpu_copy_ms", take_gpu(Frame_Trace_Measurement::gpu_copy_ns)); - values.emplace_back("pipeline_3d_gpu_sync_ms", take(gpu_window)); - values.emplace_back("pipeline_3d_readback_ms", take(interval( - Frame_Trace_Marker::readback_started, Frame_Trace_Marker::readback_finished))); - values.emplace_back("pipeline_3d_callback_ms", take(interval( - Frame_Trace_Marker::callback_started, Frame_Trace_Marker::frame_ready))); - values.emplace_back("pipeline_3d_completion_handoff_ms", remaining); - } else { - values.emplace_back("pipeline_2d_callback_ms", take(interval( - Frame_Trace_Marker::callback_started, Frame_Trace_Marker::frame_ready))); - values.emplace_back("pipeline_2d_frame_handoff_ms", remaining); - } - const auto now = std::chrono::steady_clock::now(); - const auto identity = frame.identity(); - { - std::lock_guard lock(state_mutex); - if (previous_completion != std::chrono::steady_clock::time_point{}) { - values.emplace_back("frame_interval_ms", elapsed_milliseconds(previous_completion, now)); - if (identity.sequence > previous_sequence + 1U) - dropped_sequences += identity.sequence - previous_sequence - 1U; - } - previous_completion = now; - previous_sequence = identity.sequence; - sequence = identity.sequence; - correlation_id = identity.correlation_id; - this->rendered_sequence = rendered_identity.sequence; - rendered_correlation_id = rendered_identity.correlation_id; - created_time_unix_ns = frame.created_time_unix_ns(); - width = value_width; - height = value_height; - pixel_bytes = value_pixel_bytes; - output_format = std::move(value_output_format); - native_format = std::move(value_native_format); - supported_formats = std::move(value_supported_formats); - pacing = value_pacing; - is_3d = value_is_3d; - } - frame_values.submit(values.view()); -} - -void Plot_Diagnostics::submit_input(const Plot_Input_Observation& observation) { - const std::array, 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(observation.coalesced_event_count)}}}; - input_values.submit(values); -} - -void Plot_Diagnostics::reset() { - frame_values.reset(); - input_values.reset(); - std::lock_guard lock(state_mutex); - previous_completion = {}; - previous_sequence = 0; - dropped_sequences = 0; -} - -nlohmann::json Plot_Diagnostics::snapshot() const { - nlohmann::json frame_statistics = nlohmann::json::object(); - for (const auto& value : frame_values.snapshot()) - frame_statistics[value.key] = value.statistics; - nlohmann::json input_statistics = nlohmann::json::object(); - for (const auto& value : input_values.snapshot()) - input_statistics[value.key] = value.statistics; - std::lock_guard lock(state_mutex); - const auto interval = frame_statistics.find("frame_interval_ms"); - const double frame_rate = interval != frame_statistics.end() && - interval->value("trimmed_average", 0.0) > 0.0 - ? 1'000.0 / interval->value("trimmed_average", 0.0) : 0.0; - nlohmann::json output{ - {"protocol", "aethera.plot.diagnostics"}, {"version", 1}, - {"dimension", is_3d ? "3D" : "2D"}, - {"sequence", sequence}, {"correlation_id", correlation_id}, - {"rendered_sequence", rendered_sequence}, - {"rendered_correlation_id", rendered_correlation_id}, - {"generated_time_unix_ms", static_cast(created_time_unix_ns) / 1'000'000.0}, - {"delivery", pixel_bytes == 0 ? "diagnostics" : "gallery-video"}, - {"frame_rate_fps", frame_rate}, {"dropped_sequence_count", dropped_sequences}, - {"window_capacity", diagnostic_window_capacity}, - {"pixel", {{"width", width}, {"height", height}, - {"format", output_format}, {"native_format", native_format}, - {"supported_formats", supported_formats}, {"byte_length", pixel_bytes}}}, - {"pacing", {{"mode", pacing_mode_name(pacing.mode)}, - {"fixed_rate_fps", pacing.fixed_rate_fps}, - {"render_enabled", pacing.render_enabled}, - {"video_enabled", pacing.video_enabled}}}, - {"frame_statistics", std::move(frame_statistics)}, - {"input_statistics", std::move(input_statistics)}}; - if (is_3d) { - const auto gpu = gpu_completion_state(); - const auto milliseconds = [](std::uint64_t nanoseconds) { - return static_cast(nanoseconds) / 1'000'000.0; - }; - output["gpu_completion_domain"] = { - {"capacity", gpu.capacity}, - {"in_flight", gpu.in_flight}, - {"peak_in_flight", gpu.peak_in_flight}, - {"utilization_percent", gpu.capacity == 0 ? 0.0 : - 100.0 * static_cast(gpu.in_flight) / - static_cast(gpu.capacity)}, - {"watched", gpu.watched}, - {"peak_watched", gpu.peak_watched}, - {"active_fences", gpu.active_fences}, - {"pending_fences", gpu.pending_fences}, - {"reservation_count", gpu.reservation_count}, - {"completion_count", gpu.completion_count}, - {"cancellation_count", gpu.cancellation_count}, - {"fence_probe_count", gpu.fence_probe_count}, - {"fence_wait_count", gpu.fence_wait_count}, - {"fence_wait_timeout_count", gpu.fence_wait_timeout_count}, - {"fence_wait_total_ms", milliseconds(gpu.fence_wait_total_ns)}, - {"fence_wait_average_ms", gpu.fence_wait_count == 0 ? 0.0 : - milliseconds(gpu.fence_wait_total_ns) / - static_cast(gpu.fence_wait_count)}, - {"fence_wait_max_ms", milliseconds(gpu.fence_wait_max_ns)}, - {"callback_total_ms", milliseconds(gpu.callback_total_ns)}, - {"callback_average_ms", gpu.completion_count == 0 ? 0.0 : - milliseconds(gpu.callback_total_ns) / - static_cast(gpu.completion_count)}, - {"callback_max_ms", milliseconds(gpu.callback_max_ns)}, - {"callback_failure_count", gpu.callback_failure_count}, - {"backpressure_count", gpu.backpressure_count}, - {"backpressure_wait_ms", milliseconds(gpu.backpressure_wait_ns)}, - {"fault_count", gpu.fault_count}, - {"abandoned_count", gpu.abandoned_count}, - {"stopping", gpu.stopping}}; - } - return output; -} template void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) { @@ -598,7 +292,6 @@ struct Plot::Private { std::atomic> terminal_failure{}; /* 首次 Plot Unknown Failure 的唯一终止状态。 */ std::uint64_t next_frame_sequence{1}; Frame_Policy frame_policy{}; - Plot_Diagnostics diagnostics{}; mutable std::mutex frame_mutex; static constexpr std::size_t scene_frame_capacity{3}; std::array frame_slots{}; /* Scene 借用的稳定三缓冲物理帧。 */ @@ -626,11 +319,10 @@ struct Plot::Private { [[nodiscard]] nlohmann::json schema() const; [[nodiscard]] Stream_Snapshot stream_snapshot() const; void publish(std::shared_ptr frame) noexcept; - void consume_tick(); + void consume_tick(std::weak_ptr lifetime); void clock_tick(const Plot_Render_Tick& tick); void render_frame(Plot_Render_Tick tick); void queue_completed_frame(Render_Frame* frame); - void collect_consumed_input_statistics(); void fail(std::exception_ptr failure) noexcept; }; @@ -700,18 +392,27 @@ void Plot::Private::publish( catch (...) {} } -void Plot::Private::consume_tick() { - for (;;) { - std::optional tick; - { - std::lock_guard lock(tick_mutex); - tick = std::exchange(pending_tick, {}); - if (!tick) { - tick_task_scheduled = false; - return; - } - } - clock_tick(*tick); +void Plot::Private::consume_tick(std::weak_ptr lifetime) { + std::optional tick; + { + std::lock_guard lock(tick_mutex); + tick = std::exchange(pending_tick, {}); + } + if (tick) clock_tick(*tick); + + bool schedule_again{}; + { + std::lock_guard lock(tick_mutex); + schedule_again = pending_tick.has_value(); + if (!schedule_again) tick_task_scheduled = false; + } + if (schedule_again) { + aethera::schedule_task([lifetime] { + const auto plot = lifetime.lock(); + if (!plot) return; + try { plot->d->consume_tick(lifetime); } + catch (...) { plot->d->fail(std::current_exception()); } + }); } } @@ -787,9 +488,7 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { (*scene_2d)->set<&Render_Scene_2D::Prop::viewport>( Size{static_cast(tick.width), static_cast(tick.height)}); const auto result = (*scene_2d)->render(&output); - collect_consumed_input_statistics(); - if (result != Render_Scene_2D::Render_Result::completed) - rollback_unsubmitted(); + if (!result) rollback_unsubmitted(); return; } auto& output = *std::get>(managed->frame); @@ -799,7 +498,6 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { auto& scene_3d = std::get>(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) @@ -811,40 +509,6 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { } } -void Plot::Private::collect_consumed_input_statistics() { - aethera::Scene::Event_Report_Batch reports = std::visit( - [](auto& value) { - return value->template access_query_stream( - [&](std::span events) { - aethera::Scene::Event_Report_Batch result{value->memory_resource()}; - result.reserve(events.size()); - for (const auto& event : events) result.push_back(event); - return result; - }); - }, scene); - for (const auto& event : reports) { - const auto* web_event = dynamic_cast(event.get()); - if (!web_event) continue; - const auto& metadata = web_event->web_input_metadata(); - 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.admission_ms = elapsed_milliseconds( - metadata.input.received_time, metadata.scene_dispatched); - observation.scene_wait_ms = elapsed_milliseconds( - metadata.scene_dispatched, started); - 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); - } -} void Plot::Private::queue_completed_frame(Render_Frame* frame) { if (!frame) @@ -896,15 +560,11 @@ void Plot::Private::queue_completed_frame(Render_Frame* frame) { std::shared_ptr> pixel_storage; std::uint32_t width{}; std::uint32_t height{}; - std::string_view output_format{"rgba8"}; - std::string_view native_format{"rgba8"}; if (auto* frame_2d = std::get_if>(&managed->frame)) { const auto image = (*frame_2d)->image(); width = static_cast(image.width); height = static_cast(image.height); - output_format = pixel_format_name((*frame_2d)->output_format()); - native_format = pixel_format_name(Frame_2D::native_pixel_format); if (pacing.video_enabled) { auto output = (*frame_2d)->output_pixels(); pixel_storage = std::make_shared>( @@ -916,8 +576,6 @@ void Plot::Private::queue_completed_frame(Render_Frame* frame) { else { auto& frame_3d = std::get>(managed->frame); - output_format = pixel_format_name(frame_3d->output_format()); - native_format = pixel_format_name(Frame_3D::native_pixel_format); rendered_identity = frame_3d->rendered_identity(); const auto extent = frame_3d->extent(); width = extent.width; @@ -933,24 +591,6 @@ void Plot::Private::queue_completed_frame(Render_Frame* frame) { rendered_identity.sequence, rendered_identity.correlation_id, width, height}); - frame->mark(Frame_Trace_Marker::frame_ready); - std::vector supported_formats; - const bool is_3d = std::holds_alternative>( - managed->frame); - if (is_3d) { - supported_formats.reserve(Frame_3D::supported_pixel_formats.size()); - for (const auto format : Frame_3D::supported_pixel_formats) - supported_formats.emplace_back(pixel_format_name(format)); - } else { - supported_formats.reserve(Frame_2D::supported_pixel_formats.size()); - for (const auto format : Frame_2D::supported_pixel_formats) - supported_formats.emplace_back(pixel_format_name(format)); - } - diagnostics.submit( - *frame, rendered_identity, width, height, - pixels->rgba ? pixels->rgba->size() : 0U, - std::string{output_format}, std::string{native_format}, - std::move(supported_formats), pacing, is_3d); const auto published = std::make_shared( Plot_Stream_Frame{{}, std::move(pixels)}); publish(std::move(published)); @@ -1059,7 +699,7 @@ void Plot::schedule_render(Plot_Render_Tick tick) { const auto owner = weak.lock(); if (!owner) return; try { - owner->d->consume_tick(); + owner->d->consume_tick(weak); } catch (...) { owner->d->fail(std::current_exception()); @@ -1106,77 +746,129 @@ void Plot::submit_input(Plot_Input_Event event) { } } -void Plot::async_schema(Json_Handler handler) { - if (!handler) throw std::invalid_argument("Plot schema handler is empty"); +nlohmann::json Plot::schema() { ensure_started(); - auto self = shared_from_this(); - aethera::schedule_task([self, handler = std::move(handler)]() mutable { - nlohmann::json result; - try { - result = self->d->schema(); - } - catch (...) { - const auto failure = std::current_exception(); - self->d->fail(failure); - result = {{"success", false}, - {"error", exception_description(failure)}}; - } - try { handler(std::move(result)); } - catch (...) {} - }); + return d->schema(); } -void Plot::async_write_prop(std::string component, std::string key, - nlohmann::json value, Json_Handler handler) { - if (!handler) throw std::invalid_argument("Plot property handler is empty"); +nlohmann::json Plot::write_prop(std::string_view component, + std::string_view key, + const nlohmann::json& value) { ensure_started(); - auto self = shared_from_this(); - aethera::schedule_task( - [self, component = std::move(component), key = std::move(key), - value = std::move(value), handler = std::move(handler)]() mutable { - nlohmann::json result; - try { - result = component == "frame-analysis" - ? self->d->frame_policy.write_prop(key, value) - : self->d->view->write_prop(component, key, value); - } - catch (...) { - const auto failure = std::current_exception(); - self->d->fail(failure); - result = {{"success", false}, - {"error", exception_description(failure)}}; - } - try { handler(std::move(result)); } - catch (...) {} - }); + return component == "frame-analysis" + ? d->frame_policy.write_prop(key, value) + : d->view->write_prop(component, key, value); } -void Plot::async_generate_data(nlohmann::json input, Json_Handler handler) { - if (!handler) throw std::invalid_argument("Plot data handler is empty"); +nlohmann::json Plot::generate_data(const nlohmann::json& input) { ensure_started(); - auto self = shared_from_this(); - aethera::schedule_task( - [self, input = std::move(input), handler = std::move(handler)]() mutable { - nlohmann::json result; - try { - result = self->d->view->generate_data(input); - } - catch (...) { - const auto failure = std::current_exception(); - self->d->fail(failure); - result = {{"success", false}, - {"error", exception_description(failure)}}; - } - try { handler(std::move(result)); } - catch (...) {} - }); + return d->view->generate_data(input); } nlohmann::json Plot::diagnostics() const { - return d->diagnostics.snapshot(); + nlohmann::json frame_statistics = nlohmann::json::object(); + Frame_Identity identity{}; + std::uint64_t created_time_unix_ns{}; + std::uint64_t dropped_sequences{}; + double frame_rate{}; + bool is_3d{}; + const auto read_statistics = [&](const auto& state) { + const auto& statistics = state.frame_statistics; + append_statistic_json(frame_statistics, statistics); + identity = statistics.identity; + created_time_unix_ns = statistics.created_time_unix_ns; + dropped_sequences = statistics.dropped_sequences; + const auto& interval = statistics.values[ + static_cast(Frame_Statistic::frame_interval_ms)]; + frame_rate = interval.trimmed_average > 0.0 + ? 1'000.0 / interval.trimmed_average : 0.0; + }; + std::visit([&](const auto& scene) { + using Scene_Pointer = std::remove_cvref_t; + if constexpr (std::same_as>) { + scene->template access_state( + read_statistics); + } else { + is_3d = true; + scene->template access_state( + read_statistics); + } + }, d->scene); + + const auto pacing = d->frame_policy.snapshot(); + const auto stream = d->stream_snapshot(); + nlohmann::json supported_formats = nlohmann::json::array(); + if (is_3d) { + for (const auto format : Frame_3D::supported_pixel_formats) + supported_formats.push_back(pixel_format_name(format)); + } else { + for (const auto format : Frame_2D::supported_pixel_formats) + supported_formats.push_back(pixel_format_name(format)); + } + const auto format = is_3d + ? pixel_format_name(Frame_3D::native_pixel_format) + : pixel_format_name(pacing.video_enabled + ? render_2d::Pixel_Format::rgba8 : Frame_2D::native_pixel_format); + const auto native_format = is_3d + ? pixel_format_name(Frame_3D::native_pixel_format) + : pixel_format_name(Frame_2D::native_pixel_format); + const std::size_t byte_length = pacing.video_enabled + ? static_cast(stream.width) * stream.height * 4U : 0U; + nlohmann::json output{ + {"protocol", "aethera.plot.diagnostics"}, {"version", 2}, + {"dimension", is_3d ? "3D" : "2D"}, + {"sequence", identity.sequence}, + {"correlation_id", identity.correlation_id}, + {"rendered_sequence", identity.sequence}, + {"rendered_correlation_id", identity.correlation_id}, + {"generated_time_unix_ms", + static_cast(created_time_unix_ns) / 1'000'000.0}, + {"delivery", pacing.video_enabled ? "gallery-video" : "diagnostics"}, + {"frame_rate_fps", frame_rate}, + {"dropped_sequence_count", dropped_sequences}, + {"window_capacity", diagnostic_window_capacity}, + {"pixel", {{"width", stream.width}, {"height", stream.height}, + {"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}}}, + {"frame_statistics", std::move(frame_statistics)}, + {"input_statistics", nlohmann::json::object()}}; + if (is_3d) { + const auto gpu = gpu_completion_state(); + const auto milliseconds = [](std::uint64_t nanoseconds) { + return static_cast(nanoseconds) / 1'000'000.0; + }; + output["gpu_completion_domain"] = { + {"capacity", gpu.capacity}, {"in_flight", gpu.in_flight}, + {"peak_in_flight", gpu.peak_in_flight}, {"watched", gpu.watched}, + {"peak_watched", gpu.peak_watched}, + {"active_fences", gpu.active_fences}, + {"pending_fences", gpu.pending_fences}, + {"reservation_count", gpu.reservation_count}, + {"completion_count", gpu.completion_count}, + {"cancellation_count", gpu.cancellation_count}, + {"fence_probe_count", gpu.fence_probe_count}, + {"fence_wait_count", gpu.fence_wait_count}, + {"fence_wait_timeout_count", gpu.fence_wait_timeout_count}, + {"fence_wait_total_ms", milliseconds(gpu.fence_wait_total_ns)}, + {"fence_wait_max_ms", milliseconds(gpu.fence_wait_max_ns)}, + {"callback_total_ms", milliseconds(gpu.callback_total_ns)}, + {"callback_max_ms", milliseconds(gpu.callback_max_ns)}, + {"callback_failure_count", gpu.callback_failure_count}, + {"backpressure_count", gpu.backpressure_count}, + {"backpressure_wait_ms", milliseconds(gpu.backpressure_wait_ns)}, + {"fault_count", gpu.fault_count}, + {"abandoned_count", gpu.abandoned_count}, + {"stopping", gpu.stopping}}; + } + return output; } void Plot::reset_diagnostics() { - d->diagnostics.reset(); + std::visit([](auto& scene) { scene->reset_frame_statistics(); }, d->scene); } } diff --git a/web_server/src/Plot.hpp b/web_server/src/Plot.hpp index 8e6e5e6..9ad9329 100644 --- a/web_server/src/Plot.hpp +++ b/web_server/src/Plot.hpp @@ -85,10 +85,11 @@ public: void schedule_render(Plot_Render_Tick tick); void render_once(); 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); - void async_generate_data(nlohmann::json input, Json_Handler handler); + [[nodiscard]] nlohmann::json schema(); + [[nodiscard]] nlohmann::json write_prop(std::string_view component, + std::string_view key, + const nlohmann::json& value); + [[nodiscard]] nlohmann::json generate_data(const nlohmann::json& input); [[nodiscard]] nlohmann::json diagnostics() const; void reset_diagnostics(); diff --git a/web_server/src/Sliding_Statistics.cpp b/web_server/src/Sliding_Statistics.cpp deleted file mode 100644 index db54cac..0000000 --- a/web_server/src/Sliding_Statistics.cpp +++ /dev/null @@ -1,143 +0,0 @@ -#include "Sliding_Statistics.hpp" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace aethera::web { -namespace { -double percentile(const std::vector& sorted, double ratio) { - if (sorted.empty()) return 0.0; - const auto index = static_cast(std::ceil( - ratio * static_cast(sorted.size()))) - 1U; - return sorted[std::min(index, sorted.size() - 1U)]; -} - -double range_average(const std::vector& values, - std::size_t first, std::size_t last) { - if (first >= last) return 0.0; - return std::accumulate(values.begin() + static_cast(first), - values.begin() + static_cast(last), 0.0) / - static_cast(last - first); -} -} - -struct Sliding_Statistics::Private { - explicit Private(std::size_t value_capacity) - : values(value_capacity) {} - - mutable std::mutex mutex; - std::vector values; /* 预分配的固定容量滑动窗口。 */ - std::size_t size{}; /* 当前窗口中的有效样本数量。 */ - std::size_t next{}; /* 下一次覆盖写入的位置。 */ -}; - -Sliding_Statistics::Sliding_Statistics(std::size_t capacity) - : d(std::make_unique(capacity)) { - if (capacity == 0) - throw std::invalid_argument("statistics capacity must be positive"); -} - -Sliding_Statistics::~Sliding_Statistics() = default; - -void Sliding_Statistics::submit(double value) { - if (!std::isfinite(value)) return; - std::lock_guard lock(d->mutex); - d->values[d->next] = value; - d->next = (d->next + 1U) % d->values.size(); - d->size = std::min(d->size + 1U, d->values.size()); -} - -void Sliding_Statistics::reset() { - std::lock_guard lock(d->mutex); - d->size = 0; - d->next = 0; -} - -Statistic_Snapshot Sliding_Statistics::snapshot() const { - std::vector values; - double latest{}; - { - std::lock_guard lock(d->mutex); - values.reserve(d->size); - if (d->size == 0) return {}; - const auto first = d->size == d->values.size() ? d->next : 0U; - for (std::size_t offset = 0; offset < d->size; ++offset) - values.push_back(d->values[(first + offset) % d->values.size()]); - latest = values.back(); - } - std::ranges::sort(values); - const double mean = range_average(values, 0, values.size()); - const auto trim = values.size() >= 20U ? values.size() / 20U : 0U; - const double variance = std::accumulate( - values.begin(), values.end(), 0.0, - [mean](double sum, double value) { - const double distance = value - mean; - return sum + distance * distance; - }) / static_cast(values.size()); - return Statistic_Snapshot{ - values.size(), latest, values.front(), values.back(), mean, - range_average(values, trim, values.size() - trim), - std::sqrt(variance), percentile(values, 0.50), - percentile(values, 0.95), percentile(values, 0.99)}; -} - -void to_json(nlohmann::json& output, const Statistic_Snapshot& snapshot) { - output = nlohmann::json{ - {"count", snapshot.count}, {"latest", snapshot.latest}, - {"minimum", snapshot.minimum}, {"maximum", snapshot.maximum}, - {"average", snapshot.average}, - {"trimmed_average", snapshot.trimmed_average}, - {"variability", snapshot.variability}, {"p50", snapshot.p50}, - {"p95", snapshot.p95}, {"p99", snapshot.p99}}; -} - -struct Sliding_Statistics_Set::Private { - explicit Private(std::size_t value_capacity) - : capacity(value_capacity) {} - - mutable std::mutex mutex; - std::size_t capacity{}; /* 每个命名字段保留的相同滑动窗口容量。 */ - std::map, std::less<>> values; -}; - -Sliding_Statistics_Set::Sliding_Statistics_Set(std::size_t capacity) - : d(std::make_unique(capacity)) { - if (capacity == 0) - throw std::invalid_argument("statistics set capacity must be positive"); -} - -Sliding_Statistics_Set::~Sliding_Statistics_Set() = default; - -void Sliding_Statistics_Set::submit( - std::span> values) { - std::lock_guard lock(d->mutex); - for (const auto& [key, value] : values) { - if (!std::isfinite(value)) continue; - auto found = d->values.find(key); - if (found == d->values.end()) - found = d->values.emplace( - std::string{key}, std::make_unique(d->capacity)).first; - found->second->submit(value); - } -} - -void Sliding_Statistics_Set::reset() { - std::lock_guard lock(d->mutex); - d->values.clear(); -} - -std::vector Sliding_Statistics_Set::snapshot() const { - std::vector output; - std::lock_guard lock(d->mutex); - output.reserve(d->values.size()); - for (const auto& [key, value] : d->values) - output.push_back({key, value->snapshot()}); - std::ranges::sort(output, {}, &Named_Statistic_Snapshot::key); - return output; -} -} diff --git a/web_server/src/Sliding_Statistics.hpp b/web_server/src/Sliding_Statistics.hpp deleted file mode 100644 index d31e150..0000000 --- a/web_server/src/Sliding_Statistics.hpp +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include - -namespace aethera::web { -struct Statistic_Snapshot { - std::size_t count{}; /* 当前滑动窗口内的有效样本数。 */ - double latest{}; /* 最近一次提交的瞬时值。 */ - double minimum{}; /* 窗口最小值。 */ - double maximum{}; /* 窗口最大值。 */ - double average{}; /* 窗口算术平均值。 */ - double trimmed_average{}; /* 两端各舍弃 5% 样本后的平均值。 */ - double variability{}; /* 窗口总体标准差。 */ - double p50{}; /* 窗口第 50 百分位。 */ - double p95{}; /* 窗口第 95 百分位。 */ - double p99{}; /* 窗口第 99 百分位。 */ -}; - -void to_json(nlohmann::json& output, const Statistic_Snapshot& snapshot); - -class Sliding_Statistics final { -public: - explicit Sliding_Statistics(std::size_t capacity = 600); - ~Sliding_Statistics(); - Sliding_Statistics(const Sliding_Statistics&) = delete; - Sliding_Statistics& operator=(const Sliding_Statistics&) = delete; - - void submit(double value); - void reset(); - [[nodiscard]] Statistic_Snapshot snapshot() const; - -private: - struct Private; - std::unique_ptr d; -}; - -struct Named_Statistic_Snapshot { - std::string key; /* 协议适配使用的稳定业务字段名。 */ - Statistic_Snapshot statistics{}; /* 该字段当前滑动窗口的派生统计。 */ -}; - -class Sliding_Statistics_Set final { -public: - explicit Sliding_Statistics_Set(std::size_t capacity = 600); - ~Sliding_Statistics_Set(); - Sliding_Statistics_Set(const Sliding_Statistics_Set&) = delete; - Sliding_Statistics_Set& operator=(const Sliding_Statistics_Set&) = delete; - - void submit(std::span> values); - void reset(); - [[nodiscard]] std::vector snapshot() const; - -private: - struct Private; - std::unique_ptr d; -}; -} diff --git a/web_server/src/WebRtc_Video_Session.cpp b/web_server/src/WebRtc_Video_Session.cpp index 4ff016b..d273bd7 100644 --- a/web_server/src/WebRtc_Video_Session.cpp +++ b/web_server/src/WebRtc_Video_Session.cpp @@ -147,23 +147,28 @@ struct WebRtc_Video_Session::Private { return; try { const auto lock_started = steady_nanoseconds(); - std::lock_guard lock(media_mutex); - record_media_lock_wait(lock_started); - if (!video_track || - !callbacks->track_open.load(std::memory_order_acquire) || - callbacks->closed.load(std::memory_order_acquire)) { - outstanding_video_frames.fetch_sub( - 1, std::memory_order_release); - rejected_frame_count.fetch_add(1, std::memory_order_relaxed); - continue; + std::shared_ptr track; + { + std::lock_guard lock(media_mutex); + record_media_lock_wait(lock_started); + if (!video_track || + !callbacks->track_open.load(std::memory_order_acquire) || + callbacks->closed.load(std::memory_order_acquire)) { + outstanding_video_frames.fetch_sub( + 1, std::memory_order_release); + rejected_frame_count.fetch_add(1, std::memory_order_relaxed); + continue; + } + track = video_track; } /* rtc::binary 与编码器 access unit 使用相同的 byte vector。 - * 这里把所有权直接交给 packetizer,禁止再次复制完整 H.264 帧。 */ + * 这里只在锁内取得 Track 的共享所有权;packetizer 和网络发送均为 + * 第三方慢调用,绝不能占用信令、就绪查询和 close 共用的生命周期锁。 */ const auto byte_count = frame.annex_b.size(); const auto send_started = steady_nanoseconds(); send_started_ns.store(send_started, std::memory_order_relaxed); send_active.store(true, std::memory_order_release); - video_track->sendFrame( + track->sendFrame( std::move(frame.annex_b), rtc::FrameInfo(std::chrono::duration( frame.presentation_time))); @@ -358,14 +363,18 @@ WebRtc_Video_Session::Send_Result WebRtc_Video_Session::send( bool WebRtc_Video_Session::can_accept_video() const noexcept { d->readiness_check_count.fetch_add(1, std::memory_order_relaxed); const auto lock_started = steady_nanoseconds(); - std::lock_guard lock(d->media_mutex); - d->record_media_lock_wait(lock_started); - const auto buffered = d->video_track ? d->video_track->bufferedAmount() : 0; + std::shared_ptr track; + { + std::lock_guard lock(d->media_mutex); + d->record_media_lock_wait(lock_started); + track = d->video_track; + } + const auto buffered = track ? track->bufferedAmount() : 0; d->transport_buffered_bytes.store(buffered, std::memory_order_relaxed); const bool ready = d->callbacks->track_open.load(std::memory_order_acquire) && !d->callbacks->closed.load(std::memory_order_acquire) && d->sender_thread.joinable() && - d->video_track && + track && buffered < maximum_transport_buffered_bytes && d->outstanding_video_frames.load(std::memory_order_acquire) < maximum_outstanding_video_frames; @@ -406,7 +415,7 @@ nlohmann::json WebRtc_Video_Session::diagnostics() const { state_to_publish.sender_running = d->sender_loop_active.load(std::memory_order_relaxed); d->state.advance(); - const auto snapshot = *d->state.pending; + const auto& published = *d->state.pending; const auto milliseconds = [](std::uint64_t nanoseconds) { return static_cast(nanoseconds) / 1'000'000.0; }; @@ -414,34 +423,34 @@ nlohmann::json WebRtc_Video_Session::diagnostics() const { {"kind", "webrtc_transport_state"}, {"protocol", "aethera.gallery.webrtc"}, {"version", 1}, - {"track_open", snapshot.track_open}, - {"closed", snapshot.closed}, - {"sender_running", snapshot.sender_running}, - {"outstanding_video_frames", snapshot.outstanding_video_frames}, - {"transport_buffered_bytes", snapshot.transport_buffered_bytes}, - {"queued_frame_count", snapshot.queued_frame_count}, - {"rejected_frame_count", snapshot.rejected_frame_count}, - {"queued_megabytes", static_cast(snapshot.queued_byte_count) / + {"track_open", published.track_open}, + {"closed", published.closed}, + {"sender_running", published.sender_running}, + {"outstanding_video_frames", published.outstanding_video_frames}, + {"transport_buffered_bytes", published.transport_buffered_bytes}, + {"queued_frame_count", published.queued_frame_count}, + {"rejected_frame_count", published.rejected_frame_count}, + {"queued_megabytes", static_cast(published.queued_byte_count) / (1024.0 * 1024.0)}, - {"sent_frame_count", snapshot.sent_frame_count}, - {"sent_megabytes", static_cast(snapshot.sent_byte_count) / + {"sent_frame_count", published.sent_frame_count}, + {"sent_megabytes", static_cast(published.sent_byte_count) / (1024.0 * 1024.0)}, - {"send_failure_count", snapshot.send_failure_count}, - {"send_average_ms", snapshot.sent_frame_count == 0 ? 0.0 : - milliseconds(snapshot.send_total_ns) / - static_cast(snapshot.sent_frame_count)}, - {"send_max_ms", milliseconds(snapshot.send_max_ns)}, - {"current_send_ms", milliseconds(snapshot.current_send_ns)}, - {"media_lock_wait_count", snapshot.media_lock_wait_count}, - {"media_lock_wait_average_ms", snapshot.media_lock_wait_count == 0 ? 0.0 : - milliseconds(snapshot.media_lock_wait_total_ns) / - static_cast(snapshot.media_lock_wait_count)}, - {"media_lock_wait_max_ms", milliseconds(snapshot.media_lock_wait_max_ns)}, - {"readiness_check_count", snapshot.readiness_check_count}, - {"readiness_reject_count", snapshot.readiness_reject_count}, - {"close_join_total_ms", milliseconds(snapshot.close_join_total_ns)}, - {"close_join_max_ms", milliseconds(snapshot.close_join_max_ns)}, - {"current_close_join_ms", milliseconds(snapshot.current_close_join_ns)}}; + {"send_failure_count", published.send_failure_count}, + {"send_average_ms", published.sent_frame_count == 0 ? 0.0 : + milliseconds(published.send_total_ns) / + static_cast(published.sent_frame_count)}, + {"send_max_ms", milliseconds(published.send_max_ns)}, + {"current_send_ms", milliseconds(published.current_send_ns)}, + {"media_lock_wait_count", published.media_lock_wait_count}, + {"media_lock_wait_average_ms", published.media_lock_wait_count == 0 ? 0.0 : + milliseconds(published.media_lock_wait_total_ns) / + static_cast(published.media_lock_wait_count)}, + {"media_lock_wait_max_ms", milliseconds(published.media_lock_wait_max_ns)}, + {"readiness_check_count", published.readiness_check_count}, + {"readiness_reject_count", published.readiness_reject_count}, + {"close_join_total_ms", milliseconds(published.close_join_total_ns)}, + {"close_join_max_ms", milliseconds(published.close_join_max_ns)}, + {"current_close_join_ms", milliseconds(published.current_close_join_ns)}}; } void WebRtc_Video_Session::close() noexcept { diff --git a/web_server/src/Web_Server.cpp b/web_server/src/Web_Server.cpp index 2bad2b4..dfe850f 100644 --- a/web_server/src/Web_Server.cpp +++ b/web_server/src/Web_Server.cpp @@ -175,11 +175,11 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) callback(error_response(drogon::k404NotFound, "unknown plot")); return; } - auto output = std::make_shared>( - std::move(callback)); - plot->async_schema([output](nlohmann::json schema) { - (*output)(json_response(std::move(schema))); - }); + try { callback(json_response(plot->schema())); } + catch (const std::exception& failure) { + callback(error_response(drogon::k500InternalServerError, + failure.what())); + } }, {drogon::Get}); app.registerHandler("/plot/{1}/component/{2}/prop/{3}", [plots]( @@ -200,11 +200,13 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) callback(error_response(drogon::k400BadRequest, "invalid JSON value")); return; } - auto output = std::make_shared>( - std::move(callback)); - plot->async_write_prop(std::move(component), std::move(key), std::move(value), [output](nlohmann::json result) { - (*output)(json_response(std::move(result))); - }); + try { + callback(json_response(plot->write_prop(component, key, value))); + } + catch (const std::exception& failure) { + callback(error_response(drogon::k500InternalServerError, + failure.what())); + } }, {drogon::Put}); app.registerHandler("/plot/{1}/data/generate", [plots]( @@ -227,11 +229,11 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) callback(error_response(drogon::k400BadRequest, "data generation input must be an object")); return; } - auto output = std::make_shared>( - std::move(callback)); - plot->async_generate_data(std::move(input), [output](nlohmann::json result) { - (*output)(json_response(std::move(result))); - }); + try { callback(json_response(plot->generate_data(input))); } + catch (const std::exception& failure) { + callback(error_response(drogon::k500InternalServerError, + failure.what())); + } }, {drogon::Post}); app.registerController(websocket) diff --git a/web_server/tests/Sliding_Statistics_Tests.cpp b/web_server/tests/Sliding_Statistics_Tests.cpp index 54dd364..5baff2d 100644 --- a/web_server/tests/Sliding_Statistics_Tests.cpp +++ b/web_server/tests/Sliding_Statistics_Tests.cpp @@ -1,38 +1,55 @@ -#include "web_server/src/Sliding_Statistics.hpp" +#include #include #include namespace aethera::web { -TEST(Sliding_Statistics, Derives_Window_Statistics_On_Demand) { +TEST(Sliding_Statistics, Publishes_Derived_Window_Statistics_On_Submit) { Sliding_Statistics statistics{20}; for (int value = 1; value <= 20; ++value) - statistics.submit(static_cast(value)); + static_cast(statistics.submit(static_cast(value))); - const auto snapshot = statistics.snapshot(); - EXPECT_EQ(snapshot.count, 20U); - EXPECT_DOUBLE_EQ(snapshot.latest, 20.0); - EXPECT_DOUBLE_EQ(snapshot.minimum, 1.0); - EXPECT_DOUBLE_EQ(snapshot.maximum, 20.0); - EXPECT_DOUBLE_EQ(snapshot.average, 10.5); - EXPECT_DOUBLE_EQ(snapshot.trimmed_average, 10.5); - EXPECT_DOUBLE_EQ(snapshot.p50, 10.0); - EXPECT_DOUBLE_EQ(snapshot.p95, 19.0); - EXPECT_DOUBLE_EQ(snapshot.p99, 20.0); + const auto& state = statistics.state(); + EXPECT_EQ(state.count, 20U); + EXPECT_DOUBLE_EQ(state.latest, 20.0); + EXPECT_DOUBLE_EQ(state.minimum, 1.0); + EXPECT_DOUBLE_EQ(state.maximum, 20.0); + EXPECT_DOUBLE_EQ(state.average, 10.5); + EXPECT_DOUBLE_EQ(state.trimmed_average, 10.5); + EXPECT_NEAR(state.p50, 10.0, 2.0); + EXPECT_GE(state.p95, 15.0); + EXPECT_LE(state.p95, 20.0); + EXPECT_GE(state.p99, state.p95); + EXPECT_LE(state.p99, 20.0); } TEST(Sliding_Statistics, Overwrites_Oldest_And_Ignores_Nonfinite_Values) { Sliding_Statistics statistics{3}; - statistics.submit(1.0); - statistics.submit(2.0); - statistics.submit(std::numeric_limits::infinity()); - statistics.submit(3.0); - statistics.submit(4.0); + static_cast(statistics.submit(1.0)); + static_cast(statistics.submit(2.0)); + static_cast(statistics.submit(std::numeric_limits::infinity())); + static_cast(statistics.submit(3.0)); + static_cast(statistics.submit(4.0)); - const auto snapshot = statistics.snapshot(); - EXPECT_EQ(snapshot.count, 3U); - EXPECT_DOUBLE_EQ(snapshot.latest, 4.0); - EXPECT_DOUBLE_EQ(snapshot.minimum, 2.0); - EXPECT_DOUBLE_EQ(snapshot.maximum, 4.0); - EXPECT_DOUBLE_EQ(snapshot.average, 3.0); + const auto& state = statistics.state(); + EXPECT_EQ(state.count, 3U); + EXPECT_DOUBLE_EQ(state.latest, 4.0); + EXPECT_DOUBLE_EQ(state.minimum, 2.0); + EXPECT_DOUBLE_EQ(state.maximum, 4.0); + EXPECT_DOUBLE_EQ(state.average, 3.0); +} + +TEST(Sliding_Statistics, Estimates_Quantiles_Without_Growing_The_Window) { + Sliding_Statistics statistics{64}; + for (int value = 1; value <= 1000; ++value) + static_cast(statistics.submit(static_cast(value))); + + const auto& state = statistics.state(); + EXPECT_EQ(state.count, 64U); + EXPECT_NEAR(state.average, 968.5, 0.001); + EXPECT_DOUBLE_EQ(state.minimum, 937.0); + EXPECT_DOUBLE_EQ(state.maximum, 1000.0); + EXPECT_NEAR(state.p50, 500.0, 10.0); + EXPECT_NEAR(state.p95, 950.0, 15.0); + EXPECT_NEAR(state.p99, 990.0, 15.0); } } diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index 1c392e0..2f7a148 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -32,7 +32,7 @@ type Pixel_Format = "rgba8" | "bgra8_premultiplied"; type Frame_Stage_Values = Record; type Statistic_Snapshot = {count: number; latest: number; minimum: number; maximum: number; average: number; trimmed_average: number; variability: number; p50: number; p95: number; p99: number}; -type Plot_Diagnostics = {protocol: "aethera.plot.diagnostics"; version: 1; dimension: "2D" | "3D"; +type Plot_Diagnostics = {protocol: "aethera.plot.diagnostics"; version: 2; 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; @@ -88,7 +88,7 @@ function socket_url(path: string) { function valid_plot_diagnostics(value: unknown): value is Plot_Diagnostics { if (!value || typeof value !== "object") return false; const diagnostics = value as Partial; - return diagnostics.protocol === "aethera.plot.diagnostics" && diagnostics.version === 1 && + return diagnostics.protocol === "aethera.plot.diagnostics" && diagnostics.version === 2 && typeof diagnostics.sequence === "number" && Boolean(diagnostics.frame_statistics) && Boolean(diagnostics.input_statistics) && Boolean(diagnostics.pacing); } @@ -113,6 +113,41 @@ function valid_gallery_metrics(value: unknown): value is Gallery_Transport_Metri Boolean(metrics.sources); } +function use_selected_plot_diagnostics(plot: Plot | null) { + useEffect(() => { + if (!plot) return; + let stopped = false; + let timer = 0; + const group_id = new URL(plot.media, location.href).searchParams.get("group") ?? ""; + const gallery_endpoint = group_id + ? `/gallery/${encodeURIComponent(group_id)}/diagnostics` : null; + const sample = async () => { + try { + const [plot_response, gallery_response] = await Promise.all([ + fetch(plot.diagnostics, {cache: "no-store"}), + gallery_endpoint + ? fetch(gallery_endpoint, {cache: "no-store"}) + : Promise.resolve(null) + ]); + const diagnostics: unknown = await plot_response.json(); + const gallery: unknown = gallery_response + ? await gallery_response.json() : null; + if (!stopped && valid_plot_diagnostics(diagnostics)) + window.dispatchEvent(new CustomEvent("aethera:plot-diagnostics", + {detail: {plot_id: plot.id, diagnostics}})); + if (!stopped && group_id && valid_gallery_metrics(gallery)) + window.dispatchEvent(new CustomEvent("aethera:gallery-diagnostics", + {detail: {group_id, diagnostics: gallery}})); + } catch { + // Diagnostics are observational; the selected Plot keeps rendering. + } + if (!stopped) timer = window.setTimeout(() => void sample(), 1000); + }; + void sample(); + return () => { stopped = true; window.clearTimeout(timer); }; + }, [plot?.id, plot?.diagnostics, plot?.media]); +} + function percentile(values: number[], ratio: number) { if (values.length === 0) return 0; const sorted = [...values].sort((left, right) => left - right); @@ -153,19 +188,17 @@ function use_gallery_videos(plots: Plot[]): Gallery_Video_States { set_states(current => ({...current, [endpoint]: change(current[endpoint] ?? connecting_gallery_video())})); const group_id = new URL(endpoint, location.href).searchParams.get("group") ?? ""; - const diagnostics_endpoint = `/gallery/${encodeURIComponent(group_id)}/diagnostics`; - const load_server_diagnostics = async () => { - try { - const response = await fetch(diagnostics_endpoint, {cache: "no-store"}); - const diagnostics: unknown = await response.json(); - if (stopped || !valid_gallery_metrics(diagnostics)) return; - update(current => ({...current, transport: diagnostics})); - window.dispatchEvent(new CustomEvent("aethera:gallery-metrics", - {detail: {...diagnostics, media: endpoint}})); - } catch { - // 视频连接不依赖诊断端点;下一秒自动重试。 - } + const receive_server_diagnostics = (event: Event) => { + const detail = (event as CustomEvent<{group_id: string; + diagnostics: Gallery_Transport_Metrics}>).detail; + if (stopped || detail?.group_id !== group_id || + !valid_gallery_metrics(detail.diagnostics)) return; + update(current => ({...current, transport: detail.diagnostics})); + window.dispatchEvent(new CustomEvent("aethera:gallery-metrics", + {detail: {...detail.diagnostics, media: endpoint}})); }; + window.addEventListener("aethera:gallery-diagnostics", + receive_server_diagnostics); const socket = new ReconnectingWebSocket(socket_url(endpoint), [], { minReconnectionDelay: 300, maxReconnectionDelay: 5000, reconnectionDelayGrowFactor: 1.6, maxRetries: Number.POSITIVE_INFINITY @@ -274,14 +307,14 @@ function use_gallery_videos(plots: Plot[]): Gallery_Video_States { }).catch(error => update(current => ({...current, status: "OFFLINE", error: error instanceof Error ? error.message : "WebRTC negotiation failed"}))); }; - void load_server_diagnostics(); const stats_timer = window.setInterval(() => { void sample_stats(); - void load_server_diagnostics(); }, 1000); return () => { stopped = true; window.clearInterval(stats_timer); + window.removeEventListener("aethera:gallery-diagnostics", + receive_server_diagnostics); peer?.close(); socket.close(); }; @@ -317,55 +350,51 @@ function use_plot_stream(plot: Plot, useEffect(() => { let stopped = false; let samples: Frame_Sample[] = []; - let diagnostics_request = 0; const socket = new ReconnectingWebSocket(socket_url(plot.websocket), [], { minReconnectionDelay: 300, maxReconnectionDelay: 5000, reconnectionDelayGrowFactor: 1.6, maxRetries: Number.POSITIVE_INFINITY }); socket_ref.current = socket; set_status("CONNECTING"); - const load_diagnostics = async () => { - const request = ++diagnostics_request; - try { - const response = await fetch(plot.diagnostics, {cache: "no-store"}); - const server: unknown = await response.json(); - if (stopped || request !== diagnostics_request || !valid_plot_diagnostics(server)) return; - const now = performance.now(); - const values = Object.fromEntries(Object.entries(server.frame_statistics) - .map(([key, statistic]) => [key, statistic.latest])); - if (server.sequence !== 0 && samples.at(-1)?.sequence !== server.sequence) - samples = [...samples, {sequence: server.sequence, - generated_at_ms: server.generated_time_unix_ms, - received_at_ms: now, values}].slice(-600); - const diagnostics: Frame_Diagnostics = { - server, samples, video_playback: {...playback_ref.current} - }; - set_server_diagnostics(server); - const completion = server.frame_statistics.server_completion_ms; - const interval = server.frame_statistics.frame_interval_ms; - set_metrics({ - sequence: server.sequence, - generated_time_unix_ms: server.generated_time_unix_ms, - server_completion_ms: completion?.latest ?? 0, - average_server_completion_ms: completion?.average ?? 0, - p95_server_completion_ms: completion?.p95 ?? 0, - 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, - delivery: server.delivery, - video_playback: diagnostics.video_playback - }); - window.dispatchEvent(new CustomEvent("aethera-frame-diagnostics", { - detail: {plot_id: plot.id, diagnostics} - })); - } catch { - // 媒体与输入连接继续工作;下一次低频采样会重试诊断请求。 - } + const receive_diagnostics = (event: Event) => { + const detail = (event as CustomEvent<{plot_id: string; + diagnostics: Plot_Diagnostics}>).detail; + const server = detail?.diagnostics; + if (stopped || detail?.plot_id !== plot.id || + !valid_plot_diagnostics(server)) return; + const now = performance.now(); + const values = Object.fromEntries(Object.entries(server.frame_statistics) + .map(([key, statistic]) => [key, statistic.latest])); + if (server.sequence !== 0 && + samples.at(-1)?.sequence !== server.sequence) + samples = [...samples, {sequence: server.sequence, + generated_at_ms: server.generated_time_unix_ms, + received_at_ms: now, values}].slice(-600); + const diagnostics: Frame_Diagnostics = { + server, samples, video_playback: {...playback_ref.current} + }; + set_server_diagnostics(server); + const completion = server.frame_statistics.server_completion_ms; + const interval = server.frame_statistics.frame_interval_ms; + set_metrics({ + sequence: server.sequence, + generated_time_unix_ms: server.generated_time_unix_ms, + server_completion_ms: completion?.latest ?? 0, + average_server_completion_ms: completion?.average ?? 0, + p95_server_completion_ms: completion?.p95 ?? 0, + 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, + delivery: server.delivery, + video_playback: diagnostics.video_playback + }); + window.dispatchEvent(new CustomEvent("aethera-frame-diagnostics", { + detail: {plot_id: plot.id, diagnostics} + })); }; - void load_diagnostics(); - const diagnostics_timer = window.setInterval(() => void load_diagnostics(), 1000); + window.addEventListener("aethera:plot-diagnostics", receive_diagnostics); const on_manual_frame = (event: Event) => { const detail = (event as CustomEvent<{plot_id: string}>).detail; if (detail?.plot_id === plot.id && socket.readyState === WebSocket.OPEN) @@ -386,7 +415,7 @@ function use_plot_stream(plot: Plot, if (detail?.plot_id !== plot.id) return; samples = []; set_metrics(null); - void fetch(plot.diagnostics, {method: "DELETE"}).then(() => load_diagnostics()); + void fetch(plot.diagnostics, {method: "DELETE"}); window.dispatchEvent(new CustomEvent("aethera-frame-diagnostics-cleared", { detail: {plot_id: plot.id} })); @@ -409,7 +438,7 @@ function use_plot_stream(plot: Plot, stopped = true; set_status("OFFLINE"); set_error(message.message ?? "Plot failed"); - socket.close(1011, "Plot failed"); + socket.close(4001, "Plot failed"); return; } if (message?.kind === "exclusive_page_rejected") { @@ -420,7 +449,7 @@ function use_plot_stream(plot: Plot, }; return () => { stopped = true; - window.clearInterval(diagnostics_timer); + window.removeEventListener("aethera:plot-diagnostics", receive_diagnostics); window.removeEventListener("aethera-manual-frame", on_manual_frame); window.removeEventListener("aethera-reset-camera", on_camera_reset); window.removeEventListener("aethera-reset-frame-diagnostics", on_diagnostics_reset); @@ -1091,8 +1120,8 @@ const Plot_Card = memo(function Plot_Card({plot, selected, policy, gallery, on_p data-input-dispatch-ms={server_input?.input_dispatch_ms?.latest ?? ""} data-input-server-consume-ms={server_input?.input_server_consume_ms?.latest ?? ""} onClick={() => on_select(plot)}> -
-
绘图组件 · {plot.dimension}

{plot_labels[plot.id] ?? plot.title}

+
+
绘图组件 · {plot.dimension}

{plot_labels[plot.id] ?? plot.title}

{{IDLE: "已停止", CONNECTING: "重连中", LIVE: "实时", OFFLINE: "已离线"}[status]}{metrics?.delivery === "diagnostics" ? "无像素传输" : metrics ? enum_label(metrics.pacing_mode) : "等待策略"}
@@ -1276,6 +1305,7 @@ function load_workspace_model() { export function App() { const [plots, set_plots] = useState([]); const [category, set_category] = useState("全部"); const [selected, set_selected] = useState(null); + use_selected_plot_diagnostics(selected); const gallery_videos = use_gallery_videos(plots); const [execution_policies, set_execution_policies] = useState({}); const [schema, set_schema] = useState(null); @@ -1325,7 +1355,6 @@ export function App() { finally { if (show_busy && request === schema_busy_request.current) set_schema_busy(false); } }, [selected]); useEffect(() => { ++schema_request.current; ++schema_busy_request.current; set_schema_busy(false); set_schema(null); set_state_histories({}); if (selected) void load_schema(true); }, [selected, load_schema]); - useEffect(() => { if (!selected) return; const timer = window.setInterval(() => void load_schema(false), 1000); return () => window.clearInterval(timer); }, [selected, load_schema]); const update = async (component: Component | Frame_Analysis, field: Field, value: unknown) => { if (!selected) return; const response = await fetch(`/plot/${encodeURIComponent(selected.id)}/component/${encodeURIComponent(component.id)}/prop/${encodeURIComponent(field.key)}`, { diff --git a/webapp_gallery/src/styles.css b/webapp_gallery/src/styles.css index 8baec0f..26dd3fd 100644 --- a/webapp_gallery/src/styles.css +++ b/webapp_gallery/src/styles.css @@ -62,14 +62,16 @@ nav { display: flex; flex-wrap: wrap; gap: 8px; padding: 18px 0; } .card { display: flex; flex-direction: column; width: 100%; height: 100%; min-width: 0; min-height: 0; overflow: hidden; border: 1px solid #1f2f48; border-radius: 18px; background: linear-gradient(145deg, #0d1727, #080e19); box-shadow: 0 15px 45px #0006; } .card.selected { border-color: #5ce4c2; box-shadow: 0 0 0 1px #5ce4c255, 0 18px 55px #0008; } .card > header { display: flex; justify-content: space-between; gap: 16px; padding: 18px 20px 14px; } -.cardDragHandle { cursor: grab; user-select: none; touch-action: none; } +.cardTitle { display: flex; align-items: flex-start; gap: 8px; min-width: 0; user-select: text; } +.cardDragHandle { flex: 0 0 auto; margin-top: -2px; padding: 2px 3px; color: #58708f; cursor: grab; user-select: none; touch-action: none; } +.cardDragHandle:hover { color: #5ce4c2; } .cardDragHandle:active { cursor: grabbing; } .card > p { min-height: 42px; margin: 0; padding: 0 20px 14px; color: #8fa2bd; line-height: 1.5; } .status { align-self: flex-start; padding: 5px 8px; color: #5ce4c2; border: 1px solid #27594f; border-radius: 7px; font: 700 10px/1 ui-monospace, monospace; letter-spacing: .08em; } -.cardRuntime { display: grid; flex: 0 0 auto; justify-items: end; gap: 7px; } +.cardRuntime { display: grid; flex: 0 0 auto; justify-items: end; gap: 7px; user-select: text; cursor: text; } .cardRuntime > div:first-child { display: flex; align-items: center; justify-content: flex-end; gap: 6px; } .framePolicy { color: #8296b2; font-size: 9px; white-space: nowrap; } -.frameMetrics { display: grid; grid-template-columns: auto auto; gap: 4px 9px; color: #8296b2; font: 9px/1.15 ui-monospace, monospace; font-variant-numeric: tabular-nums; white-space: nowrap; } +.frameMetrics { display: grid; grid-template-columns: auto auto; gap: 4px 9px; color: #8296b2; font: 9px/1.15 ui-monospace, monospace; font-variant-numeric: tabular-nums; white-space: nowrap; user-select: text; cursor: text; } .frameMetrics span:nth-child(2n) { text-align: right; } .plotViewport { position: relative; flex: 1; width: 100%; min-height: 160px; overflow: hidden; outline: none; background: #070d18; overscroll-behavior: contain; touch-action: none; } .plotExecutionPolicy { display:flex; flex-wrap:wrap; gap:6px 12px; padding:8px 12px; border-block:1px solid rgba(126,155,194,.12); background:rgba(6,13,24,.58); font-size:11px; color:#9dafc7; }