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