仍有bug
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user