仍有bug

This commit is contained in:
2026-08-24 16:27:27 +08:00
parent ca6d8f8e79
commit f463a756fc
27 changed files with 1201 additions and 898 deletions
+43 -10
View File
@@ -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));
}
+35 -23
View File
@@ -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},
+59 -12
View File
@@ -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 QP18
* 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
View File
@@ -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);
}
}
+5 -4
View File
@@ -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();
-143
View File
@@ -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;
}
}
-63
View File
@@ -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;
};
}
+51 -42
View File
@@ -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 {
+17 -15
View File
@@ -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)
+41 -24
View File
@@ -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);
}
}