优化
This commit is contained in:
@@ -81,6 +81,7 @@ std::unique_ptr<detail::Renderable_Descriptor> make_renderable_component(
|
||||
using Tag = typename Definition::Base_Tag;
|
||||
using State = typename Definition::State;
|
||||
using Adapter = detail::Renderable_Adapter<Object, Fields...,
|
||||
detail::Prop_Field<&Renderable_2D::Prop::cache_enabled, "cache_enabled", "Whether this renderable reuses its complete color result across unchanged frames.">,
|
||||
detail::State_Field<Tag, &State::prepare_dirty, "prepare_dirty", "Whether source changes require the prepare stage to run again.">,
|
||||
detail::State_Field<Tag, &State::paint_dirty, "paint_dirty", "Whether prepared visual data requires the paint stage to run again.">,
|
||||
detail::State_Field<Tag, &State::prepare_executed, "prepare_executed", "Whether the prepare stage executed during the latest scene cycle.">,
|
||||
@@ -281,7 +282,8 @@ nlohmann::json generate_2d_data(Object& object, const Json& input) {
|
||||
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);
|
||||
object.update_samples(values);
|
||||
object.template pending_buffer<Spectrum_Frame_Tag>() = Spectrum_Frame{std::move(values)};
|
||||
object.template mark_dirty<Prepare_Data_Tag>();
|
||||
generated_count = count;
|
||||
} else if constexpr (std::same_as<Definition, Frequency_Trace>) {
|
||||
const auto count = generator_count(input, "sample_count");
|
||||
@@ -570,7 +572,9 @@ std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
|
||||
+ 42.0 * std::exp(-260.0 * std::pow(x - 0.68, 2.0))
|
||||
+ 2.5 * std::sin(i * 0.31 + event.time_milliseconds * 0.004);
|
||||
}
|
||||
raw->update_samples(samples);
|
||||
raw->template pending_buffer<Spectrum_Frame_Tag>() =
|
||||
Spectrum_Frame{std::vector<Spectrum_Power>(samples.begin(), samples.end())};
|
||||
raw->template mark_dirty<Prepare_Data_Tag>();
|
||||
};
|
||||
auto view = make_scene_view<
|
||||
Prop_Field<&Spectrum::Prop::center_frequency, "center_frequency", "Frequency placed at the visual center of the spectrum axis.">,
|
||||
|
||||
@@ -338,8 +338,7 @@ public:
|
||||
generated.push_back(std::move(item));
|
||||
}
|
||||
}
|
||||
if (visual_->update_items(std::move(generated)) != Definition::Update_Items_Result::updated)
|
||||
throw std::invalid_argument("generated items were rejected by the Visual validator");
|
||||
visual_->template set<&Definition::Prop::items>(std::move(generated));
|
||||
return {{"success", true}, {"generated_count", count}};
|
||||
} catch (const std::exception& error) {
|
||||
return {{"success", false}, {"error", error.what()}};
|
||||
@@ -617,8 +616,7 @@ struct Spectrogram_Data_Generator {
|
||||
parameters = next;
|
||||
auto mesh = spectrogram_mesh(parameters);
|
||||
const auto vertex_count = mesh.size();
|
||||
if (visual.update_items(std::move(mesh)) != Mesh_Visual::Update_Items_Result::updated)
|
||||
throw std::invalid_argument("generated spectrogram mesh was rejected");
|
||||
visual.template set<&Mesh_Visual::Prop::items>(std::move(mesh));
|
||||
plot::Axis_Descriptor time_axis{{0.0, parameters.time_span_seconds},
|
||||
plot::Axis_Scale::time, "Time", "s", 6, 1, true, true, true};
|
||||
plot::Axis_Descriptor frequency_axis{{parameters.minimum_frequency_hz,
|
||||
@@ -644,8 +642,7 @@ struct Spectrogram_Data_Generator {
|
||||
request.sequence % parameters.update_every_n_frames != 0) return;
|
||||
const double animation_seconds = request.time_milliseconds / 1000.0 * parameters.animation_speed;
|
||||
auto mesh = spectrogram_mesh(parameters, animation_seconds);
|
||||
if (visual.update_items(std::move(mesh)) != Mesh_Visual::Update_Items_Result::updated)
|
||||
throw std::logic_error("animated spectrogram mesh was rejected");
|
||||
visual.template set<&Mesh_Visual::Prop::items>(std::move(mesh));
|
||||
if (markers == nullptr) return;
|
||||
auto items = markers->template read_prop<Marker_Visual::Base_Tag>().items;
|
||||
for (auto& marker : items) {
|
||||
@@ -654,8 +651,7 @@ struct Spectrogram_Data_Generator {
|
||||
marker.position.z = -1.0F + 2.0F * spectrogram_level(
|
||||
time, frequency, animation_seconds, parameters.ridge_count);
|
||||
}
|
||||
if (markers->update_items(std::move(items)) != Marker_Visual::Update_Items_Result::updated)
|
||||
throw std::logic_error("surface-attached spectrogram markers were rejected");
|
||||
markers->template set<&Marker_Visual::Prop::items>(std::move(items));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "Gallery_Video_Stream.hpp"
|
||||
#include "Sliding_Statistics.hpp"
|
||||
#include "detail/Gallery_Frame_Atlas.hpp"
|
||||
#include "detail/Gallery_Frame_Clock.hpp"
|
||||
#include <asio/post.hpp>
|
||||
@@ -9,7 +10,6 @@
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <mutex>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -38,20 +38,6 @@ std::string exception_description(const std::exception_ptr& failure) {
|
||||
return "empty gallery video failure";
|
||||
}
|
||||
|
||||
double percentile(std::vector<double> values, double quantile) {
|
||||
if (values.empty()) return 0.0;
|
||||
std::ranges::sort(values);
|
||||
const auto index = static_cast<std::size_t>(std::llround(
|
||||
quantile * static_cast<double>(values.size() - 1U)));
|
||||
return values[std::min(index, values.size() - 1U)];
|
||||
}
|
||||
|
||||
double average(const std::vector<double>& values) {
|
||||
return values.empty()
|
||||
? 0.0
|
||||
: std::accumulate(values.begin(), values.end(), 0.0) /
|
||||
static_cast<double>(values.size());
|
||||
}
|
||||
}
|
||||
|
||||
struct Gallery_Video_Stream::Private {
|
||||
@@ -88,9 +74,11 @@ 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 真实画面数。 */
|
||||
std::vector<double> metric_compose_samples{}; /* 当前窗口图集快照合成耗时,单位毫秒。 */
|
||||
std::vector<double> metric_encode_samples{}; /* 当前窗口实际硬件编码耗时,单位毫秒。 */
|
||||
std::vector<double> metric_publish_samples{}; /* 当前窗口 WebRTC 发送调用耗时,单位毫秒。 */
|
||||
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 请求只复制该快照。 */
|
||||
|
||||
Private(asio::any_io_executor value_clock_executor,
|
||||
asio::any_io_executor encoding_executor,
|
||||
@@ -131,14 +119,14 @@ struct Gallery_Video_Stream::Private {
|
||||
}
|
||||
|
||||
void publish(std::optional<Encoded_Video_Frame> video,
|
||||
std::string metadata) noexcept {
|
||||
if (!video && metadata.empty()) return;
|
||||
std::string notification) noexcept {
|
||||
if (!video && notification.empty()) return;
|
||||
try {
|
||||
const auto handler = consumer_handler();
|
||||
if (!handler) return;
|
||||
try {
|
||||
(*handler)(Gallery_Stream_Frame{
|
||||
std::move(video), std::move(metadata)});
|
||||
std::move(video), std::move(notification)});
|
||||
return;
|
||||
}
|
||||
catch (...) {}
|
||||
@@ -212,7 +200,7 @@ struct Gallery_Video_Stream::Private {
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string make_metrics(
|
||||
void update_metrics(
|
||||
const Plot_Render_Tick& tick,
|
||||
const detail::Gallery_Atlas_Composition& composition,
|
||||
std::size_t encoded_bytes,
|
||||
@@ -228,13 +216,10 @@ struct Gallery_Video_Stream::Private {
|
||||
metric_rendered_starts[slot] =
|
||||
composition.sources[slot].rendered_frame_count;
|
||||
}
|
||||
metric_compose_samples.clear();
|
||||
metric_encode_samples.clear();
|
||||
metric_publish_samples.clear();
|
||||
return {};
|
||||
return;
|
||||
}
|
||||
const auto elapsed = now - metric_started;
|
||||
if (elapsed < metric_interval) return {};
|
||||
if (elapsed < metric_interval) return;
|
||||
const double seconds = std::chrono::duration<double>(elapsed).count();
|
||||
const auto clock_total =
|
||||
delivered_clock_ticks.load(std::memory_order_relaxed);
|
||||
@@ -266,6 +251,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();
|
||||
auto output = nlohmann::json{
|
||||
{"kind", "gallery_metrics"},
|
||||
{"protocol", "aethera.gallery.video"},
|
||||
@@ -279,12 +267,14 @@ struct Gallery_Video_Stream::Private {
|
||||
static_cast<double>(clock_total - metric_clock_start) / seconds},
|
||||
{"encoded_frame_rate_fps",
|
||||
static_cast<double>(encoded_frame_count - metric_encoded_start) / seconds},
|
||||
{"compose_average_ms", average(metric_compose_samples)},
|
||||
{"compose_p95_ms", percentile(metric_compose_samples, 0.95)},
|
||||
{"encode_average_ms", average(metric_encode_samples)},
|
||||
{"encode_p95_ms", percentile(metric_encode_samples, 0.95)},
|
||||
{"publish_average_ms", average(metric_publish_samples)},
|
||||
{"publish_p95_ms", percentile(metric_publish_samples, 0.95)},
|
||||
{"compose_average_ms", compose.average},
|
||||
{"compose_p95_ms", compose.p95},
|
||||
{"encode_average_ms", encode.average},
|
||||
{"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}}},
|
||||
{"encoded_bytes", encoded_bytes},
|
||||
{"fresh_tiles", composition.fresh_tile_count},
|
||||
{"missing_tiles", composition.missing_tile_count},
|
||||
@@ -292,14 +282,14 @@ struct Gallery_Video_Stream::Private {
|
||||
{"skipped_encode_ticks",
|
||||
skipped_encode_ticks.load(std::memory_order_relaxed)},
|
||||
{"sources", std::move(source_metrics)}
|
||||
}.dump();
|
||||
};
|
||||
metric_started = now;
|
||||
metric_encoded_start = encoded_frame_count;
|
||||
metric_clock_start = clock_total;
|
||||
metric_compose_samples.clear();
|
||||
metric_encode_samples.clear();
|
||||
metric_publish_samples.clear();
|
||||
return output;
|
||||
{
|
||||
std::lock_guard lock(diagnostics_mutex);
|
||||
latest_diagnostics = output;
|
||||
}
|
||||
}
|
||||
|
||||
void encode_latest(std::weak_ptr<Gallery_Video_Stream> lifetime) {
|
||||
@@ -319,7 +309,7 @@ struct Gallery_Video_Stream::Private {
|
||||
try {
|
||||
const auto compose_started = std::chrono::steady_clock::now();
|
||||
auto composition = atlas->compose();
|
||||
metric_compose_samples.push_back(
|
||||
metric_compose_samples.submit(
|
||||
std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - compose_started)
|
||||
.count());
|
||||
@@ -331,17 +321,17 @@ 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.push_back(encode_time);
|
||||
metric_encode_samples.submit(encode_time);
|
||||
if (video) {
|
||||
++encoded_frame_count;
|
||||
const auto encoded_bytes = video->annex_b.size();
|
||||
const auto encoder_backend = video->backend;
|
||||
auto metadata = make_metrics(
|
||||
update_metrics(
|
||||
tick, composition, encoded_bytes, encoder_backend);
|
||||
const auto publish_started =
|
||||
std::chrono::steady_clock::now();
|
||||
publish(std::move(*video), std::move(metadata));
|
||||
metric_publish_samples.push_back(
|
||||
publish(std::move(*video), {});
|
||||
metric_publish_samples.submit(
|
||||
std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - publish_started)
|
||||
.count());
|
||||
@@ -527,4 +517,12 @@ std::string Gallery_Video_Stream::layout_description() const {
|
||||
{"plots", std::move(plots)}
|
||||
}.dump();
|
||||
}
|
||||
|
||||
nlohmann::json Gallery_Video_Stream::diagnostics() const {
|
||||
std::lock_guard lock(d->diagnostics_mutex);
|
||||
if (!d->latest_diagnostics.is_null()) return d->latest_diagnostics;
|
||||
return {{"kind", "gallery_metrics"},
|
||||
{"protocol", "aethera.gallery.video"}, {"version", 2},
|
||||
{"sources", nlohmann::json::object()}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -12,7 +13,7 @@
|
||||
namespace aethera::web {
|
||||
struct Gallery_Stream_Frame {
|
||||
std::optional<Encoded_Video_Frame> video{}; /* 页面唯一 H.264 图集 access unit;交付时转移所有权。 */
|
||||
std::string metadata{}; /* 低频图集编码和来源完整性指标。 */
|
||||
std::string notification{}; /* 仅承载终止错误等低频控制通知。 */
|
||||
};
|
||||
|
||||
class Gallery_Video_Stream final
|
||||
@@ -41,6 +42,7 @@ public:
|
||||
void request_video_key_frame();
|
||||
void shutdown() noexcept;
|
||||
[[nodiscard]] std::string layout_description() const;
|
||||
[[nodiscard]] nlohmann::json diagnostics() const;
|
||||
|
||||
private:
|
||||
struct Private;
|
||||
|
||||
@@ -94,9 +94,9 @@ void Gallery_WebSocket::deliver(
|
||||
fail_media(std::current_exception());
|
||||
}
|
||||
}
|
||||
if (!frame.metadata.empty()) {
|
||||
if (!frame.notification.empty()) {
|
||||
try {
|
||||
connection->send(std::move(frame.metadata),
|
||||
connection->send(std::move(frame.notification),
|
||||
drogon::WebSocketMessageType::Text);
|
||||
}
|
||||
catch (...) {}
|
||||
|
||||
@@ -57,36 +57,28 @@ void Graph_WebSocket::start() {
|
||||
|
||||
void Graph_WebSocket::deliver_frame(
|
||||
std::shared_ptr<const Plot_Stream_Frame> frame) {
|
||||
if (!frame || frame->metadata.empty() ||
|
||||
if (!frame || frame->notification.empty() ||
|
||||
!d->attached.load(std::memory_order_acquire)) return;
|
||||
const auto connection = d->connection.lock();
|
||||
if (connection && connection->connected()) {
|
||||
try {
|
||||
connection->send(frame->metadata,
|
||||
connection->send(frame->notification,
|
||||
drogon::WebSocketMessageType::Text);
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
}
|
||||
|
||||
void Graph_WebSocket::deliver_input_observation(
|
||||
void Graph_WebSocket::deliver_input_completion(
|
||||
Plot_Input_Observation observation) {
|
||||
if (!d->attached.load(std::memory_order_acquire)) return;
|
||||
const auto connection = d->connection.lock();
|
||||
if (!connection || !connection->connected()) return;
|
||||
const nlohmann::json message{
|
||||
{"kind", "input_observation"},
|
||||
{"kind", "input_completion"},
|
||||
{"protocol", "aethera.input.latency"},
|
||||
{"version", 1},
|
||||
{"version", 2},
|
||||
{"sequence", observation.sequence},
|
||||
{"event_type", magic_enum::enum_name(observation.type)},
|
||||
{"stage", magic_enum::enum_name(observation.stage)},
|
||||
{"plot_queue_ms", observation.plot_queue_ms},
|
||||
{"scene_queue_ms", observation.scene_queue_ms},
|
||||
{"backend_queue_ms", observation.backend_queue_ms},
|
||||
{"frame_completion_ms", observation.frame_completion_ms},
|
||||
{"server_total_ms", observation.server_total_ms},
|
||||
{"target_frame_sequence", observation.target_frame_sequence},
|
||||
{"rendered_frame_sequence", observation.rendered_frame_sequence},
|
||||
{"coalesced_event_count", observation.coalesced_event_count}
|
||||
};
|
||||
@@ -180,7 +172,7 @@ void Graph_WebSocket::receive(std::string_view message) {
|
||||
std::move(decoded),
|
||||
[weak](Plot_Input_Observation observation) {
|
||||
if (const auto socket = weak.lock())
|
||||
socket->deliver_input_observation(std::move(observation));
|
||||
socket->deliver_input_completion(std::move(observation));
|
||||
});
|
||||
}
|
||||
catch (const nlohmann::json::exception&) {}
|
||||
|
||||
@@ -19,7 +19,7 @@ public:
|
||||
void close() noexcept;
|
||||
private:
|
||||
void deliver_frame(std::shared_ptr<const Plot_Stream_Frame> frame);
|
||||
void deliver_input_observation(Plot_Input_Observation observation);
|
||||
void deliver_input_completion(Plot_Input_Observation observation);
|
||||
struct Private;
|
||||
std::unique_ptr<Private> d;
|
||||
};
|
||||
|
||||
+287
-51
@@ -1,8 +1,8 @@
|
||||
#include "Plot.hpp"
|
||||
#include "Renderable_Adapter.hpp"
|
||||
#include "Sliding_Statistics.hpp"
|
||||
#include <asio/post.hpp>
|
||||
#include <asio/strand.hpp>
|
||||
#include <magic_enum/magic_enum.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <render_2D/plottable/Plottables.hpp>
|
||||
#include <render_3D/Render_3D.hpp>
|
||||
@@ -13,10 +13,12 @@
|
||||
#include <cmath>
|
||||
#include <concepts>
|
||||
#include <exception>
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
@@ -29,8 +31,27 @@ using namespace render_2d;
|
||||
using namespace render_3d;
|
||||
using Scene_2D = Impl<Render_Scene_2D>;
|
||||
using Scene_3D = Impl<Render_Scene_3D>;
|
||||
constexpr std::uint16_t frame_protocol_version{9};
|
||||
constexpr std::uint64_t diagnostic_sample_period{100};
|
||||
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) {
|
||||
@@ -200,41 +221,248 @@ nlohmann::json Frame_Policy::write_prop(std::string_view key,
|
||||
return {{"success", false}, {"error", "unknown frame runtime property"}};
|
||||
}
|
||||
|
||||
nlohmann::json frame_metadata(Render_Frame& frame, std::uint32_t width,
|
||||
std::uint32_t height, std::size_t pixel_bytes,
|
||||
Frame_Identity rendered_identity,
|
||||
std::string_view output_format,
|
||||
std::string_view native_format,
|
||||
nlohmann::json supported_formats,
|
||||
const Frame_Pacing_Properties& pacing) {
|
||||
nlohmann::json markers = nlohmann::json::object();
|
||||
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;
|
||||
}
|
||||
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[std::string(magic_enum::enum_name(point.marker))] = point.elapsed_ns;
|
||||
nlohmann::json measurements = nlohmann::json::object();
|
||||
for (const auto& value : frame.trace_values())
|
||||
measurements[std::string(magic_enum::enum_name(value.measurement))] = value.value_ns;
|
||||
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>, 6> values{{
|
||||
{"input_plot_queue_ms", observation.plot_queue_ms},
|
||||
{"input_scene_queue_ms", observation.scene_queue_ms},
|
||||
{"input_backend_queue_ms", observation.backend_queue_ms},
|
||||
{"input_frame_completion_ms", observation.frame_completion_ms},
|
||||
{"input_server_total_ms", observation.server_total_ms},
|
||||
{"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;
|
||||
return {
|
||||
{"kind", "frame_metadata"}, {"protocol", "aethera.video.frame"},
|
||||
{"version", frame_protocol_version}, {"sequence", identity.sequence},
|
||||
{"correlation_id", identity.correlation_id},
|
||||
{"rendered_sequence", rendered_identity.sequence},
|
||||
{"rendered_correlation_id", rendered_identity.correlation_id},
|
||||
{"created_time_unix_ms", static_cast<double>(frame.created_time_unix_ns()) / 1'000'000.0},
|
||||
{"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"},
|
||||
{"pixel", {{"width", width}, {"height", height}, {"format", output_format},
|
||||
{"native_format", native_format},
|
||||
{"supported_formats", std::move(supported_formats)},
|
||||
{"byte_length", pixel_bytes}}},
|
||||
{"video", {{"codec", "H264"},
|
||||
{"transport", "shared WebRTC atlas"}}},
|
||||
{"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}}},
|
||||
{"trace", {{"clock", "steady_elapsed_ns"}, {"markers", std::move(markers)},
|
||||
{"measurements", std::move(measurements)}}}
|
||||
};
|
||||
{"frame_statistics", std::move(frame_statistics)},
|
||||
{"input_statistics", std::move(input_statistics)}};
|
||||
}
|
||||
|
||||
template <typename Scene_Object>
|
||||
@@ -329,6 +557,7 @@ 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 借用的稳定三缓冲物理帧。 */
|
||||
@@ -377,7 +606,7 @@ void Plot::Private::fail(std::exception_ptr failure) noexcept {
|
||||
Plot_Stream_Frame{nlohmann::json{
|
||||
{"kind", "plot_error"},
|
||||
{"protocol", "aethera.video.frame"},
|
||||
{"version", frame_protocol_version},
|
||||
{"version", plot_stream_protocol_version},
|
||||
{"message", *description}
|
||||
}.dump(), {}});
|
||||
publish(std::move(output));
|
||||
@@ -587,6 +816,7 @@ void Plot::Private::complete_event_reports(Frame_Identity rendered_identity) {
|
||||
observation.rendered_frame_sequence = rendered_identity.sequence;
|
||||
observation.coalesced_event_count =
|
||||
metadata.input.coalesced_event_count;
|
||||
diagnostics.submit_input(observation);
|
||||
try {
|
||||
if (metadata.handler) metadata.handler(std::move(observation));
|
||||
}
|
||||
@@ -685,27 +915,25 @@ void Plot::Private::queue_completed_frame(Render_Frame* frame) {
|
||||
width, height});
|
||||
|
||||
frame->mark(Frame_Trace_Marker::frame_ready);
|
||||
std::string metadata;
|
||||
if (identity.sequence == 1 ||
|
||||
identity.sequence % diagnostic_sample_period == 0) {
|
||||
nlohmann::json supported_formats = nlohmann::json::array();
|
||||
if (std::holds_alternative<std::unique_ptr<Frame_2D>>(
|
||||
managed->frame)) {
|
||||
for (const auto format : Frame_2D::supported_pixel_formats)
|
||||
supported_formats.push_back(pixel_format_name(format));
|
||||
}
|
||||
else {
|
||||
for (const auto format : Frame_3D::supported_pixel_formats)
|
||||
supported_formats.push_back(pixel_format_name(format));
|
||||
}
|
||||
metadata = frame_metadata(
|
||||
*frame, width, height,
|
||||
pixels->rgba ? pixels->rgba->size() : 0U,
|
||||
rendered_identity, output_format, native_format,
|
||||
std::move(supported_formats), pacing).dump();
|
||||
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(metadata), std::move(pixels)});
|
||||
Plot_Stream_Frame{{}, std::move(pixels)});
|
||||
publish(std::move(published));
|
||||
retire();
|
||||
}
|
||||
@@ -769,7 +997,7 @@ Plot::Stream_Id Plot::subscribe(Stream_Handler handler) {
|
||||
Plot_Stream_Frame{nlohmann::json{
|
||||
{"kind", "plot_error"},
|
||||
{"protocol", "aethera.video.frame"},
|
||||
{"version", frame_protocol_version},
|
||||
{"version", plot_stream_protocol_version},
|
||||
{"message", failure ? *failure : "Plot unavailable"}
|
||||
}.dump(), {}}));
|
||||
}
|
||||
@@ -925,4 +1153,12 @@ void Plot::async_generate_data(nlohmann::json input, Json_Handler handler) {
|
||||
catch (...) {}
|
||||
});
|
||||
}
|
||||
|
||||
nlohmann::json Plot::diagnostics() const {
|
||||
return d->diagnostics.snapshot();
|
||||
}
|
||||
|
||||
void Plot::reset_diagnostics() {
|
||||
d->diagnostics.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ struct Plot_Pixel_Frame {
|
||||
};
|
||||
|
||||
struct Plot_Stream_Frame {
|
||||
std::string metadata{}; /* 抽样诊断文本;空值表示本帧不产生诊断流量。 */
|
||||
std::string notification{}; /* 仅用于终止错误等低频控制通知;正常帧为空。 */
|
||||
std::shared_ptr<const Plot_Pixel_Frame> pixels{}; /* 每帧完成进度及其可选图集像素。 */
|
||||
};
|
||||
|
||||
@@ -112,6 +112,8 @@ public:
|
||||
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 diagnostics() const;
|
||||
void reset_diagnostics();
|
||||
|
||||
private:
|
||||
struct Private;
|
||||
|
||||
@@ -272,7 +272,8 @@ inline std::string_view protocol_field_label(std::string_view key) {
|
||||
{"type", "星座类型"}, {"point_color", "数据点颜色"}, {"anchor_color", "参考点颜色"},
|
||||
{"points", "星座点数据"}, {"label_font", "标签字体"}, {"label_pen", "标签画笔"},
|
||||
{"selection_brush", "选区画刷"}, {"selection_border_pen", "选区边框画笔"},
|
||||
{"selected_regions", "已选区域"}, {"transform", "空间变换"}, {"visible", "是否可见"},
|
||||
{"selected_regions", "已选区域"}, {"cache_enabled", "启用绘制缓存"},
|
||||
{"transform", "空间变换"}, {"visible", "是否可见"},
|
||||
{"depth_test", "深度测试"}, {"items", "项目数据"},
|
||||
{"prepare_dirty", "准备阶段待更新"}, {"paint_dirty", "绘制阶段待更新"},
|
||||
{"prepare_executed", "准备阶段已执行"}, {"paint_executed", "绘制阶段已执行"},
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#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;
|
||||
};
|
||||
}
|
||||
@@ -150,11 +150,42 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root)
|
||||
{"id", id}, {"title", id}, {"category", "Plots"}, {"description", ""},
|
||||
{"dimension", id.starts_with("datoviz_") ? "3D" : "2D"}, {"websocket", "/ws/plot/" + id},
|
||||
{"media", plot_media->at(id)},
|
||||
{"schema", "/plot/" + id + "/schema"}});
|
||||
{"schema", "/plot/" + id + "/schema"},
|
||||
{"diagnostics", "/plot/" + id + "/diagnostics"}});
|
||||
}
|
||||
callback(json_response(std::move(result)));
|
||||
}, {drogon::Get});
|
||||
|
||||
app.registerHandler("/plot/{1}/diagnostics", [plots](
|
||||
const drogon::HttpRequestPtr& request,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& callback,
|
||||
std::string plot_id) {
|
||||
auto plot = find_plot(*plots, plot_id);
|
||||
if (!plot) {
|
||||
callback(error_response(drogon::k404NotFound, "unknown plot"));
|
||||
return;
|
||||
}
|
||||
if (request->method() == drogon::Delete) {
|
||||
plot->reset_diagnostics();
|
||||
callback(json_response({{"success", true}}));
|
||||
return;
|
||||
}
|
||||
callback(json_response(plot->diagnostics()));
|
||||
}, {drogon::Get, drogon::Delete});
|
||||
|
||||
app.registerHandler("/gallery/{1}/diagnostics", [gallery_streams](
|
||||
const drogon::HttpRequestPtr&,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& callback,
|
||||
std::string group_id) {
|
||||
const auto found = gallery_streams->find(group_id);
|
||||
if (found == gallery_streams->end()) {
|
||||
callback(error_response(drogon::k404NotFound,
|
||||
"unknown gallery media group"));
|
||||
return;
|
||||
}
|
||||
callback(json_response(found->second->diagnostics()));
|
||||
}, {drogon::Get});
|
||||
|
||||
app.registerHandler("/plot/{1}/schema", [plots](
|
||||
const drogon::HttpRequestPtr&,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& callback,
|
||||
|
||||
Reference in New Issue
Block a user