仍有bug
This commit is contained in:
@@ -411,6 +411,26 @@ public:
|
||||
std::lock_guard<Lock> guard(lock);
|
||||
advance_unlocked();
|
||||
}
|
||||
template <detail::State_Tag_In<States> Tag>
|
||||
void publish_state() {
|
||||
std::lock_guard<Lock> guard(lock);
|
||||
data().state.advance();
|
||||
data().state_callbacks.template notify<Tag>(*data().state.current);
|
||||
}
|
||||
template <detail::State_Tag_In<States> Tag, auto... Members, typename Callback>
|
||||
requires (sizeof...(Members) > 0) &&
|
||||
(detail::State_Member<Members, State> && ...) &&
|
||||
std::invocable<Callback, State_Access<State>>
|
||||
void publish_state(Callback&& callback) {
|
||||
std::lock_guard<Lock> guard(lock);
|
||||
(before_state_set(Members), ...);
|
||||
std::invoke(std::forward<Callback>(callback),
|
||||
State_Access{*data().state.pending});
|
||||
(after_state_set(Members), ...);
|
||||
(emit_state_dependencies<Members>(), ...);
|
||||
data().state.advance();
|
||||
data().state_callbacks.template notify<Tag>(*data().state.current);
|
||||
}
|
||||
template <std::invocable<const State&> Callback>
|
||||
void advance(Callback&& callback) {
|
||||
std::lock_guard<Lock> guard(lock);
|
||||
|
||||
+150
-10
@@ -1,9 +1,11 @@
|
||||
#include "frame.hpp"
|
||||
#include "frame_statistics.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
namespace aethera {
|
||||
namespace {
|
||||
constexpr std::size_t marker_count = static_cast<std::size_t>(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<Frame_Trace_Point> Render_Frame::trace_points() const {
|
||||
std::vector<Frame_Trace_Point> 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<Frame_Trace_Marker>(index), decode_present_value(encoded)});
|
||||
return result;
|
||||
}
|
||||
std::vector<Frame_Trace_Value> Render_Frame::trace_values() const {
|
||||
std::vector<Frame_Trace_Value> 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<Frame_Trace_Measurement>(index), decode_present_value(encoded)});
|
||||
Frame_Statistics_Sample Render_Frame::statistics(Frame_Dimension dimension) const {
|
||||
std::array<std::optional<double>, 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<double>(decode_present_value(encoded)) /
|
||||
1'000'000.0;
|
||||
}
|
||||
std::array<double, measurement_count> 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<double>(decode_present_value(encoded)) /
|
||||
1'000'000.0;
|
||||
}
|
||||
const auto marker = [&](Frame_Trace_Marker value) {
|
||||
return markers[static_cast<std::size_t>(value)].value_or(0.0);
|
||||
};
|
||||
const auto interval = [&](Frame_Trace_Marker first, Frame_Trace_Marker last) {
|
||||
const auto start = markers[static_cast<std::size_t>(first)];
|
||||
const auto finish = markers[static_cast<std::size_t>(last)];
|
||||
return start && finish ? std::max(0.0, *finish - *start) : 0.0;
|
||||
};
|
||||
const auto measurement = [&](Frame_Trace_Measurement value) {
|
||||
return measurements[static_cast<std::size_t>(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
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<Frame_Trace_Point> trace_points() const;
|
||||
[[nodiscard]] std::vector<Frame_Trace_Value> trace_values() const;
|
||||
[[nodiscard]] Frame_Statistics_Sample statistics(Frame_Dimension dimension) const;
|
||||
protected:
|
||||
/* 物理帧槽再次承载新逻辑帧时,重建其唯一身份和诊断时间原点。 */
|
||||
void begin(Frame_Identity identity) noexcept;
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
#include "frame_statistics.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace aethera {
|
||||
void Frame_Statistics_Sample::set(Frame_Statistic statistic, double value) noexcept {
|
||||
const auto index = static_cast<std::size_t>(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<std::size_t>(
|
||||
static_cast<std::ptrdiff_t>(index) +
|
||||
static_cast<std::ptrdiff_t>(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<std::ptrdiff_t>(count_));
|
||||
const auto index = std::min(count_ - 1U, static_cast<std::size_t>(
|
||||
std::ceil(probability_ * static_cast<double>(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<double>(size_);
|
||||
state_ = Statistic_State{
|
||||
size_, value, minimum_.value(), maximum_.value(), mean,
|
||||
trimmed_sum_ / static_cast<double>(size_),
|
||||
std::sqrt(std::max(0.0,
|
||||
squared_sum_ / static_cast<double>(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<double, std::milli>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
#pragma once
|
||||
#include "frame.hpp"
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
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<std::size_t>(Frame_Statistic::count);
|
||||
|
||||
struct Frame_Statistics_Sample {
|
||||
std::array<double, frame_statistic_count> values{};
|
||||
std::bitset<frame_statistic_count> 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<double, 5> initial_{};
|
||||
std::array<double, 5> heights_{};
|
||||
std::array<double, 5> positions_{};
|
||||
std::array<double, 5> desired_{};
|
||||
std::array<double, 5> 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<Node> nodes_;
|
||||
std::size_t head_{};
|
||||
std::size_t tail_{};
|
||||
bool minimum_{};
|
||||
};
|
||||
|
||||
/* 原始值和截尾值只为精确滑动均值/方差服务,构造后不再分配。 */
|
||||
std::vector<double> values_;
|
||||
std::vector<double> 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<Statistic_State, frame_statistic_count> 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<Sliding_Statistics> values_;
|
||||
Frame_Statistics_State state_{};
|
||||
std::chrono::steady_clock::time_point previous_completion_{};
|
||||
std::uint64_t previous_sequence_{};
|
||||
};
|
||||
}
|
||||
@@ -202,14 +202,13 @@ private:
|
||||
std::atomic_bool state_callback_enabled{};
|
||||
void create_executor(std::size_t workers, std::shared_ptr<tf::WorkerInterface> worker_interface) {
|
||||
executor = std::make_unique<tf::Executor>(workers, std::move(worker_interface));
|
||||
observer = executor->make_observer<Task_Observer>();
|
||||
observer.reset();
|
||||
state = {};
|
||||
}
|
||||
void ensure_executor() {
|
||||
std::lock_guard guard(state_mutex);
|
||||
if (executor) return;
|
||||
executor = std::make_unique<tf::Executor>();
|
||||
observer = executor->make_observer<Task_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<void(const Task_Runtime_State&)> callback) {
|
||||
ensure_executor();
|
||||
std::lock_guard guard(state_mutex);
|
||||
if (!observer) observer = executor->make_observer<Task_Observer>();
|
||||
state_callbacks.template set<Task_Runtime_State_Tag>(std::move(callback));
|
||||
state_callback_enabled.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
@@ -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<Private&>(*d).dispatch->render(this, frame); }
|
||||
std::expected<void, Render_Scene_2D::Render_Result>
|
||||
Render_Scene_2D::render(Frame_2D* frame) {
|
||||
return static_cast<Private&>(*d).dispatch->render(this, frame);
|
||||
}
|
||||
void Render_Scene_2D::set_frame_callback(Frame_Callback callback) { static_cast<Private&>(*d).dispatch->set_frame_callback(this, std::move(callback)); }
|
||||
tf::Taskflow& Render_Scene_2D::completion_taskflow() {
|
||||
return static_cast<Private&>(*d).completion_graph;
|
||||
@@ -15,4 +18,7 @@ void Render_Scene_2D::activate_view() {
|
||||
void Render_Scene_2D::deactivate_view() {
|
||||
static_cast<Private&>(*d).dispatch->set_active(this, false);
|
||||
}
|
||||
void Render_Scene_2D::reset_frame_statistics() {
|
||||
static_cast<Private&>(*d).dispatch->reset_statistics(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "../base/Renderable_2D.hpp"
|
||||
#include "../render/Blend2D_Cache.hpp"
|
||||
#include <scene.hpp>
|
||||
#include <frame_statistics.hpp>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
namespace aethera::render_2d {
|
||||
@@ -17,6 +19,7 @@ struct Render_Scene_2D : Def<Render_Scene_2D, Scene,
|
||||
bool operator==(const Prop&) const;
|
||||
};
|
||||
struct State : Prev_State {
|
||||
Frame_Statistics_State frame_statistics{};
|
||||
bool operator==(const State&) const;
|
||||
};
|
||||
/* 完整声明、合成顺序和 CRTP 分派见 Render_Scene_2D.ipp。 */
|
||||
@@ -33,10 +36,10 @@ struct Render_Scene_2D : Def<Render_Scene_2D, Scene,
|
||||
private:
|
||||
std::vector<std::function<std::expected<void, Dependency_Graph_Error>(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<void(Frame_2D*)>;
|
||||
/* 向调用方拥有的帧合成一次;必须先安装回调。回调返回前的并发请求返回 frame_in_flight。 */
|
||||
[[nodiscard]] Render_Result render(Frame_2D* frame);
|
||||
[[nodiscard]] std::expected<void, Render_Result> render(Frame_2D* frame);
|
||||
/* 安装完成帧回调;回调收到对应 render(frame) 的对象,返回时 Scene 才释放下一帧准入。 */
|
||||
void set_frame_callback(Frame_Callback callback);
|
||||
/*
|
||||
@@ -50,6 +53,7 @@ struct Render_Scene_2D : Def<Render_Scene_2D, Scene,
|
||||
void activate_view();
|
||||
/* 停止后续 render 调用,不清除最后一帧。 */
|
||||
void deactivate_view();
|
||||
void reset_frame_statistics();
|
||||
};
|
||||
}
|
||||
#include "Render_Scene_2D.ipp"
|
||||
|
||||
@@ -34,7 +34,7 @@ std::expected<std::unique_ptr<Object>, 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<void, Render_Result> (*)(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<tf::Taskflow> paint_taskflow{}; /* 仅由二维 Paint 图构建的执行图。 */
|
||||
~Private();
|
||||
@@ -81,7 +83,9 @@ struct Render_Scene_2D::Private : Prev_Private {
|
||||
template <Attached Object>
|
||||
void dispatch_events(Object* object, Size viewport,
|
||||
std::uint64_t frame_sequence);
|
||||
template <Attached Object> [[nodiscard]] Render_Result render(Object* object, Frame_2D* frame);
|
||||
template <Attached Object>
|
||||
[[nodiscard]] std::expected<void, Render_Result> render(Object* object,
|
||||
Frame_2D* frame);
|
||||
template <Attached Object>
|
||||
[[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>(callback));
|
||||
}
|
||||
template <Attached Object>
|
||||
Render_Scene_2D::Render_Result Render_Scene_2D::Private::render(Object* object, Frame_2D* frame) {
|
||||
std::expected<void, Render_Scene_2D::Render_Result>
|
||||
Render_Scene_2D::Private::render(Object* object, Frame_2D* frame) {
|
||||
static_cast<void>(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<typename Object::Private&>(*this);
|
||||
auto& scene_state = static_cast<State&>(*private_data.state.pending);
|
||||
scene_state.frame_statistics = frame_statistics.submit(
|
||||
*frame, Frame_Dimension::two_dimensional);
|
||||
private_data.state.advance();
|
||||
object->template notify_state<Render_Scene_2D::Base_Tag>();
|
||||
});
|
||||
if (completed) return Render_Result::completed;
|
||||
if (completed) return {};
|
||||
const auto& prop = object->template read_prop<Render_Scene_2D::Base_Tag>();
|
||||
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 <Attached Object>
|
||||
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 <Attached Object>
|
||||
const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for() {
|
||||
static const Dispatch value{
|
||||
[](Root* root) {
|
||||
auto* object = static_cast<Object*>(root);
|
||||
auto& data = static_cast<typename Object::Private&>(*object->d);
|
||||
object->template publish_state<Render_Scene_2D::Base_Tag,
|
||||
&State::frame_statistics>(
|
||||
[&data](State_Access<typename Object::State> states) {
|
||||
data.frame_statistics.reset();
|
||||
states.template get<Render_Scene_2D::Base_Tag>()
|
||||
.frame_statistics = {};
|
||||
});
|
||||
},
|
||||
[](Root* root, Frame_2D* frame) {
|
||||
auto* object = static_cast<Object*>(root);
|
||||
return static_cast<typename Object::Private&>(*object->d).render(object, frame);
|
||||
|
||||
@@ -18,7 +18,7 @@ std::unique_ptr<Frame_2D> render_frame(Scene* scene) {
|
||||
static std::uint64_t sequence{1};
|
||||
auto frame = std::make_unique<Frame_2D>(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 <typename Scene>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<const std::byte*>(view.data) +
|
||||
static_cast<std::ptrdiff_t>(y) * view.stride;
|
||||
for (std::ptrdiff_t index = 0;
|
||||
index < static_cast<std::ptrdiff_t>(view.width * sizeof(Pixel));
|
||||
++index) {
|
||||
value ^= static_cast<std::uint8_t>(row[index]);
|
||||
value *= 1099511628211ULL;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
template <typename Scene>
|
||||
std::unique_ptr<Frame_2D> render_frame(Scene* scene) {
|
||||
static std::uint64_t sequence{1};
|
||||
auto frame = std::make_unique<Frame_2D>(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<Frequency_Axis>;
|
||||
using Power = Impl<Numeric_Axis>;
|
||||
using Spectrum_Object = Impl<Spectrum>;
|
||||
using Scene_Object = Impl<Render_Scene_2D>;
|
||||
initialize_runtime(4);
|
||||
auto frequency = build_object<Frequency>();
|
||||
auto power = build_object<Power>();
|
||||
auto spectrum = build_object<Spectrum_Object>(frequency.get(), power.get());
|
||||
auto scene = build_scene<Scene_Object>(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<Spectrum_Power> samples(128, -95.0);
|
||||
const std::size_t peak = 8 + frame_index % 112;
|
||||
samples[peak] = -15.0;
|
||||
spectrum->pending_buffer<Spectrum_Frame_Tag>() =
|
||||
Spectrum_Frame{std::move(samples)};
|
||||
spectrum->mark_dirty<Prepare_Data_Tag>();
|
||||
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<Renderable::Base_Tag>().prepare_executed);
|
||||
EXPECT_TRUE(spectrum->read_state<Renderable::Base_Tag>().paint_executed);
|
||||
EXPECT_EQ(spectrum->read_state<Spectrum::Base_Tag>().sample_count, 128u);
|
||||
}
|
||||
EXPECT_GT(changed_frames, 700u);
|
||||
}
|
||||
|
||||
@@ -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<Private&>(*d).completion_graph;
|
||||
}
|
||||
void Render_Scene_3D::reset_frame_statistics() {
|
||||
static_cast<Private&>(*d).dispatch->reset_statistics(this);
|
||||
}
|
||||
void Render_Scene_3D::activate_view() { static_cast<Private&>(*d).dispatch->set_active(this, true); }
|
||||
void Render_Scene_3D::deactivate_view() { static_cast<Private&>(*d).dispatch->set_active(this, false); }
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "../detail/Backend_Types.hpp"
|
||||
#include "../visual/Visuals.hpp"
|
||||
#include <scene.hpp>
|
||||
#include <frame_statistics.hpp>
|
||||
#include <array>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
@@ -22,6 +23,7 @@ struct Render_Scene_3D : Def<Render_Scene_3D, Scene, Dependency_Graph_Type<Submi
|
||||
bool operator==(const Prop&) const;
|
||||
};
|
||||
struct State : Prev_State {
|
||||
Frame_Statistics_State frame_statistics{};
|
||||
bool operator==(const State&) const;
|
||||
};
|
||||
struct Private;
|
||||
@@ -71,6 +73,7 @@ struct Render_Scene_3D : Def<Render_Scene_3D, Scene, Dependency_Graph_Type<Submi
|
||||
[[nodiscard]] tf::Taskflow& completion_taskflow();
|
||||
void activate_view();
|
||||
void deactivate_view();
|
||||
void reset_frame_statistics();
|
||||
};
|
||||
}
|
||||
#include "Render_Scene_3D.ipp"
|
||||
|
||||
@@ -20,7 +20,9 @@ struct Render_Scene_3D::Private : Prev_Private {
|
||||
using Render_Run = Render_Result (*)(Root*, Frame_3D*);
|
||||
using Callback_Run = void (*)(Root*, Frame_Callback);
|
||||
using Active_Run = void (*)(Root*, bool);
|
||||
Frame_Statistics_Accumulator frame_statistics{}; /* 完成线程在 Scene 状态发布临界区内更新。 */
|
||||
struct Dispatch {
|
||||
void (*reset_statistics)(Root*);
|
||||
Render_Run render; /* 执行 CPU 图并异步提交 Paint。 */
|
||||
Callback_Run set_frame_callback; /* 安装最终完成帧回调。 */
|
||||
Active_Run set_active; /* 修改最终 Scene 的活动属性。 */
|
||||
@@ -168,16 +170,24 @@ void Render_Scene_3D::Private::initialize_backend(
|
||||
const auto initial = parameters(object);
|
||||
backend = std::make_shared<detail::Async_Render_Backend>(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<Render_Scene_3D::Base_Tag,
|
||||
&State::frame_statistics>(
|
||||
[this, frame](State_Access<typename Object::State> states) {
|
||||
states.template get<Render_Scene_3D::Base_Tag>()
|
||||
.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 <Attached Object> 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<Object*>(root); return static_cast<typename Object::Private&>(*object->d).render(object, frame); }, [](Root* root, Frame_Callback callback) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); std::lock_guard lock(data.render_mutex); data.frame_callback = std::move(callback); }, [](Root* root, bool active) { static_cast<Object*>(root)->template set<&Prop::view_active>(active); }}; return value; }
|
||||
template <Attached Object> const Render_Scene_3D::Private::Dispatch& Render_Scene_3D::Private::dispatch_for() { static const Dispatch value{[](Root* root) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); object->template publish_state<Render_Scene_3D::Base_Tag, &State::frame_statistics>([&data](State_Access<typename Object::State> states) { data.frame_statistics.reset(); states.template get<Render_Scene_3D::Base_Tag>().frame_statistics = {}; }); }, [](Root* root, Frame_3D* frame) { auto* object = static_cast<Object*>(root); return static_cast<typename Object::Private&>(*object->d).render(object, frame); }, [](Root* root, Frame_Callback callback) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); std::lock_guard lock(data.render_mutex); data.frame_callback = std::move(callback); }, [](Root* root, bool active) { static_cast<Object*>(root)->template set<&Prop::view_active>(active); }}; return value; }
|
||||
template <Attached Object> void Render_Scene_3D::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for<Object>(); }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <render_2D/plottable/Plottables.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <numbers>
|
||||
@@ -27,6 +28,7 @@ public:
|
||||
struct Data_Generator {
|
||||
nlohmann::json schema;
|
||||
std::function<nlohmann::json(const nlohmann::json&)> generate;
|
||||
std::function<void(const nlohmann::json&, const Plot_Render_Tick&)> advance;
|
||||
explicit operator bool() const noexcept { return static_cast<bool>(generate); }
|
||||
};
|
||||
Scene_View_Model(std::vector<std::unique_ptr<detail::Renderable_Descriptor>> 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<const nlohmann::json>(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<std::unique_ptr<detail::Renderable_Descriptor>> descriptors;
|
||||
std::function<void(const Plot_Render_Tick&, bool)> update_scene;
|
||||
Data_Generator data_generator;
|
||||
bool generated_data_active{}; /* true 后保留用户生成的数据,不再用演示输入覆盖;viewport 更新仍持续。 */
|
||||
std::atomic<std::shared_ptr<const nlohmann::json>> generated_data{}; /* 成功生成后发布不可变参数;渲染任务按同一配置持续产生压力数据。 */
|
||||
std::tuple<Owned_Objects...> objects;
|
||||
};
|
||||
template <auto Member, structive::Fixed_String Key, structive::Fixed_String Description>
|
||||
@@ -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 <typename Object>
|
||||
@@ -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<Definition, Spectrum>) {
|
||||
const auto count = generator_count(input, "sample_count");
|
||||
const auto [minimum, maximum] = generator_range(input, "power_min", "power_max");
|
||||
std::vector<Plot_Value> 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_Tag>() = Spectrum_Frame{std::move(values)};
|
||||
object.template mark_dirty<Prepare_Data_Tag>();
|
||||
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<std::vector<Plot_Value>> blocks(block_count, std::vector<Plot_Value>(width));
|
||||
std::vector<Plot_Value> 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<Afterglow_Stream_Tag>(
|
||||
std::make_shared<const std::vector<Plot_Value>>(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<Plot_Value> 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<Plot_Time_Tick>(row), std::move(row_values)});
|
||||
}
|
||||
object.template set<&Waterfall::Prop::frequency_bin_count>(width);
|
||||
@@ -418,7 +431,27 @@ std::unique_ptr<Plot::Scene_View> make_scene_view(
|
||||
std::move(components),
|
||||
std::move(update),
|
||||
typename Scene_View_Model<std::remove_cvref_t<Owned_Objects>...>::Data_Generator{
|
||||
generator_2d_schema<Definition>(), [&object](const nlohmann::json& input) { return generate_2d_data(object, input); }},
|
||||
generator_2d_schema<Definition>(),
|
||||
[&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>(owned_objects)...);
|
||||
}
|
||||
std::unique_ptr<Frequency_Axis_Object> make_frequency_axis() {
|
||||
@@ -542,7 +575,7 @@ std::shared_ptr<Plot> 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<Plot>(std::move(scene), std::move(view));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "Gallery_Video_Stream.hpp"
|
||||
#include "Sliding_Statistics.hpp"
|
||||
#include <frame_statistics.hpp>
|
||||
#include "detail/Gallery_Frame_Atlas.hpp"
|
||||
#include "detail/Gallery_Frame_Clock.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -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<nlohmann::json> diagnostics{};
|
||||
} state{};
|
||||
|
||||
std::vector<Source> sources{}; /* 已按业务标识排序的稳定图集来源。 */
|
||||
std::unique_ptr<detail::Gallery_Frame_Atlas> atlas{}; /* 最近完成帧与 RGBA 图集的唯一状态源。 */
|
||||
@@ -72,11 +89,6 @@ struct Gallery_Video_Stream::Private {
|
||||
std::uint64_t metric_clock_start{}; /* 指标窗口起点的累计已分发 tick 数。 */
|
||||
std::vector<std::uint64_t> metric_completion_starts{}; /* 指标窗口起点各 Plot 逻辑完成回调数。 */
|
||||
std::vector<std::uint64_t> 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<Plot_Entry> 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<Gallery_Video_Stream> 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<void>(state.compose_ms.submit(
|
||||
std::chrono::duration<double, std::milli>(
|
||||
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<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - started).count();
|
||||
metric_encode_samples.submit(encode_time);
|
||||
static_cast<void>(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<void>(state.publish_ms.submit(
|
||||
std::chrono::duration<double, std::milli>(
|
||||
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},
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -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<char, AV_ERROR_MAX_STRING_SIZE> 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),
|
||||
|
||||
+158
-466
@@ -1,7 +1,7 @@
|
||||
#include "Plot.hpp"
|
||||
#include "Renderable_Adapter.hpp"
|
||||
#include "Sliding_Statistics.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <magic_enum/magic_enum.hpp>
|
||||
#include <render_2D/plottable/Plottables.hpp>
|
||||
#include <render_3D/Render_3D.hpp>
|
||||
#include <render_3D/Gpu_Completion_State.hpp>
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
@@ -33,43 +34,11 @@ using Scene_3D = Impl<Render_Scene_3D>;
|
||||
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<std::pair<std::string_view, double>> 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<const std::pair<std::string_view, double>> view() const {
|
||||
return {values.data(), size};
|
||||
}
|
||||
private:
|
||||
std::array<std::pair<std::string_view, double>, 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<double, std::milli>(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<Frame_Pacing_Mode> 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<Frame_Pacing_Mode>(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<std::string> 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<std::string> 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<Frame_Statistic>()) {
|
||||
if (statistic == Frame_Statistic::count) continue;
|
||||
const auto& value =
|
||||
state.values[static_cast<std::size_t>(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<std::string> value_supported_formats,
|
||||
const Frame_Pacing_Properties& value_pacing, bool value_is_3d) {
|
||||
constexpr auto marker_count = static_cast<std::size_t>(Frame_Trace_Marker::count);
|
||||
constexpr auto measurement_count = static_cast<std::size_t>(Frame_Trace_Measurement::count);
|
||||
std::array<std::optional<double>, marker_count> markers{};
|
||||
for (const auto& point : frame.trace_points())
|
||||
markers[static_cast<std::size_t>(point.marker)] =
|
||||
static_cast<double>(point.elapsed_ns) / 1'000'000.0;
|
||||
const auto marker = [&](Frame_Trace_Marker value) {
|
||||
return markers[static_cast<std::size_t>(value)].value_or(0.0);
|
||||
};
|
||||
const auto interval = [&](Frame_Trace_Marker first, Frame_Trace_Marker last) {
|
||||
const auto start = markers[static_cast<std::size_t>(first)];
|
||||
const auto finish = markers[static_cast<std::size_t>(last)];
|
||||
return start && finish ? std::max(0.0, *finish - *start) : 0.0;
|
||||
};
|
||||
std::array<double, measurement_count> measurements{};
|
||||
const auto trace_measurements = frame.trace_values();
|
||||
for (const auto& value : trace_measurements)
|
||||
measurements[static_cast<std::size_t>(value.measurement)] =
|
||||
static_cast<double>(value.value_ns) / 1'000'000.0;
|
||||
const auto measurement = [&](Frame_Trace_Measurement value) {
|
||||
return measurements[static_cast<std::size_t>(value)];
|
||||
};
|
||||
Statistic_Input_Batch values{
|
||||
{"server_completion_ms", marker(Frame_Trace_Marker::frame_ready)},
|
||||
{"payload_megabytes", static_cast<double>(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<double>(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<std::pair<std::string_view, double>, 5> values{{
|
||||
{"input_admission_ms", observation.admission_ms},
|
||||
{"input_scene_wait_ms", observation.scene_wait_ms},
|
||||
{"input_dispatch_ms", observation.dispatch_ms},
|
||||
{"input_server_consume_ms", observation.server_consume_ms},
|
||||
{"input_coalesced_event_count", static_cast<double>(observation.coalesced_event_count)}}};
|
||||
input_values.submit(values);
|
||||
}
|
||||
|
||||
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<double>(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<double>(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<double>(gpu.in_flight) /
|
||||
static_cast<double>(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<double>(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<double>(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 <typename Scene_Object>
|
||||
void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) {
|
||||
@@ -598,7 +292,6 @@ struct Plot::Private {
|
||||
std::atomic<std::shared_ptr<const std::string>> 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<Managed_Frame, scene_frame_capacity> 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<const Plot_Stream_Frame> frame) noexcept;
|
||||
void consume_tick();
|
||||
void consume_tick(std::weak_ptr<Plot> 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<Plot_Render_Tick> 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<Plot> lifetime) {
|
||||
std::optional<Plot_Render_Tick> 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<int>(tick.width), static_cast<int>(tick.height)});
|
||||
const auto result = (*scene_2d)->render(&output);
|
||||
collect_consumed_input_statistics();
|
||||
if (result != Render_Scene_2D::Render_Result::completed)
|
||||
rollback_unsubmitted();
|
||||
if (!result) rollback_unsubmitted();
|
||||
return;
|
||||
}
|
||||
auto& output = *std::get<std::unique_ptr<Frame_3D>>(managed->frame);
|
||||
@@ -799,7 +498,6 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
|
||||
auto& scene_3d = std::get<std::unique_ptr<Scene_3D>>(scene);
|
||||
scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{tick.width, tick.height});
|
||||
const auto result = scene_3d->render(&output);
|
||||
collect_consumed_input_statistics();
|
||||
if (result == Render_Scene_3D::Render_Result::submitted) return;
|
||||
rollback_unsubmitted();
|
||||
if (result == Render_Scene_3D::Render_Result::backend_unavailable)
|
||||
@@ -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<aethera::Scene_Event_Stream_Tag>(
|
||||
[&](std::span<const aethera::Scene::Event_Pointer> 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<const Web_Input_Event*>(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<const std::vector<std::byte>> 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<std::unique_ptr<Frame_2D>>(&managed->frame)) {
|
||||
const auto image = (*frame_2d)->image();
|
||||
width = static_cast<std::uint32_t>(image.width);
|
||||
height = static_cast<std::uint32_t>(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<const std::vector<std::byte>>(
|
||||
@@ -916,8 +576,6 @@ void Plot::Private::queue_completed_frame(Render_Frame* frame) {
|
||||
else {
|
||||
auto& frame_3d =
|
||||
std::get<std::unique_ptr<Frame_3D>>(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<std::string> supported_formats;
|
||||
const bool is_3d = std::holds_alternative<std::unique_ptr<Frame_3D>>(
|
||||
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<const Plot_Stream_Frame>(
|
||||
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<std::size_t>(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<decltype(scene)>;
|
||||
if constexpr (std::same_as<Scene_Pointer, std::unique_ptr<Scene_2D>>) {
|
||||
scene->template access_state<Render_Scene_2D::Base_Tag>(
|
||||
read_statistics);
|
||||
} else {
|
||||
is_3d = true;
|
||||
scene->template access_state<Render_Scene_3D::Base_Tag>(
|
||||
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<std::size_t>(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<double>(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<double>(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
#include "Sliding_Statistics.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <mutex>
|
||||
#include <numeric>
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace aethera::web {
|
||||
namespace {
|
||||
double percentile(const std::vector<double>& sorted, double ratio) {
|
||||
if (sorted.empty()) return 0.0;
|
||||
const auto index = static_cast<std::size_t>(std::ceil(
|
||||
ratio * static_cast<double>(sorted.size()))) - 1U;
|
||||
return sorted[std::min(index, sorted.size() - 1U)];
|
||||
}
|
||||
|
||||
double range_average(const std::vector<double>& values,
|
||||
std::size_t first, std::size_t last) {
|
||||
if (first >= last) return 0.0;
|
||||
return std::accumulate(values.begin() + static_cast<std::ptrdiff_t>(first),
|
||||
values.begin() + static_cast<std::ptrdiff_t>(last), 0.0) /
|
||||
static_cast<double>(last - first);
|
||||
}
|
||||
}
|
||||
|
||||
struct Sliding_Statistics::Private {
|
||||
explicit Private(std::size_t value_capacity)
|
||||
: values(value_capacity) {}
|
||||
|
||||
mutable std::mutex mutex;
|
||||
std::vector<double> values; /* 预分配的固定容量滑动窗口。 */
|
||||
std::size_t size{}; /* 当前窗口中的有效样本数量。 */
|
||||
std::size_t next{}; /* 下一次覆盖写入的位置。 */
|
||||
};
|
||||
|
||||
Sliding_Statistics::Sliding_Statistics(std::size_t capacity)
|
||||
: d(std::make_unique<Private>(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<double> 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<double>(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::string, std::unique_ptr<Sliding_Statistics>, std::less<>> values;
|
||||
};
|
||||
|
||||
Sliding_Statistics_Set::Sliding_Statistics_Set(std::size_t capacity)
|
||||
: d(std::make_unique<Private>(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<const std::pair<std::string_view, double>> 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<Sliding_Statistics>(d->capacity)).first;
|
||||
found->second->submit(value);
|
||||
}
|
||||
}
|
||||
|
||||
void Sliding_Statistics_Set::reset() {
|
||||
std::lock_guard lock(d->mutex);
|
||||
d->values.clear();
|
||||
}
|
||||
|
||||
std::vector<Named_Statistic_Snapshot> Sliding_Statistics_Set::snapshot() const {
|
||||
std::vector<Named_Statistic_Snapshot> 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;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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<Private> 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<const std::pair<std::string_view, double>> values);
|
||||
void reset();
|
||||
[[nodiscard]] std::vector<Named_Statistic_Snapshot> snapshot() const;
|
||||
|
||||
private:
|
||||
struct Private;
|
||||
std::unique_ptr<Private> d;
|
||||
};
|
||||
}
|
||||
@@ -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<rtc::Track> 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<double>(
|
||||
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<rtc::Track> 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<double>(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<double>(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<double>(published.queued_byte_count) /
|
||||
(1024.0 * 1024.0)},
|
||||
{"sent_frame_count", snapshot.sent_frame_count},
|
||||
{"sent_megabytes", static_cast<double>(snapshot.sent_byte_count) /
|
||||
{"sent_frame_count", published.sent_frame_count},
|
||||
{"sent_megabytes", static_cast<double>(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<double>(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<double>(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<double>(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<double>(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 {
|
||||
|
||||
@@ -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::function<void(const drogon::HttpResponsePtr&)>>(
|
||||
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::function<void(const drogon::HttpResponsePtr&)>>(
|
||||
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::function<void(const drogon::HttpResponsePtr&)>>(
|
||||
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)
|
||||
|
||||
@@ -1,38 +1,55 @@
|
||||
#include "web_server/src/Sliding_Statistics.hpp"
|
||||
#include <frame_statistics.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <limits>
|
||||
|
||||
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<double>(value));
|
||||
static_cast<void>(statistics.submit(static_cast<double>(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<double>::infinity());
|
||||
statistics.submit(3.0);
|
||||
statistics.submit(4.0);
|
||||
static_cast<void>(statistics.submit(1.0));
|
||||
static_cast<void>(statistics.submit(2.0));
|
||||
static_cast<void>(statistics.submit(std::numeric_limits<double>::infinity()));
|
||||
static_cast<void>(statistics.submit(3.0));
|
||||
static_cast<void>(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<void>(statistics.submit(static_cast<double>(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);
|
||||
}
|
||||
}
|
||||
|
||||
+93
-64
@@ -32,7 +32,7 @@ type Pixel_Format = "rgba8" | "bgra8_premultiplied";
|
||||
type Frame_Stage_Values = Record<string, number>;
|
||||
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<Plot_Diagnostics>;
|
||||
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)}>
|
||||
<header className="cardDragHandle">
|
||||
<div><span className="eyebrow">绘图组件 · {plot.dimension}</span><h2>{plot_labels[plot.id] ?? plot.title}</h2></div>
|
||||
<header>
|
||||
<div className="cardTitle"><span className="cardDragHandle" aria-label="拖动卡片" title="拖动卡片">⠿</span><div><span className="eyebrow">绘图组件 · {plot.dimension}</span><h2>{plot_labels[plot.id] ?? plot.title}</h2></div></div>
|
||||
<div className="cardRuntime" title={`帧序号 ${metrics?.sequence ?? 0} · 创建于 ${generated_time}`}>
|
||||
<div><span className="status">{{IDLE: "已停止", CONNECTING: "重连中", LIVE: "实时", OFFLINE: "已离线"}[status]}</span><span className="framePolicy">{metrics?.delivery === "diagnostics" ? "无像素传输" : metrics ? enum_label(metrics.pacing_mode) : "等待策略"}</span></div>
|
||||
<div className="frameMetrics">
|
||||
@@ -1276,6 +1305,7 @@ function load_workspace_model() {
|
||||
|
||||
export function App() {
|
||||
const [plots, set_plots] = useState<Plot[]>([]); const [category, set_category] = useState("全部"); const [selected, set_selected] = useState<Plot | null>(null);
|
||||
use_selected_plot_diagnostics(selected);
|
||||
const gallery_videos = use_gallery_videos(plots);
|
||||
const [execution_policies, set_execution_policies] = useState<Plot_Execution_Policies>({});
|
||||
const [schema, set_schema] = useState<Schema | null>(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)}`, {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user