diff --git a/kernel/main.cmake b/kernel/main.cmake index 4e15ac9..6f9ec79 100644 --- a/kernel/main.cmake +++ b/kernel/main.cmake @@ -52,12 +52,27 @@ if (Aethera_BUILD_TESTS) set(Aethera_Kernel_test_dir "${CMAKE_CURRENT_LIST_DIR}/src/test") append_glob_source(Aethera_Kernel_test_sources "${Aethera_Kernel_test_dir}") add_executable(Aethera_Kernel_exe ${Aethera_Kernel_test_sources}) - target_link_libraries(Aethera_Kernel_exe PRIVATE Aethera_Kernel GTest::gtest benchmark::benchmark) + target_link_libraries(Aethera_Kernel_exe PRIVATE + Aethera_Kernel + GTest::gtest + GTest::gmock) add_test(NAME Aethera_Kernel_exe COMMAND Aethera_Kernel_exe) set_tests_properties(Aethera_Kernel_exe PROPERTIES LABELS "Aethera_Kernel" ENVIRONMENT "Aethera_ERROR_MODE=exception") list(APPEND Aethera_Kernel_test_targets Aethera_Kernel_exe) + add_executable(Aethera_Frame_Policy_Benchmarks + "${CMAKE_CURRENT_LIST_DIR}/src/benchmark/Frame_Policy_Benchmarks.cpp") + target_link_libraries(Aethera_Frame_Policy_Benchmarks PRIVATE + Aethera_Kernel + benchmark::benchmark) + add_custom_target(Aethera_Frame_Policy_benchmark + COMMAND Aethera_Frame_Policy_Benchmarks + --benchmark_min_time=1s + --benchmark_repetitions=3 + --benchmark_report_aggregates_only=true + DEPENDS Aethera_Frame_Policy_Benchmarks + USES_TERMINAL) endif () if (Aethera_BUILD_TESTS) add_custom_target(Aethera_Kernel_check diff --git a/kernel/src/benchmark/Frame_Policy_Benchmarks.cpp b/kernel/src/benchmark/Frame_Policy_Benchmarks.cpp new file mode 100644 index 0000000..75d5d88 --- /dev/null +++ b/kernel/src/benchmark/Frame_Policy_Benchmarks.cpp @@ -0,0 +1,107 @@ +#include "Frame_Policy/Frame_Policy.hpp" +#include "Frame_Policy/Frame_Policy_3D.hpp" +#include +#include +#include +#include +#include + +namespace { + +template +std::unique_ptr build_policy() { + auto built = typename Policy::template Builder{}.build(); + if (!built) throw std::logic_error("benchmark frame policy build failed"); + return std::move(*built); +} + +void frame_policy_request_storm(benchmark::State& state) { + auto policy = build_policy(); + const auto origin = std::chrono::steady_clock::now(); + std::uint64_t request_sequence{}; + constexpr std::uint64_t batch_size = 256; + for (auto _ : state) { + static_cast(_); + for (std::uint64_t index = 0; index < batch_size; ++index) { + ++request_sequence; + policy->submit_request({ + .issued_at = origin + + std::chrono::nanoseconds(request_sequence), + .time_milliseconds = static_cast(request_sequence), + .source = index + 1 == batch_size + ? aethera::Frame_Request_Source::immediate + : aethera::Frame_Request_Source::periodic}); + } + static_cast(policy->consume_events()); + const auto production = policy->begin_production(); + if (!production) { + state.SkipWithError("request storm did not admit one frame"); + break; + } + const auto published_at = std::chrono::steady_clock::now(); + policy->submit_publication_dispatch({ + .started_at = published_at, + .completed_at = published_at, + .sequence = production->sequence, + .asynchronous_feedback_count = 0, + .succeeded = true}); + static_cast(policy->consume_events()); + } + state.SetItemsProcessed(state.iterations() * batch_size); +} + +void frame_policy_slow_sender(benchmark::State& state) { + auto policy = build_policy(); + policy->set_mode(aethera::Frame_Pacing_Mode::maximum_rate); + const auto origin = std::chrono::steady_clock::now(); + policy->submit_request({ + .issued_at = origin, + .source = aethera::Frame_Request_Source::maximum_rate}); + static_cast(policy->consume_events()); + const auto active = policy->begin_production(); + if (!active) { + state.SkipWithError("slow sender setup did not admit a frame"); + return; + } + const auto published_at = std::chrono::steady_clock::now(); + policy->submit_publication_dispatch({ + .started_at = published_at, + .completed_at = published_at, + .sequence = active->sequence, + .asynchronous_feedback_count = 1, + .succeeded = false}); + std::uint64_t request_sequence{}; + constexpr std::uint64_t batch_size = 256; + for (auto _ : state) { + static_cast(_); + for (std::uint64_t index = 0; index < batch_size; ++index) { + ++request_sequence; + policy->submit_request({ + .issued_at = origin + + std::chrono::nanoseconds(request_sequence), + .time_milliseconds = static_cast(request_sequence), + .source = aethera::Frame_Request_Source::maximum_rate}); + } + static_cast(policy->consume_events()); + if (policy->begin_production()) { + state.SkipWithError("delivery window admitted a second frame"); + break; + } + } + policy->submit_publication_feedback({ + .completed_at = std::chrono::steady_clock::now(), + .sequence = active->sequence, + .succeeded = true}); + static_cast(policy->consume_events()); + const auto resumed = policy->begin_production(); + if (!resumed) + state.SkipWithError("delivery completion did not resume production"); + state.SetItemsProcessed(state.iterations() * batch_size); +} + +BENCHMARK(frame_policy_request_storm)->UseRealTime(); +BENCHMARK(frame_policy_slow_sender)->UseRealTime(); + +} + +BENCHMARK_MAIN(); diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy.cpp b/kernel/src/kernel/Frame_Policy/Frame_Policy.cpp index 3405509..4297136 100644 --- a/kernel/src/kernel/Frame_Policy/Frame_Policy.cpp +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy.cpp @@ -1,4 +1,5 @@ #include "Frame_Policy.hpp" +#include "Frame_Policy_Lifecycle.ipp" #include #include #include @@ -37,12 +38,37 @@ void reset_counters(Frame_Policy::State& state, const auto pixel_delivery_enabled = state.pixel_delivery_enabled; const auto mode = state.mode; const auto fixed_rate_fps = state.fixed_rate_fps; + auto pending_request = std::move(state.pending_request); + const auto next_frame_sequence = state.next_frame_sequence; + const auto in_flight_frame_sequence = state.in_flight_frame_sequence; + const auto publication_started_ns = state.publication_started_ns; + const auto publication_feedback_expected = + state.publication_feedback_expected; + const auto publication_feedback_received = + state.publication_feedback_received; + const auto publication_latest_feedback_ns = + state.publication_latest_feedback_ns; + const auto publication_dispatch_known = state.publication_dispatch_known; + const auto publication_dispatch_succeeded = + state.publication_dispatch_succeeded; + const auto publication_feedback_succeeded = + state.publication_feedback_succeeded; state = {}; state.generation = generation; state.render_enabled = render_enabled; state.pixel_delivery_enabled = pixel_delivery_enabled; state.mode = mode; state.fixed_rate_fps = fixed_rate_fps; + state.pending_request = std::move(pending_request); + state.next_frame_sequence = next_frame_sequence; + state.in_flight_frame_sequence = in_flight_frame_sequence; + state.publication_started_ns = publication_started_ns; + state.publication_feedback_expected = publication_feedback_expected; + state.publication_feedback_received = publication_feedback_received; + state.publication_latest_feedback_ns = publication_latest_feedback_ns; + state.publication_dispatch_known = publication_dispatch_known; + state.publication_dispatch_succeeded = publication_dispatch_succeeded; + state.publication_feedback_succeeded = publication_feedback_succeeded; state.observation_started_ns = occurred_ns; state.observed_until_ns = occurred_ns; } @@ -79,33 +105,63 @@ void Frame_Policy::reset_statistics() { submit({.type = detail::Frame_Policy_Event_Type::reset}); } -void Frame_Policy::record_request( - Frame_Request_Source source, - std::chrono::steady_clock::time_point occurred_at) { +void Frame_Policy::submit_request(Frame_Request request) { + if (request.issued_at.time_since_epoch().count() == 0) + request.issued_at = std::chrono::steady_clock::now(); + const auto source = request.source; + const auto occurred_at = request.issued_at; + auto& data = static_cast(*d); + if (!data.requests.enqueue(std::move(request))) throw std::bad_alloc{}; submit({.type = detail::Frame_Policy_Event_Type::request_received, .source = source, .occurred_ns = monotonic_nanoseconds(occurred_at)}); } -void Frame_Policy::record_request_accepted(Frame_Request_Source source) { - submit({.type = detail::Frame_Policy_Event_Type::request_accepted, - .source = source}); +std::optional Frame_Policy::begin_production() { + auto& data = static_cast(*d); + auto& state = static_cast( + double_buffer::detail::Internal_Access::pending_state(this)); + auto production = detail::begin_frame_production(data.requests, state); + publish_state(); + return production; } -void Frame_Policy::record_request_coalesced() { - submit({.type = detail::Frame_Policy_Event_Type::request_coalesced}); +void Frame_Policy::defer_production(Frame_Production production) { + auto& state = static_cast( + double_buffer::detail::Internal_Access::pending_state(this)); + detail::defer_frame_production(state, std::move(production)); + publish_state(); } -void Frame_Policy::record_policy_rejection() { - submit({.type = detail::Frame_Policy_Event_Type::policy_rejected}); +void Frame_Policy::reject_production(std::uint64_t sequence) { + auto& state = static_cast( + double_buffer::detail::Internal_Access::pending_state(this)); + detail::reject_frame_production(state, sequence); + publish_state(); } -void Frame_Policy::record_frame_slot_backpressure() { - submit({.type = detail::Frame_Policy_Event_Type::frame_slot_backpressure}); +void Frame_Policy::submit_publication_dispatch( + Frame_Publication_Dispatch dispatch) { + if (dispatch.started_at.time_since_epoch().count() == 0 || + dispatch.completed_at < dispatch.started_at) + throw std::invalid_argument( + "frame publication requires an ordered dispatch interval"); + submit({.type = detail::Frame_Policy_Event_Type::publication_dispatched, + .sequence = dispatch.sequence, + .occurred_ns = monotonic_nanoseconds(dispatch.started_at), + .duration_ns = monotonic_nanoseconds(dispatch.completed_at), + .count = dispatch.asynchronous_feedback_count, + .enabled = dispatch.succeeded}); } -void Frame_Policy::record_scene_rejection() { - submit({.type = detail::Frame_Policy_Event_Type::scene_rejected}); +void Frame_Policy::submit_publication_feedback( + Frame_Publication_Feedback feedback) { + if (feedback.completed_at.time_since_epoch().count() == 0) + feedback.completed_at = std::chrono::steady_clock::now(); + submit({.type = detail::Frame_Policy_Event_Type::publication_feedback, + .sequence = feedback.sequence, + .occurred_ns = monotonic_nanoseconds(feedback.completed_at), + .enabled = feedback.succeeded}); } void Frame_Policy::record_frame_submitted( @@ -164,21 +220,6 @@ bool Frame_Policy::consume_events() { else if (event.source == Frame_Request_Source::maximum_rate) ++state.maximum_rate_request_count; break; - case detail::Frame_Policy_Event_Type::request_accepted: - ++state.accepted_request_count; - break; - case detail::Frame_Policy_Event_Type::request_coalesced: - ++state.coalesced_request_count; - break; - case detail::Frame_Policy_Event_Type::policy_rejected: - ++state.policy_rejection_count; - break; - case detail::Frame_Policy_Event_Type::frame_slot_backpressure: - ++state.frame_slot_backpressure_count; - break; - case detail::Frame_Policy_Event_Type::scene_rejected: - ++state.scene_rejection_count; - break; case detail::Frame_Policy_Event_Type::frame_submitted: ++state.submitted_frame_count; ++state.active_frame_count; @@ -212,6 +253,16 @@ bool Frame_Policy::consume_events() { state.last_completion_ns = event.occurred_ns; break; } + case detail::Frame_Policy_Event_Type::publication_dispatched: + detail::begin_frame_publication( + state, event.sequence, event.occurred_ns, + event.duration_ns, event.count, event.enabled); + break; + case detail::Frame_Policy_Event_Type::publication_feedback: + detail::observe_publication_feedback( + state, event.sequence, event.occurred_ns, + event.enabled); + break; case detail::Frame_Policy_Event_Type::reset: reset_counters(state, event.occurred_ns); break; diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp b/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp index 1a28bab..68c184e 100644 --- a/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy.hpp @@ -1,7 +1,9 @@ #pragma once #include "double_buffer/model.hpp" #include +#include #include +#include namespace aethera { @@ -18,6 +20,35 @@ enum struct Frame_Request_Source : std::uint8_t { maximum_rate }; +struct Frame_Request { + std::chrono::steady_clock::time_point issued_at{}; /* 请求进入帧策略的单调时刻。 */ + std::uint64_t sequence{}; /* 全局帧时钟关联序号;手动请求为 0。 */ + double time_milliseconds{}; /* 页面单调时间线上的动画时刻。 */ + std::uint32_t width{320}; /* 无媒体消费者时使用的请求宽度。 */ + std::uint32_t height{192}; /* 无媒体消费者时使用的请求高度。 */ + Frame_Request_Source source{Frame_Request_Source::immediate}; /* 请求来源及合并优先级。 */ + bool operator==(const Frame_Request&) const = default; +}; + +struct Frame_Production { + Frame_Request request{}; /* 已通过策略准入、必须完成或显式退回的生产请求。 */ + std::uint64_t sequence{}; /* 帧策略分配的 Plot 单调帧序号。 */ +}; + +struct Frame_Publication_Feedback { + std::chrono::steady_clock::time_point completed_at{}; /* 媒体发布反馈任务实际执行的单调时刻。 */ + std::uint64_t sequence{}; /* 被反馈的策略帧序号。 */ + bool succeeded{}; /* 至少一个媒体传输是否成功接收该帧。 */ +}; + +struct Frame_Publication_Dispatch { + std::chrono::steady_clock::time_point started_at{}; /* Plot 开始分派完成帧的单调时刻。 */ + std::chrono::steady_clock::time_point completed_at{}; /* Plot 完成全部订阅者调用的单调时刻。 */ + std::uint64_t sequence{}; /* 被分派的策略帧序号。 */ + std::size_t asynchronous_feedback_count{}; /* 已接受并承诺异步反馈的媒体任务数。 */ + bool succeeded{}; /* 同步消费者是否至少成功一个。 */ +}; + namespace detail { struct Frame_Policy_Event_Stream; enum struct Frame_Policy_Event_Type : std::uint8_t { @@ -26,13 +57,10 @@ enum struct Frame_Policy_Event_Type : std::uint8_t { mode, fixed_rate, request_received, - request_accepted, - request_coalesced, - policy_rejected, - frame_slot_backpressure, - scene_rejected, frame_submitted, frame_completed, + publication_dispatched, + publication_feedback, reset }; struct Frame_Policy_Event { @@ -42,6 +70,7 @@ struct Frame_Policy_Event { std::uint64_t sequence{}; std::uint64_t occurred_ns{}; std::uint64_t duration_ns{}; + std::uint64_t count{}; double number{}; bool enabled{}; }; @@ -64,6 +93,25 @@ struct Frame_Policy final : double_buffer::Def< Frame_Pacing_Mode mode{Frame_Pacing_Mode::fixed_rate}; double fixed_rate_fps{30.0}; + std::optional pending_request{}; /* 尚未生产时只保留最高优先级的最新请求。 */ + std::uint64_t next_frame_sequence{1}; /* 下一次生产准入分配的单调序号。 */ + std::uint64_t in_flight_frame_sequence{}; /* 非零时帧策略唯一管理的生产到发布在途帧。 */ + std::uint64_t publication_started_ns{}; /* 当前在途帧开始向媒体发布的单调时刻。 */ + std::uint64_t publication_feedback_expected{}; /* 本帧异步媒体发布任务总数。 */ + std::uint64_t publication_feedback_received{}; /* 已由 Taskflow 节点报告的媒体发布任务数。 */ + std::uint64_t publication_latest_feedback_ns{}; /* 最近一次媒体发布反馈完成时刻。 */ + bool publication_dispatch_known{}; /* Plot 分派结果是否已由策略消费者归并。 */ + bool publication_dispatch_succeeded{}; /* Plot 同步分派阶段是否已有成功消费者。 */ + bool publication_feedback_succeeded{}; /* 异步媒体反馈是否已有成功结果。 */ + std::uint64_t latest_publication_dispatch_latency_ns{}; + std::uint64_t publication_dispatch_latency_total_ns{}; + std::uint64_t maximum_publication_dispatch_latency_ns{}; + std::uint64_t published_frame_count{}; /* 至少一次发布成功并完成反馈的累计帧数。 */ + std::uint64_t publication_failed_count{}; /* 没有媒体消费者发布成功的累计帧数。 */ + std::uint64_t latest_publication_latency_ns{}; /* 最近一帧从 Plot 发布到反馈任务的墙钟耗时。 */ + std::uint64_t publication_latency_total_ns{}; + std::uint64_t maximum_publication_latency_ns{}; + std::uint64_t observation_started_ns{}; std::uint64_t observed_until_ns{}; std::uint64_t request_count{}; @@ -95,7 +143,7 @@ struct Frame_Policy final : double_buffer::Def< Frame_Request_Source last_request_source{}; bool operator==(const State&) const = default; }; - struct Private : Prev_Private {}; + struct Private; void set_render_enabled(bool enabled); void set_pixel_delivery_enabled(bool enabled); @@ -103,13 +151,12 @@ struct Frame_Policy final : double_buffer::Def< void set_fixed_rate(double fps); void reset_statistics(); - void record_request(Frame_Request_Source source, - std::chrono::steady_clock::time_point occurred_at); - void record_request_accepted(Frame_Request_Source source); - void record_request_coalesced(); - void record_policy_rejection(); - void record_frame_slot_backpressure(); - void record_scene_rejection(); + void submit_request(Frame_Request request); + [[nodiscard]] std::optional begin_production(); + void defer_production(Frame_Production production); + void reject_production(std::uint64_t sequence); + void submit_publication_dispatch(Frame_Publication_Dispatch dispatch); + void submit_publication_feedback(Frame_Publication_Feedback feedback); void record_frame_submitted( std::uint64_t sequence, Frame_Request_Source source, std::chrono::steady_clock::duration tick_queue_duration); @@ -125,3 +172,5 @@ private: }; } + +#include "Frame_Policy.ipp" diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy.ipp b/kernel/src/kernel/Frame_Policy/Frame_Policy.ipp new file mode 100644 index 0000000..72adcb8 --- /dev/null +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy.ipp @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace aethera { + +struct Frame_Policy::Private : Prev_Private { + moodycamel::ConcurrentQueue requests{}; /* MPMC 请求入口;唯一策略 consumer 归并到 State。 */ +}; + +} diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.cpp b/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.cpp index 8a772c6..68db079 100644 --- a/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.cpp +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.cpp @@ -1,4 +1,5 @@ #include "Frame_Policy_3D.hpp" +#include "Frame_Policy_Lifecycle.ipp" #include #include #include @@ -38,12 +39,37 @@ void reset_common_counters(Frame_Policy::State& state, const auto pixel_delivery_enabled = state.pixel_delivery_enabled; const auto mode = state.mode; const auto fixed_rate_fps = state.fixed_rate_fps; + auto pending_request = std::move(state.pending_request); + const auto next_frame_sequence = state.next_frame_sequence; + const auto in_flight_frame_sequence = state.in_flight_frame_sequence; + const auto publication_started_ns = state.publication_started_ns; + const auto publication_feedback_expected = + state.publication_feedback_expected; + const auto publication_feedback_received = + state.publication_feedback_received; + const auto publication_latest_feedback_ns = + state.publication_latest_feedback_ns; + const auto publication_dispatch_known = state.publication_dispatch_known; + const auto publication_dispatch_succeeded = + state.publication_dispatch_succeeded; + const auto publication_feedback_succeeded = + state.publication_feedback_succeeded; state = {}; state.generation = generation; state.render_enabled = render_enabled; state.pixel_delivery_enabled = pixel_delivery_enabled; state.mode = mode; state.fixed_rate_fps = fixed_rate_fps; + state.pending_request = std::move(pending_request); + state.next_frame_sequence = next_frame_sequence; + state.in_flight_frame_sequence = in_flight_frame_sequence; + state.publication_started_ns = publication_started_ns; + state.publication_feedback_expected = publication_feedback_expected; + state.publication_feedback_received = publication_feedback_received; + state.publication_latest_feedback_ns = publication_latest_feedback_ns; + state.publication_dispatch_known = publication_dispatch_known; + state.publication_dispatch_succeeded = publication_dispatch_succeeded; + state.publication_feedback_succeeded = publication_feedback_succeeded; state.observation_started_ns = occurred_ns; state.observed_until_ns = occurred_ns; } @@ -81,33 +107,65 @@ void Frame_Policy_3D::reset_statistics() { submit({.type = detail::Frame_Policy_Event_Type::reset}); } -void Frame_Policy_3D::record_request( - Frame_Request_Source source, - std::chrono::steady_clock::time_point occurred_at) { +void Frame_Policy_3D::submit_request(Frame_Request request) { + if (request.issued_at.time_since_epoch().count() == 0) + request.issued_at = std::chrono::steady_clock::now(); + const auto source = request.source; + const auto occurred_at = request.issued_at; + auto& data = static_cast(*d); + if (!data.requests.enqueue(std::move(request))) throw std::bad_alloc{}; submit({.type = detail::Frame_Policy_Event_Type::request_received, .source = source, .occurred_ns = monotonic_nanoseconds_3d(occurred_at)}); } -void Frame_Policy_3D::record_request_accepted(Frame_Request_Source source) { - submit({.type = detail::Frame_Policy_Event_Type::request_accepted, - .source = source}); +std::optional Frame_Policy_3D::begin_production() { + auto& data = static_cast(*d); + auto& state = static_cast( + double_buffer::detail::Internal_Access::pending_state(this)); + auto production = detail::begin_frame_production( + data.requests, state.common); + publish_state(); + return production; } -void Frame_Policy_3D::record_request_coalesced() { - submit({.type = detail::Frame_Policy_Event_Type::request_coalesced}); +void Frame_Policy_3D::defer_production(Frame_Production production) { + auto& state = static_cast( + double_buffer::detail::Internal_Access::pending_state(this)); + detail::defer_frame_production( + state.common, std::move(production)); + publish_state(); } -void Frame_Policy_3D::record_policy_rejection() { - submit({.type = detail::Frame_Policy_Event_Type::policy_rejected}); +void Frame_Policy_3D::reject_production(std::uint64_t sequence) { + auto& state = static_cast( + double_buffer::detail::Internal_Access::pending_state(this)); + detail::reject_frame_production(state.common, sequence); + publish_state(); } -void Frame_Policy_3D::record_frame_slot_backpressure() { - submit({.type = detail::Frame_Policy_Event_Type::frame_slot_backpressure}); +void Frame_Policy_3D::submit_publication_dispatch( + Frame_Publication_Dispatch dispatch) { + if (dispatch.started_at.time_since_epoch().count() == 0 || + dispatch.completed_at < dispatch.started_at) + throw std::invalid_argument( + "3D frame publication requires an ordered dispatch interval"); + submit({.type = detail::Frame_Policy_Event_Type::publication_dispatched, + .sequence = dispatch.sequence, + .occurred_ns = monotonic_nanoseconds_3d(dispatch.started_at), + .duration_ns = monotonic_nanoseconds_3d(dispatch.completed_at), + .count = dispatch.asynchronous_feedback_count, + .enabled = dispatch.succeeded}); } -void Frame_Policy_3D::record_scene_rejection() { - submit({.type = detail::Frame_Policy_Event_Type::scene_rejected}); +void Frame_Policy_3D::submit_publication_feedback( + Frame_Publication_Feedback feedback) { + if (feedback.completed_at.time_since_epoch().count() == 0) + feedback.completed_at = std::chrono::steady_clock::now(); + submit({.type = detail::Frame_Policy_Event_Type::publication_feedback, + .sequence = feedback.sequence, + .occurred_ns = monotonic_nanoseconds_3d(feedback.completed_at), + .enabled = feedback.succeeded}); } void Frame_Policy_3D::record_frame_submitted( @@ -203,21 +261,6 @@ bool Frame_Policy_3D::consume_events() { else if (event.source == Frame_Request_Source::maximum_rate) ++common.maximum_rate_request_count; break; - case detail::Frame_Policy_Event_Type::request_accepted: - ++common.accepted_request_count; - break; - case detail::Frame_Policy_Event_Type::request_coalesced: - ++common.coalesced_request_count; - break; - case detail::Frame_Policy_Event_Type::policy_rejected: - ++common.policy_rejection_count; - break; - case detail::Frame_Policy_Event_Type::frame_slot_backpressure: - ++common.frame_slot_backpressure_count; - break; - case detail::Frame_Policy_Event_Type::scene_rejected: - ++common.scene_rejection_count; - break; case detail::Frame_Policy_Event_Type::frame_submitted: ++common.submitted_frame_count; ++common.active_frame_count; @@ -253,6 +296,16 @@ bool Frame_Policy_3D::consume_events() { common.last_completion_ns = event.occurred_ns; break; } + case detail::Frame_Policy_Event_Type::publication_dispatched: + detail::begin_frame_publication( + common, event.sequence, event.occurred_ns, + event.duration_ns, event.count, event.enabled); + break; + case detail::Frame_Policy_Event_Type::publication_feedback: + detail::observe_publication_feedback( + common, event.sequence, event.occurred_ns, + event.enabled); + break; case detail::Frame_Policy_Event_Type::reset: reset_common_counters(common, event.occurred_ns); state.overlapped_release_count = 0; diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.hpp b/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.hpp index 2c91e1d..8d94233 100644 --- a/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.hpp +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.hpp @@ -23,7 +23,7 @@ struct Frame_Policy_3D final : double_buffer::Def< std::uint64_t last_completed_sequence{}; /* 最近退休的帧序号;未退休时为 0。 */ bool operator==(const State&) const = default; }; - struct Private : Prev_Private {}; + struct Private; static constexpr std::size_t pipeline_capacity{3}; void set_render_enabled(bool enabled); @@ -32,13 +32,12 @@ struct Frame_Policy_3D final : double_buffer::Def< void set_fixed_rate(double fps); void reset_statistics(); - void record_request(Frame_Request_Source source, - std::chrono::steady_clock::time_point occurred_at); - void record_request_accepted(Frame_Request_Source source); - void record_request_coalesced(); - void record_policy_rejection(); - void record_frame_slot_backpressure(); - void record_scene_rejection(); + void submit_request(Frame_Request request); + [[nodiscard]] std::optional begin_production(); + void defer_production(Frame_Production production); + void reject_production(std::uint64_t sequence); + void submit_publication_dispatch(Frame_Publication_Dispatch dispatch); + void submit_publication_feedback(Frame_Publication_Feedback feedback); void record_frame_submitted( std::uint64_t sequence, Frame_Request_Source source, std::chrono::steady_clock::duration tick_queue_duration); @@ -53,3 +52,5 @@ private: void submit(detail::Frame_Policy_Event event); }; } + +#include "Frame_Policy_3D.ipp" diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.ipp b/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.ipp new file mode 100644 index 0000000..14a2a39 --- /dev/null +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy_3D.ipp @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace aethera { + +struct Frame_Policy_3D::Private : Prev_Private { + moodycamel::ConcurrentQueue requests{}; /* MPMC 请求入口;唯一策略 consumer 归并到公共 State。 */ +}; + +} diff --git a/kernel/src/kernel/Frame_Policy/Frame_Policy_Lifecycle.ipp b/kernel/src/kernel/Frame_Policy/Frame_Policy_Lifecycle.ipp new file mode 100644 index 0000000..a6afdf1 --- /dev/null +++ b/kernel/src/kernel/Frame_Policy/Frame_Policy_Lifecycle.ipp @@ -0,0 +1,158 @@ +#pragma once +#include +#include + +namespace aethera::detail { + +inline int frame_request_priority(Frame_Request_Source source) noexcept { + switch (source) { + case Frame_Request_Source::unspecified: + case Frame_Request_Source::periodic: + return 0; + case Frame_Request_Source::maximum_rate: + return 1; + case Frame_Request_Source::immediate: + return 2; + } + return 0; +} + +inline void merge_frame_request(Frame_Policy::State& state, + Frame_Request request) { + if (!state.pending_request) { + state.pending_request = std::move(request); + return; + } + ++state.coalesced_request_count; + const auto current_priority = frame_request_priority( + state.pending_request->source); + const auto next_priority = frame_request_priority(request.source); + if (current_priority > next_priority || + (current_priority == next_priority && + state.pending_request->issued_at >= request.issued_at)) + return; + state.pending_request = std::move(request); +} + +inline bool frame_request_matches(const Frame_Policy::State& state, + Frame_Request_Source source) noexcept { + return state.render_enabled && + (source == Frame_Request_Source::immediate || + (source == Frame_Request_Source::periodic && + state.mode == Frame_Pacing_Mode::fixed_rate) || + (source == Frame_Request_Source::maximum_rate && + state.mode == Frame_Pacing_Mode::maximum_rate)); +} + +template +std::optional begin_frame_production( + Queue& requests, Frame_Policy::State& state) { + Frame_Request request; + while (requests.try_dequeue(request)) + merge_frame_request(state, std::move(request)); + if (state.in_flight_frame_sequence != 0 || !state.pending_request) + return std::nullopt; + if (!frame_request_matches(state, state.pending_request->source)) { + state.pending_request.reset(); + ++state.policy_rejection_count; + return std::nullopt; + } + Frame_Production production{ + std::move(*state.pending_request), state.next_frame_sequence++}; + state.pending_request.reset(); + state.in_flight_frame_sequence = production.sequence; + ++state.accepted_request_count; + return production; +} + +inline void defer_frame_production(Frame_Policy::State& state, + Frame_Production production) { + if (production.sequence == 0 || + state.in_flight_frame_sequence != production.sequence) + throw std::logic_error( + "deferred frame production does not own the delivery slot"); + state.in_flight_frame_sequence = 0; + ++state.frame_slot_backpressure_count; + merge_frame_request(state, std::move(production.request)); +} + +inline void reject_frame_production(Frame_Policy::State& state, + std::uint64_t sequence) { + if (sequence == 0 || state.in_flight_frame_sequence != sequence) + throw std::logic_error( + "rejected frame production does not own the delivery slot"); + state.in_flight_frame_sequence = 0; + ++state.scene_rejection_count; +} + +inline void finish_frame_publication(Frame_Policy::State& state, + std::uint64_t completed_ns) noexcept { + const bool succeeded = state.publication_dispatch_succeeded || + state.publication_feedback_succeeded; + if (succeeded) ++state.published_frame_count; + else ++state.publication_failed_count; + const auto latency = completed_ns >= state.publication_started_ns + ? completed_ns - state.publication_started_ns + : 0U; + state.latest_publication_latency_ns = latency; + state.publication_latency_total_ns += latency; + state.maximum_publication_latency_ns = std::max( + state.maximum_publication_latency_ns, latency); + state.in_flight_frame_sequence = 0; + state.publication_started_ns = 0; + state.publication_feedback_expected = 0; + state.publication_feedback_received = 0; + state.publication_latest_feedback_ns = 0; + state.publication_dispatch_known = false; + state.publication_dispatch_succeeded = false; + state.publication_feedback_succeeded = false; +} + +inline void begin_frame_publication( + Frame_Policy::State& state, + std::uint64_t sequence, + std::uint64_t started_ns, + std::uint64_t completed_ns, + std::size_t asynchronous_feedback_count, + bool dispatch_succeeded) { + if (sequence == 0 || state.in_flight_frame_sequence != sequence) + throw std::logic_error( + "frame publication does not own the in-flight policy frame"); + if (state.publication_dispatch_known) + throw std::logic_error("frame publication is already started"); + state.publication_started_ns = started_ns; + state.publication_feedback_expected = asynchronous_feedback_count; + state.publication_dispatch_known = true; + state.publication_dispatch_succeeded = dispatch_succeeded; + const auto dispatch_latency = completed_ns >= started_ns + ? completed_ns - started_ns + : 0U; + state.latest_publication_dispatch_latency_ns = dispatch_latency; + state.publication_dispatch_latency_total_ns += dispatch_latency; + state.maximum_publication_dispatch_latency_ns = std::max( + state.maximum_publication_dispatch_latency_ns, dispatch_latency); + if (state.publication_feedback_received >= + state.publication_feedback_expected) + finish_frame_publication( + state, std::max(completed_ns, + state.publication_latest_feedback_ns)); +} + +inline void observe_publication_feedback( + Frame_Policy::State& state, + std::uint64_t sequence, + std::uint64_t completed_ns, + bool succeeded) noexcept { + if (sequence == 0 || state.in_flight_frame_sequence != sequence) + return; + state.publication_feedback_succeeded |= succeeded; + ++state.publication_feedback_received; + state.publication_latest_feedback_ns = std::max( + state.publication_latest_feedback_ns, completed_ns); + if (state.publication_dispatch_known && + state.publication_feedback_received >= + state.publication_feedback_expected) + finish_frame_publication(state, completed_ns); +} + +} diff --git a/kernel/src/test/Frame_Policy_3D_Test.cpp b/kernel/src/test/Frame_Policy_3D_Test.cpp index da4f00f..b12fcf7 100644 --- a/kernel/src/test/Frame_Policy_3D_Test.cpp +++ b/kernel/src/test/Frame_Policy_3D_Test.cpp @@ -41,16 +41,18 @@ TEST(frame_policy_3d, common_reset_resets_completed_gpu_statistics) { const auto now = std::chrono::steady_clock::now(); policy->set_mode(aethera::Frame_Pacing_Mode::maximum_rate); - policy->record_request( - aethera::Frame_Request_Source::maximum_rate, now); - policy->record_request_accepted( - aethera::Frame_Request_Source::maximum_rate); + policy->submit_request(aethera::Frame_Request{ + .issued_at = now, + .source = aethera::Frame_Request_Source::maximum_rate}); + static_cast(policy->consume_events()); + const auto production = policy->begin_production(); + ASSERT_TRUE(production.has_value()); policy->record_frame_submitted( - 31, aethera::Frame_Request_Source::maximum_rate, + production->sequence, aethera::Frame_Request_Source::maximum_rate, std::chrono::microseconds{40}); - EXPECT_TRUE(policy->consume_events()); + EXPECT_FALSE(policy->consume_events()); policy->record_frame_completed( - 31, std::chrono::microseconds{90}, false); + production->sequence, std::chrono::microseconds{90}, false); const auto& completed = policy->read_state< aethera::Frame_Policy_3D::Base_Tag>(); diff --git a/kernel/src/test/Frame_Policy_Lifecycle_Test.cpp b/kernel/src/test/Frame_Policy_Lifecycle_Test.cpp new file mode 100644 index 0000000..2d346f4 --- /dev/null +++ b/kernel/src/test/Frame_Policy_Lifecycle_Test.cpp @@ -0,0 +1,208 @@ +#include "Frame_Policy/Frame_Policy.hpp" +#include "Frame_Policy/Frame_Policy_3D.hpp" +#include +#include +#include +#include +#include + +namespace { + +using namespace std::chrono_literals; + +class Publication_Task { +public: + MOCK_METHOD(bool, publish, (std::uint64_t sequence)); +}; + +template +std::unique_ptr build_policy() { + auto built = typename Policy::template Builder{}.build(); + if (!built) throw std::logic_error("test frame policy build failed"); + return std::move(*built); +} + +template +const aethera::Frame_Policy::State& common_state(const Policy& policy) { + if constexpr (std::same_as) + return policy.template read_state(); + else + return policy.template read_state().common; +} + +TEST(frame_policy_lifecycle, + newest_interaction_owns_the_only_production_admission) { + auto policy = build_policy(); + const auto origin = std::chrono::steady_clock::now(); + policy->submit_request({ + .issued_at = origin, + .source = aethera::Frame_Request_Source::periodic}); + policy->submit_request({ + .issued_at = origin + 1ms, + .source = aethera::Frame_Request_Source::maximum_rate}); + policy->submit_request({ + .issued_at = origin + 2ms, + .time_milliseconds = 2.0, + .source = aethera::Frame_Request_Source::immediate}); + policy->submit_request({ + .issued_at = origin + 3ms, + .source = aethera::Frame_Request_Source::periodic}); + static_cast(policy->consume_events()); + + const auto production = policy->begin_production(); + ASSERT_TRUE(production.has_value()); + EXPECT_EQ(production->request.source, + aethera::Frame_Request_Source::immediate); + EXPECT_DOUBLE_EQ(production->request.time_milliseconds, 2.0); + const auto& state = common_state(*policy); + EXPECT_EQ(state.request_count, 4u); + EXPECT_EQ(state.coalesced_request_count, 3u); + EXPECT_EQ(state.accepted_request_count, 1u); + EXPECT_EQ(state.in_flight_frame_sequence, production->sequence); + EXPECT_FALSE(state.pending_request.has_value()); +} + +TEST(frame_policy_lifecycle, + production_stays_closed_until_publication_task_reports_feedback) { + auto policy = build_policy(); + Publication_Task publication; + const auto origin = std::chrono::steady_clock::now(); + policy->submit_request({ + .issued_at = origin, + .source = aethera::Frame_Request_Source::periodic}); + static_cast(policy->consume_events()); + const auto first = policy->begin_production(); + ASSERT_TRUE(first.has_value()); + + policy->record_frame_submitted( + first->sequence, first->request.source, 10us); + policy->record_frame_completed(first->sequence, 20us); + policy->submit_request({ + .issued_at = origin + 1ms, + .source = aethera::Frame_Request_Source::immediate}); + static_cast(policy->consume_events()); + EXPECT_FALSE(policy->begin_production().has_value()); + + policy->submit_publication_dispatch({ + .started_at = origin + 2ms, + .completed_at = origin + 2500us, + .sequence = first->sequence, + .asynchronous_feedback_count = 1, + .succeeded = false}); + EXPECT_CALL(publication, publish(first->sequence)) + .WillOnce(::testing::Return(true)); + policy->submit_publication_feedback({ + .completed_at = origin + 3ms, + .sequence = first->sequence, + .succeeded = publication.publish(first->sequence)}); + static_cast(policy->consume_events()); + + const auto second = policy->begin_production(); + ASSERT_TRUE(second.has_value()); + EXPECT_EQ(second->request.source, + aethera::Frame_Request_Source::immediate); + const auto& state = common_state(*policy); + EXPECT_EQ(state.published_frame_count, 1u); + EXPECT_EQ(state.latest_publication_dispatch_latency_ns, 500'000u); + EXPECT_EQ(state.latest_publication_latency_ns, 1'000'000u); +} + +TEST(frame_policy_lifecycle, + early_taskflow_feedback_is_reconciled_after_dispatch_registration) { + auto policy = build_policy(); + const auto origin = std::chrono::steady_clock::now(); + policy->submit_request({ + .issued_at = origin, + .source = aethera::Frame_Request_Source::immediate}); + static_cast(policy->consume_events()); + const auto production = policy->begin_production(); + ASSERT_TRUE(production.has_value()); + + policy->submit_publication_feedback({ + .completed_at = origin + 2ms, + .sequence = production->sequence, + .succeeded = true}); + policy->submit_publication_dispatch({ + .started_at = origin + 1ms, + .completed_at = origin + 1ms, + .sequence = production->sequence, + .asynchronous_feedback_count = 1, + .succeeded = false}); + static_cast(policy->consume_events()); + + const auto& state = common_state(*policy); + EXPECT_EQ(state.in_flight_frame_sequence, 0u); + EXPECT_EQ(state.published_frame_count, 1u); + EXPECT_EQ(state.latest_publication_latency_ns, 1'000'000u); +} + +TEST(frame_policy_lifecycle, + slow_sender_coalesces_request_storm_without_expanding_delivery_window) { + auto policy = build_policy(); + policy->set_mode(aethera::Frame_Pacing_Mode::maximum_rate); + const auto origin = std::chrono::steady_clock::now(); + policy->submit_request({ + .issued_at = origin, + .source = aethera::Frame_Request_Source::maximum_rate}); + static_cast(policy->consume_events()); + const auto active = policy->begin_production(); + ASSERT_TRUE(active.has_value()); + policy->submit_publication_dispatch({ + .started_at = origin, + .completed_at = origin, + .sequence = active->sequence, + .asynchronous_feedback_count = 1, + .succeeded = false}); + + constexpr std::uint64_t request_count = 10'000; + for (std::uint64_t index = 0; index < request_count; ++index) { + policy->submit_request({ + .issued_at = origin + std::chrono::nanoseconds(index + 1), + .time_milliseconds = static_cast(index), + .source = index + 1 == request_count + ? aethera::Frame_Request_Source::immediate + : aethera::Frame_Request_Source::maximum_rate}); + } + static_cast(policy->consume_events()); + EXPECT_FALSE(policy->begin_production().has_value()); + const auto& blocked = common_state(*policy); + ASSERT_TRUE(blocked.pending_request.has_value()); + EXPECT_EQ(blocked.pending_request->source, + aethera::Frame_Request_Source::immediate); + EXPECT_EQ(blocked.in_flight_frame_sequence, active->sequence); + EXPECT_EQ(blocked.coalesced_request_count, request_count - 1); + + policy->submit_publication_feedback({ + .completed_at = origin + std::chrono::milliseconds{1}, + .sequence = active->sequence, + .succeeded = true}); + static_cast(policy->consume_events()); + const auto resumed = policy->begin_production(); + ASSERT_TRUE(resumed.has_value()); + EXPECT_EQ(resumed->request.source, + aethera::Frame_Request_Source::immediate); +} + +TEST(frame_policy_lifecycle, + deferred_physical_slot_returns_the_same_request_to_policy) { + auto policy = build_policy(); + const auto issued_at = std::chrono::steady_clock::now(); + policy->submit_request({ + .issued_at = issued_at, + .time_milliseconds = 17.0, + .source = aethera::Frame_Request_Source::periodic}); + static_cast(policy->consume_events()); + auto production = policy->begin_production(); + ASSERT_TRUE(production.has_value()); + const auto first_sequence = production->sequence; + policy->defer_production(std::move(*production)); + + const auto retried = policy->begin_production(); + ASSERT_TRUE(retried.has_value()); + EXPECT_GT(retried->sequence, first_sequence); + EXPECT_EQ(retried->request.issued_at, issued_at); + EXPECT_DOUBLE_EQ(retried->request.time_milliseconds, 17.0); + EXPECT_EQ(common_state(*policy).frame_slot_backpressure_count, 1u); +} + +} diff --git a/kernel/src/test/render_test.cpp b/kernel/src/test/render_test.cpp index a745686..ec19edf 100644 --- a/kernel/src/test/render_test.cpp +++ b/kernel/src/test/render_test.cpp @@ -60,13 +60,16 @@ TEST(frame_policy, mpmc_events_publish_one_double_buffer_state) { policy->set_mode(aethera::Frame_Pacing_Mode::maximum_rate); policy->set_fixed_rate(47.5); policy->set_pixel_delivery_enabled(false); - policy->record_request(aethera::Frame_Request_Source::maximum_rate, - std::chrono::steady_clock::now()); - policy->record_request_accepted(aethera::Frame_Request_Source::maximum_rate); + policy->submit_request(aethera::Frame_Request{ + .issued_at = std::chrono::steady_clock::now(), + .source = aethera::Frame_Request_Source::maximum_rate}); + static_cast(policy->consume_events()); + const auto production = policy->begin_production(); + ASSERT_TRUE(production.has_value()); policy->record_frame_submitted( - 7, aethera::Frame_Request_Source::maximum_rate, + production->sequence, aethera::Frame_Request_Source::maximum_rate, std::chrono::microseconds{250}); - EXPECT_TRUE(policy->consume_events()); + EXPECT_FALSE(policy->consume_events()); const auto& state = policy->read_state(); EXPECT_TRUE(state.render_enabled); @@ -76,6 +79,7 @@ TEST(frame_policy, mpmc_events_publish_one_double_buffer_state) { EXPECT_EQ(state.request_count, 1); EXPECT_EQ(state.accepted_request_count, 1); EXPECT_EQ(state.submitted_frame_count, 1); + EXPECT_EQ(state.in_flight_frame_sequence, production->sequence); EXPECT_EQ(state.latest_tick_queue_ns, 250'000); EXPECT_THROW(policy->set_fixed_rate(0.0), std::invalid_argument); } diff --git a/mcp/core/Control_Service.cpp b/mcp/core/Control_Service.cpp index 15d7a1d..7d24875 100644 --- a/mcp/core/Control_Service.cpp +++ b/mcp/core/Control_Service.cpp @@ -137,7 +137,7 @@ template if (!plot) return Tool_Call_Output{ Tool_Call_Result::unknown_plot, {}, "unknown plot"}; - plot->schedule_render(web::Plot_Render_Tick{ + plot->schedule_render(Frame_Request{ .issued_at = std::chrono::steady_clock::now(), .time_milliseconds = request.time_milliseconds, .width = request.width, diff --git a/mcp/core/runtime/Gallery_Plots_2D.cpp b/mcp/core/runtime/Gallery_Plots_2D.cpp index d5a3713..b576348 100644 --- a/mcp/core/runtime/Gallery_Plots_2D.cpp +++ b/mcp/core/runtime/Gallery_Plots_2D.cpp @@ -27,13 +27,13 @@ public: struct Data_Generator { nlohmann::json schema; std::function generate; - std::function advance; + std::function advance; explicit operator bool() const noexcept { return static_cast(generate); } }; Scene_View_Model(std::vector> value_descriptors, - std::function value_update, + std::function value_update, Data_Generator value_data_generator, Owned_Objects... owned_objects) : descriptors(std::move(value_descriptors)), update_scene(std::move(value_update)), @@ -85,14 +85,14 @@ public: std::memory_order_release); return result; } - void update(const Plot_Render_Tick& request) override { + void update(const Frame_Request& request) override { 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> descriptors; - std::function update_scene; + std::function update_scene; Data_Generator data_generator; std::atomic> generated_data{}; /* 成功生成后发布不可变参数;渲染任务按同一配置持续产生压力数据。 */ std::tuple objects; @@ -420,7 +420,7 @@ template std::unique_ptr make_scene_view( Object & object, Scene_2D & scene, - std::function < void(const Plot_Render_Tick &, bool) > update, + std::function < void(const Frame_Request &, bool) > update, Owned_Objects &&... owned_objects) { using Definition = typename Object::Attached_Object; using Tag = typename Definition::Base_Tag; @@ -456,7 +456,7 @@ std::unique_ptr make_scene_view( return generate_2d_data(object, input); }, [&object](const nlohmann::json& input, - const Plot_Render_Tick& tick) { + const Frame_Request& tick) { const auto interval = generator_count( input, "update_every_n_frames", 100'000); if (tick.sequence % interval != 0) return; @@ -555,7 +555,7 @@ std::shared_ptr make_axes_plot() { auto update = [scene = not_null{scene.get()}, frequency = not_null{frequency.get()}, numeric = not_null{numeric.get()}, - time = not_null{time.get()}](const Plot_Render_Tick& event, bool) { + time = not_null{time.get()}](const Frame_Request& event, bool) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, numeric, time); constexpr double day_milliseconds = 86'400'000.0; time->append_time(Time_Of_Day{ @@ -630,7 +630,7 @@ std::shared_ptr make_spectrum_plot() { auto update = [scene = not_null{scene.get()}, raw = not_null{spectrum.get()}, frequency = not_null{frequency.get()}, - vertical = not_null{vertical.get()}](const Plot_Render_Tick& event, bool demo_data) { + vertical = not_null{vertical.get()}](const Frame_Request& event, bool demo_data) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); if (!demo_data) return; std::array < double, 256 > samples{}; @@ -695,7 +695,7 @@ std::shared_ptr make_frequency_trace_plot() { auto update = [scene = not_null{scene.get()}, raw = not_null{trace.get()}, time = not_null{time.get()}, - vertical = not_null{vertical.get()}](const Plot_Render_Tick& event, bool demo_data) { + vertical = not_null{vertical.get()}](const Frame_Request& event, bool demo_data) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, time, vertical); if (!demo_data) return; constexpr double day_milliseconds = 86'400'000.0; @@ -739,7 +739,7 @@ std::shared_ptr make_sweep_spectrum_plot() { auto update = [scene = not_null{scene.get()}, raw = not_null{sweep.get()}, frequency = not_null{frequency.get()}, - vertical = not_null{vertical.get()}](const Plot_Render_Tick& event, bool demo_data) { + vertical = not_null{vertical.get()}](const Frame_Request& event, bool demo_data) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); if (!demo_data) return; const auto& state = raw->template read_prop(); @@ -793,7 +793,7 @@ std::shared_ptr make_afterglow_plot() { auto update = [scene = not_null{scene.get()}, raw = not_null{afterglow.get()}, frequency = not_null{frequency.get()}, - vertical = not_null{vertical.get()}](const Plot_Render_Tick& event, bool demo_data) { + vertical = not_null{vertical.get()}](const Frame_Request& event, bool demo_data) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); if (!demo_data) return; std::array < double, 192 > values{}; @@ -841,7 +841,7 @@ std::shared_ptr make_waterfall_plot() { auto update = [scene = not_null{scene.get()}, raw = not_null{waterfall.get()}, frequency = not_null{frequency.get()}, - time = not_null{time.get()}](const Plot_Render_Tick& event, bool demo_data) { + time = not_null{time.get()}](const Frame_Request& event, bool demo_data) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, time); if (!demo_data) return; constexpr double day_milliseconds = 86'400'000.0; @@ -900,7 +900,7 @@ std::shared_ptr make_constellation_plot() { auto update = [scene = not_null{scene.get()}, raw = not_null{constellation.get()}, horizontal = not_null{horizontal.get()}, - vertical = not_null{vertical.get()}](const Plot_Render_Tick& event, bool demo_data) { + vertical = not_null{vertical.get()}](const Frame_Request& event, bool demo_data) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, horizontal, vertical); if (!demo_data) return; const auto& state = raw->template read_prop(); @@ -952,7 +952,7 @@ std::shared_ptr make_selection_overlay_plot() { .build(); auto update = [scene = not_null{scene.get()}, horizontal = not_null{horizontal.get()}, - vertical = not_null{vertical.get()}](const Plot_Render_Tick& event, bool) { + vertical = not_null{vertical.get()}](const Frame_Request& event, bool) { resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, horizontal, vertical); }; auto view = make_scene_view< diff --git a/mcp/core/runtime/Gallery_Plots_3D.cpp b/mcp/core/runtime/Gallery_Plots_3D.cpp index 344a13f..eb657bc 100644 --- a/mcp/core/runtime/Gallery_Plots_3D.cpp +++ b/mcp/core/runtime/Gallery_Plots_3D.cpp @@ -233,7 +233,7 @@ struct Random_Data_Generator { } template void update(Visual_Object& visual, Axes_3D&, Marker_Visual*, - const Plot_Render_Tick& request) { + const Frame_Request& request) { using Definition = typename Visual_Object::Attached_Object; using Prop = typename Definition::Prop; const auto sequence = request.sequence != 0 @@ -436,11 +436,11 @@ public: } } } - void update(const Plot_Render_Tick& request) override { + void update(const Frame_Request& request) override { if constexpr (requires(Data_Generator& generator, Visual_Object& visual, Axes_Object& axes, Marker_Object* markers, - const Plot_Render_Tick& frame) { + const Frame_Request& frame) { generator.update(visual, axes, markers, frame); }) { data_generator_.update( @@ -757,7 +757,7 @@ struct Spectrogram_Data_Generator { } void update(Mesh_Visual& visual, Axes_3D&, Marker_Visual* markers, - const Plot_Render_Tick& request) { + const Frame_Request& request) { if (!parameters.animation_enabled || request.sequence % parameters.update_every_n_frames != 0) return; diff --git a/mcp/core/runtime/Plot.cpp b/mcp/core/runtime/Plot.cpp index a66d5b3..bb16762 100644 --- a/mcp/core/runtime/Plot.cpp +++ b/mcp/core/runtime/Plot.cpp @@ -211,6 +211,39 @@ nlohmann::json frame_policy_state_json(const Frame_Policy::State& state) { {"active_frame_count", state.active_frame_count} } }, + { + "lifecycle", { + {"pending_request", state.pending_request.has_value()}, + {"in_flight_window", 1}, + {"in_flight_frame_sequence", state.in_flight_frame_sequence}, + {"publication_feedback_expected", + state.publication_feedback_expected}, + {"publication_feedback_received", + state.publication_feedback_received}, + {"publication_feedback_remaining", + state.publication_feedback_expected > + state.publication_feedback_received + ? state.publication_feedback_expected - + state.publication_feedback_received + : 0}, + {"latest_publication_dispatch_ms", + static_cast( + state.latest_publication_dispatch_latency_ns) / + 1'000'000.0}, + {"maximum_publication_dispatch_ms", + static_cast( + state.maximum_publication_dispatch_latency_ns) / + 1'000'000.0}, + {"published_frame_count", state.published_frame_count}, + {"publication_failed_count", state.publication_failed_count}, + {"latest_publication_ms", + static_cast(state.latest_publication_latency_ns) / + 1'000'000.0}, + {"maximum_publication_ms", + static_cast(state.maximum_publication_latency_ns) / + 1'000'000.0} + } + }, { "throughput", { {"request_rate_fps", rate(state.request_count)}, @@ -613,11 +646,6 @@ struct Plot::Private { rendering, /* Scene::advance -> Plot pixel publish,不可重入。 */ consuming /* 外接 Taskflow 正在消费已发布帧;允许下一帧渲染。 */ }; - enum struct Render_Admission_State : std::uint8_t { - ready, - rendering, - frame_slots_exhausted - }; struct Managed_Frame { std::chrono::microseconds presentation_time{}; /* 共享页面时钟产生的媒体时间戳。 */ std::chrono::steady_clock::time_point tick_issued_at{}; /* 本逻辑帧请求进入 Plot 的时刻。 */ @@ -647,9 +675,12 @@ struct Plot::Private { std::atomic> consumers{ std::make_shared() }; /* 低频订阅修改发布不可变版本。 */ + struct Publication_Dispatch { + std::size_t asynchronous_feedback_count{}; + bool succeeded{}; + }; std::atomic_uint64_t next_stream_id{1}; std::atomic> terminal_failure{}; /* 首次 Plot Unknown Failure 的唯一终止状态。 */ - std::uint64_t next_frame_sequence{1}; std::unique_ptr frame_policy_2d{}; std::unique_ptr frame_policy_3d{}; Frame_Scheduler::Timer frame_timer{}; /* 每 Plot/Scene 只有轻量时间轮节点,不持有线程。 */ @@ -658,11 +689,8 @@ struct Plot::Private { std::atomic latest_statistics_frame{}; /* 只定位权威帧槽,不保存统计副本。 */ std::atomic retired_frames{}; /* 完成回调返回、唯一帧策略写者消费的物理帧。 */ Scene scene; /* 析构顺序保证 Scene 先停止,再释放物理帧。 */ - moodycamel::ConcurrentQueue tick_requests{}; /* 多生产者提交、唯一短任务消费的帧请求流。 */ - std::optional deferred_tick{}; /* 仅 tick consumer 任务访问的 latest 延后请求。 */ std::atomic_uint64_t consumer_work_generation{}; /* tick 或退休帧入队后推进,关闭 consumer 尾部唤醒竞争窗口。 */ std::atomic_bool tick_task_scheduled{}; /* 唯一短任务准入;不占用 Worker 等待。 */ - std::atomic render_admission{Render_Admission_State::ready}; /* Plot 渲染准入及物理槽背压的唯一状态源。 */ std::chrono::steady_clock::time_point clock_origin{std::chrono::steady_clock::now()}; std::atomic_size_t taskflow_trace_remaining{}; /* 尚待标记的实际渲染帧数。 */ static constexpr std::size_t maximum_taskflow_trace_frames{120}; @@ -717,17 +745,16 @@ struct Plot::Private { } [[nodiscard]] nlohmann::json schema() const; [[nodiscard]] Stream_Snapshot stream_snapshot() const; - void publish(std::shared_ptr frame) noexcept; - void submit_tick_request(Plot_Render_Tick tick); - void keep_latest_tick(const Plot_Render_Tick& tick); + [[nodiscard]] Publication_Dispatch publish( + std::shared_ptr frame) noexcept; + void submit_frame_request(Frame_Request request); void arm_tick_consumer(std::weak_ptr lifetime); - void release_render_admission(std::weak_ptr lifetime); + void request_next_maximum_rate_frame(); void retain_frame_policy_lifetime(); void release_frame_policy_lifetime_if_idle(); void consume_tick(std::weak_ptr lifetime); void refresh_schedule(); - void clock_tick(const Plot_Render_Tick& tick); - void render_frame(Plot_Render_Tick tick); + void render_frame(Frame_Production production); void publish_completed_frame(not_null completed); void consume_completed_frame(not_null frame); void retire_completed_frame(not_null frame); @@ -766,7 +793,7 @@ void Plot::Private::fail(std::exception_ptr failure) noexcept { }.dump(), {} }); - publish(std::move(output)); + static_cast(publish(std::move(output))); } catch (...) {} } @@ -802,22 +829,32 @@ Plot::Private::Stream_Snapshot Plot::Private::stream_snapshot() const { result.height = std::clamp(result.height == 0 ? 192U : result.height, 120U, 1080U) & ~1U; return result; } -void Plot::Private::publish( +Plot::Private::Publication_Dispatch Plot::Private::publish( std::shared_ptr frame) noexcept { - if (!frame) return; + Publication_Dispatch dispatch; + if (!frame) return dispatch; try { const auto snapshot = stream_snapshot(); std::vector failed_consumers; for (const auto& [id, consumer] : *snapshot.consumers) { if (!consumer.handler) continue; try { - consumer.handler(frame); + switch (consumer.handler(frame)) { + case Plot_Frame_Publication::ignored: + break; + case Plot_Frame_Publication::completed: + dispatch.succeeded = true; + break; + case Plot_Frame_Publication::asynchronous: + ++dispatch.asynchronous_feedback_count; + break; + } } catch (...) { failed_consumers.push_back(id); } } - if (failed_consumers.empty()) return; + if (failed_consumers.empty()) return dispatch; auto current = consumers.load(std::memory_order_acquire); for (;;) { auto next = std::make_shared(*current); @@ -830,6 +867,7 @@ void Plot::Private::publish( } } catch (...) {} + return dispatch; } void Plot::Private::refresh_schedule() { if (!frame_timer.valid()) return; @@ -846,50 +884,24 @@ void Plot::Private::refresh_schedule() { frame_timer.cancel(); if (pacing.mode != Frame_Pacing_Mode::maximum_rate) return; const auto now = std::chrono::steady_clock::now(); - submit_tick_request(Plot_Render_Tick{ + submit_frame_request(Frame_Request{ .issued_at = now, .time_milliseconds = std::chrono::duration( now - clock_origin).count(), .source = Frame_Request_Source::maximum_rate }); - arm_tick_consumer(lifetime); } -void Plot::Private::submit_tick_request(Plot_Render_Tick tick) { - const auto source = tick.source; - const auto issued_at = tick.issued_at; - if (!tick_requests.enqueue(std::move(tick))) throw std::bad_alloc{}; +void Plot::Private::submit_frame_request(Frame_Request request) { with_frame_policy([&](auto& policy) { - policy.record_request(source, issued_at); + policy.submit_request(std::move(request)); }); consumer_work_generation.fetch_add(1, std::memory_order_release); -} -void Plot::Private::keep_latest_tick(const Plot_Render_Tick& tick) { - const auto priority = [](Frame_Request_Source source) { - switch (source) { - case Frame_Request_Source::unspecified: return 0; - case Frame_Request_Source::periodic: return 0; - case Frame_Request_Source::maximum_rate: return 1; - case Frame_Request_Source::immediate: return 2; - } - return 0; - }; - if (deferred_tick) { - const auto current_priority = priority(deferred_tick->source); - const auto next_priority = priority(tick.source); - with_frame_policy([](auto& policy) { - policy.record_request_coalesced(); - }); - if (current_priority > next_priority || - (current_priority == next_priority && - deferred_tick->issued_at >= tick.issued_at)) - return; - } - deferred_tick = tick; + arm_tick_consumer(lifetime); } void Plot::Private::arm_tick_consumer(std::weak_ptr lifetime) { if (terminal_failure.load(std::memory_order_acquire)) return; if (tick_task_scheduled.exchange(true, std::memory_order_acq_rel)) return; - aethera::schedule_task("web.plot.tick.consume", [lifetime] { + aethera::schedule_task("frame.policy.consume", [lifetime] { const auto plot = lifetime.lock(); if (!plot) return; try { @@ -900,18 +912,13 @@ void Plot::Private::arm_tick_consumer(std::weak_ptr lifetime) { } }); } -void Plot::Private::release_render_admission(std::weak_ptr lifetime) { - auto expected = Render_Admission_State::rendering; - if (!render_admission.compare_exchange_strong( - expected, Render_Admission_State::ready, - std::memory_order_acq_rel, std::memory_order_acquire)) - return; +void Plot::Private::request_next_maximum_rate_frame() { const auto& pacing = pacing_state(); if (pacing.render_enabled && pacing.mode == Frame_Pacing_Mode::maximum_rate) { const auto current_consumers = consumers.load(std::memory_order_acquire); if (!current_consumers->empty()) { const auto now = std::chrono::steady_clock::now(); - submit_tick_request(Plot_Render_Tick{ + submit_frame_request(Frame_Request{ .issued_at = now, .time_milliseconds = std::chrono::duration( now - clock_origin).count(), @@ -919,7 +926,6 @@ void Plot::Private::release_render_admission(std::weak_ptr lifetime) { }); } } - arm_tick_consumer(std::move(lifetime)); } void Plot::Private::retain_frame_policy_lifetime() { if (frame_policy_lifetime.load(std::memory_order_acquire)) return; @@ -944,43 +950,18 @@ void Plot::Private::consume_tick(std::weak_ptr lifetime) { consumer_work_generation.load(std::memory_order_acquire); consume_retired_frames(); if (consume_frame_policy_events()) refresh_schedule(); - Plot_Render_Tick requested; - while (tick_requests.try_dequeue(requested)) keep_latest_tick(requested); - if (render_admission.load(std::memory_order_acquire) == - Render_Admission_State::ready) { - auto tick = std::exchange(deferred_tick, {}); - if (tick) clock_tick(*tick); - } + auto production = with_frame_policy([](auto& policy) { + return policy.begin_production(); + }); + if (production) render_frame(std::move(*production)); consume_retired_frames(); if (consume_frame_policy_events()) refresh_schedule(); tick_task_scheduled.store(false, std::memory_order_release); if (consumer_work_generation.load(std::memory_order_acquire) != observed_generation || - retired_frames.load(std::memory_order_acquire) || - (render_admission.load(std::memory_order_acquire) == - Render_Admission_State::ready && deferred_tick)) + retired_frames.load(std::memory_order_acquire)) arm_tick_consumer(std::move(lifetime)); } -void Plot::Private::clock_tick(const Plot_Render_Tick& tick) { - if (terminal_failure.load(std::memory_order_acquire)) return; - const auto& pacing = pacing_state(); - const bool accepted = pacing.render_enabled && - (tick.source == Frame_Request_Source::immediate || - (tick.source == Frame_Request_Source::periodic && - pacing.mode == Frame_Pacing_Mode::fixed_rate) || - (tick.source == Frame_Request_Source::maximum_rate && - pacing.mode == Frame_Pacing_Mode::maximum_rate)); - if (!accepted) { - with_frame_policy([](auto& policy) { - policy.record_policy_rejection(); - }); - return; - } - with_frame_policy([&](auto& policy) { - policy.record_request_accepted(tick.source); - }); - render_frame(tick); -} bool Plot::Private::mark_taskflow_trace(Render_Frame& frame) { auto remaining = taskflow_trace_remaining.load(std::memory_order_acquire); while (remaining != 0) { @@ -1035,22 +1016,24 @@ nlohmann::json Plot::Private::trace_response( {"frames", std::move(frames)} }; } -void Plot::Private::render_frame(Plot_Render_Tick tick) { - if (terminal_failure.load(std::memory_order_acquire)) return; +void Plot::Private::render_frame(Frame_Production production) { + const auto sequence = production.sequence; + auto tick = std::move(production.request); + if (terminal_failure.load(std::memory_order_acquire)) { + with_frame_policy([&](auto& policy) { + policy.reject_production(sequence); + }); + return; + } const auto streams = stream_snapshot(); const auto& pacing = pacing_state(); const bool direct_diagnostics_frame = streams.consumers->empty() && tick.source == Frame_Request_Source::immediate; - if (!pacing.render_enabled || - (streams.consumers->empty() && !direct_diagnostics_frame)) - return; - auto admission_expected = Render_Admission_State::ready; - if (!render_admission.compare_exchange_strong( - admission_expected, Render_Admission_State::rendering, - std::memory_order_acq_rel, - std::memory_order_acquire)) { - keep_latest_tick(tick); + if (streams.consumers->empty() && !direct_diagnostics_frame) { + with_frame_policy([&](auto& policy) { + policy.reject_production(sequence); + }); return; } std::size_t slot_index{}; @@ -1092,19 +1075,15 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { break; } if (!managed) { - with_frame_policy([](auto& policy) { - policy.record_frame_slot_backpressure(); + with_frame_policy([&](auto& policy) { + policy.defer_production(std::move(production)); }); - keep_latest_tick(tick); /* * 三个槽都仍被外接消费者持有时,只保留 latest pending。这里绝不能 * 立即 arm tick consumer,否则会在没有任何槽可用期间形成 * consume -> no slot -> consume 的 Taskflow 任务风暴。真正的唤醒点 * 是 retire_completed_frame:某个 consuming 槽变回 available 后只唤醒一次。 */ - render_admission.store( - Render_Admission_State::frame_slots_exhausted, - std::memory_order_release); return; } retain_frame_policy_lifetime(); @@ -1135,7 +1114,6 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { tick.width = streams.width; tick.height = streams.height; } - const std::uint64_t sequence = next_frame_sequence++; const Frame_Identity identity{ sequence, tick.sequence == 0 ? sequence : tick.sequence }; @@ -1201,12 +1179,12 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { } }); if (!result) { - with_frame_policy([](auto& policy) { - policy.record_scene_rejection(); + with_frame_policy([&](auto& policy) { + policy.reject_production(sequence); }); rollback_unsubmitted(); restore_taskflow_trace_claim(); - release_render_admission(lifetime); + request_next_maximum_rate_frame(); } else { taskflow_trace_claimed = false; @@ -1230,7 +1208,7 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { .submitted = [weak](not_null, bool) { if (auto owner = weak.lock()) { try { - owner->d->release_render_admission(weak); + owner->d->request_next_maximum_rate_frame(); } catch (...) { owner->d->fail(std::current_exception()); @@ -1260,18 +1238,22 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { }); return; } - with_frame_policy([](auto& policy) { - policy.record_scene_rejection(); + with_frame_policy([&](auto& policy) { + policy.reject_production(sequence); }); rollback_unsubmitted(); restore_taskflow_trace_claim(); - release_render_admission(lifetime); + request_next_maximum_rate_frame(); if (result == Render_Scene_3D::Render_Result::backend_unavailable) throw std::runtime_error("3D render backend became unavailable before submission"); } catch (...) { rollback_unsubmitted(); restore_taskflow_trace_claim(); - release_render_admission(lifetime); + const auto active_sequence = pacing_state().in_flight_frame_sequence; + if (active_sequence == sequence) + with_frame_policy([&](auto& policy) { + policy.reject_production(sequence); + }); throw; } } @@ -1330,11 +1312,21 @@ void Plot::Private::publish_completed_frame( const auto published = std::make_shared( Plot_Stream_Frame{{}, std::move(pixels)}); const auto publish_started = std::chrono::steady_clock::now(); - publish(std::move(published)); + const auto dispatch = publish(std::move(published)); + const auto publish_completed = std::chrono::steady_clock::now(); + with_frame_policy([&](auto& policy) { + policy.submit_publication_dispatch({ + .started_at = publish_started, + .completed_at = publish_completed, + .sequence = identity.sequence, + .asynchronous_feedback_count = + dispatch.asynchronous_feedback_count, + .succeeded = dispatch.succeeded}); + }); frame->record(Frame_Trace_Measurement::plot_publish_ns, static_cast(std::max( 0, std::chrono::duration_cast( - std::chrono::steady_clock::now() - publish_started) + publish_completed - publish_started) .count()))); auto expected = Frame_State::rendering; if (!managed->state.compare_exchange_strong( @@ -1359,7 +1351,7 @@ void Plot::Private::consume_completed_frame(not_null frame) { /* Scene 完成即释放 Plot admission。媒体采样属于 Gallery 自己的独立时钟和 * DAG,不再借 Plot 保存一套 post-publish 管线状态。 */ if (std::holds_alternative>(managed->frame)) - release_render_admission(lifetime); + request_next_maximum_rate_frame(); } void Plot::Private::consume_retired_frames() { for (;;) { @@ -1482,11 +1474,6 @@ void Plot::Private::finalize_retired_frame(not_null frame) { std::memory_order_acquire)) throw std::logic_error("retired Plot frame is not consuming"); latest_statistics_frame.store(managed, std::memory_order_release); - /* 物理槽是唯一背压原因;归还任意槽后只解除一次耗尽状态。 */ - auto admission = Render_Admission_State::frame_slots_exhausted; - static_cast(render_admission.compare_exchange_strong( - admission, Render_Admission_State::ready, - std::memory_order_acq_rel, std::memory_order_acquire)); arm_tick_consumer(lifetime); release_frame_policy_lifetime_if_idle(); if (captured_trace) { @@ -1515,7 +1502,7 @@ void Plot::ensure_started() { d->frame_timer = Frame_Scheduler::instance().make_timer( [weak](Frame_Scheduler::Tick tick) { if (const auto owner = weak.lock()) { - owner->schedule_render(Plot_Render_Tick{ + owner->schedule_render(Frame_Request{ .issued_at = tick.issued_at, .sequence = tick.sequence, .time_milliseconds = tick.time_milliseconds, @@ -1592,22 +1579,31 @@ void Plot::configure_stream(Stream_Id stream, std::uint32_t width, return; } } -void Plot::schedule_render(Plot_Render_Tick tick) { - if (!std::isfinite(tick.time_milliseconds) || - tick.time_milliseconds < 0.0) +void Plot::schedule_render(Frame_Request request) { + if (!std::isfinite(request.time_milliseconds) || + request.time_milliseconds < 0.0) throw std::invalid_argument( "render time_milliseconds must be finite and non-negative"); - if (tick.width == 0 || tick.height == 0) throw std::invalid_argument("render viewport must be non-zero"); + if (request.width == 0 || request.height == 0) throw std::invalid_argument("render viewport must be non-zero"); ensure_started(); if (d->terminal_failure.load(std::memory_order_acquire)) return; - d->submit_tick_request(std::move(tick)); + d->submit_frame_request(std::move(request)); +} +void Plot::submit_publication_feedback( + Frame_Publication_Feedback feedback) { + ensure_started(); + if (d->terminal_failure.load(std::memory_order_acquire)) return; + d->with_frame_policy([&](auto& policy) { + policy.submit_publication_feedback(std::move(feedback)); + }); + d->consumer_work_generation.fetch_add(1, std::memory_order_release); d->arm_tick_consumer(weak_from_this()); } void Plot::render_once() { ensure_started(); const auto now = std::chrono::steady_clock::now(); const auto elapsed = now - d->clock_origin; - schedule_render(Plot_Render_Tick{ + schedule_render(Frame_Request{ .issued_at = now, .time_milliseconds = std::chrono::duration(elapsed).count(), @@ -1728,15 +1724,11 @@ nlohmann::json Plot::diagnostics() const { } const auto& pacing = d->pacing_state(); const auto stream = d->stream_snapshot(); - const auto admission = d->render_admission.load(std::memory_order_acquire); - const auto admission_name = [&] { - switch (admission) { - case Private::Render_Admission_State::ready: return "ready"; - case Private::Render_Admission_State::rendering: return "rendering"; - case Private::Render_Admission_State::frame_slots_exhausted: return "frame_slots_exhausted"; - } - return "unknown"; - }(); + const auto lifecycle_name = pacing.in_flight_frame_sequence != 0 + ? "publication_in_flight" + : pacing.pending_request + ? "request_pending" + : "ready"; 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)); @@ -1783,7 +1775,7 @@ nlohmann::json Plot::diagnostics() const { } }, {"frame_policy", frame_policy_state_json(pacing)}, - {"render_admission", admission_name}, + {"frame_lifecycle", lifecycle_name}, {"frame_statistics", std::move(frame_statistics)}, {"input_statistics", std::move(input_statistics)} }; diff --git a/mcp/core/runtime/Plot.hpp b/mcp/core/runtime/Plot.hpp index 841559f..213a421 100644 --- a/mcp/core/runtime/Plot.hpp +++ b/mcp/core/runtime/Plot.hpp @@ -27,14 +27,6 @@ struct Plot_Input_Event { std::uint32_t native_key{}; /* 浏览器原生按键码。 */ bool auto_repeat{}; /* 是否为系统重复按键。 */ }; -struct Plot_Render_Tick { - std::chrono::steady_clock::time_point issued_at{}; /* 页面帧时钟发布本 tick 的单调时刻;手动帧在提交时填写。 */ - std::uint64_t sequence{}; /* Kernel 全局 Frame_Scheduler 时间轴上的关联序号。 */ - double time_milliseconds{}; /* 页面级单调时间线,所有图共享同一个动画时刻。 */ - std::uint32_t width{320}; /* 当前图在媒体图集中的固定像素宽度。 */ - std::uint32_t height{192}; /* 当前图在媒体图集中的固定像素高度。 */ - Frame_Request_Source source{Frame_Request_Source::immediate}; /* 本次请求来自周期时钟、手动操作或最大吞吐自驱动。 */ -}; enum struct Plot_Pixel_Layout : std::uint8_t { bgra8, rgba8 @@ -54,10 +46,16 @@ struct Plot_Stream_Frame { std::string notification{}; /* 仅用于终止错误等低频控制通知;正常帧为空。 */ std::shared_ptr pixels{}; /* 每帧完成进度及其可选图集像素。 */ }; +enum struct Plot_Frame_Publication : std::uint8_t { + ignored, /* 订阅者不消费本像素帧,不参与发布反馈。 */ + completed, /* 订阅回调返回时已同步完成业务消费。 */ + asynchronous /* 订阅者已接管发布,并会执行帧策略反馈任务。 */ +}; struct Plot final : public std::enable_shared_from_this { public: using Stream_Id = std::uint64_t; - using Stream_Handler = std::function)>; + using Stream_Handler = std::function)>; using Json_Handler = std::function; struct Scene_View { public: @@ -72,7 +70,7 @@ public: const std::vector& components) const = 0; [[nodiscard]] virtual nlohmann::json data_generator_schema() const = 0; [[nodiscard]] virtual nlohmann::json generate_data(const nlohmann::json& input) = 0; - virtual void update(const Plot_Render_Tick& tick) = 0; + virtual void update(const Frame_Request& request) = 0; }; Plot(std::unique_ptr scene, std::unique_ptr view); @@ -84,7 +82,8 @@ public: [[nodiscard]] Stream_Id subscribe(Stream_Handler handler); void unsubscribe(Stream_Id stream); void configure_stream(Stream_Id stream, std::uint32_t width, std::uint32_t height); - void schedule_render(Plot_Render_Tick tick); + void schedule_render(Frame_Request request); + void submit_publication_feedback(Frame_Publication_Feedback feedback); void render_once(); void submit_input(Plot_Input_Event event); [[nodiscard]] nlohmann::json schema(); diff --git a/mcp/tests/Control_Path_Benchmarks.cpp b/mcp/tests/Control_Path_Benchmarks.cpp index 71cbac3..a560622 100644 --- a/mcp/tests/Control_Path_Benchmarks.cpp +++ b/mcp/tests/Control_Path_Benchmarks.cpp @@ -223,9 +223,11 @@ public: const auto stream = plot->subscribe( [completed = std::move(completed)]( std::shared_ptr frame) { - if (!frame || !frame->pixels) return; + if (!frame || !frame->pixels) + return web::Plot_Frame_Publication::ignored; completed->fetch_add( 1, std::memory_order_relaxed); + return web::Plot_Frame_Publication::completed; }); plot->configure_stream( stream, configuration.width, configuration.height); diff --git a/mcp/tests/Event_Latency_Benchmarks.cpp b/mcp/tests/Event_Latency_Benchmarks.cpp index 17d4d98..fdcd218 100644 --- a/mcp/tests/Event_Latency_Benchmarks.cpp +++ b/mcp/tests/Event_Latency_Benchmarks.cpp @@ -329,18 +329,20 @@ void run_event_latency(benchmark::State& state) { const auto probe = item.probe; item.stream = item.plot->subscribe( [probe](std::shared_ptr frame) { - if (!frame || !frame->pixels) return; + if (!frame || !frame->pixels) + return web::Plot_Frame_Publication::ignored; const auto expected = probe->expected_correlation.load( std::memory_order_acquire); if (expected == 0 || frame->pixels->correlation_id != expected) - return; + return web::Plot_Frame_Publication::completed; std::uint64_t incomplete{}; static_cast( probe->completed_steady_ns.compare_exchange_strong( incomplete, steady_time_ns(), std::memory_order_release, std::memory_order_relaxed)); + return web::Plot_Frame_Publication::completed; }); item.plot->configure_stream( item.stream, configuration.width, configuration.height); @@ -384,7 +386,7 @@ void run_event_latency(benchmark::State& state) { return; } } - item.plot->schedule_render(web::Plot_Render_Tick{ + item.plot->schedule_render(aethera::Frame_Request{ .issued_at = Clock::now(), .sequence = correlation, .time_milliseconds = diff --git a/web_server/main.cmake b/web_server/main.cmake index 164a14b..ba5c9e5 100644 --- a/web_server/main.cmake +++ b/web_server/main.cmake @@ -177,6 +177,7 @@ if (MSVC) endif () if (Aethera_BUILD_TESTS) + find_package(benchmark CONFIG REQUIRED) set(Aethera_Web_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests") append_glob_source(Aethera_Web_test_sources "${Aethera_Web_test_dir}") set(Aethera_Web_test_targets) @@ -184,6 +185,9 @@ if (Aethera_BUILD_TESTS) if (NOT Aethera_Web_test_source MATCHES "\\.(c|cc|cpp|cxx)$") continue() endif () + if (Aethera_Web_test_source MATCHES "_Benchmarks\\.(c|cc|cpp|cxx)$") + continue() + endif () get_filename_component(Aethera_Web_test_name "${Aethera_Web_test_source}" NAME_WE) set(Aethera_Web_test_target "Aethera_Web_${Aethera_Web_test_name}") @@ -205,6 +209,38 @@ if (Aethera_BUILD_TESTS) LABELS "Aethera_Web") list(APPEND Aethera_Web_test_targets "${Aethera_Web_test_target}") endforeach () + add_executable(Aethera_Web_Event_Latency_Benchmarks + "${Aethera_Web_test_dir}/Event_Latency_Benchmarks.cpp") + target_link_libraries(Aethera_Web_Event_Latency_Benchmarks PRIVATE + Aethera_Web_Core + benchmark::benchmark + TBB::tbbmalloc_proxy) + renderive_stage_render_3D_runtime(Aethera_Web_Event_Latency_Benchmarks) + if (MSVC) + target_compile_options(Aethera_Web_Event_Latency_Benchmarks PRIVATE + /utf-8 /bigobj) + target_link_options(Aethera_Web_Event_Latency_Benchmarks PRIVATE + "/INCLUDE:__TBB_malloc_proxy") + add_custom_command(TARGET Aethera_Web_Event_Latency_Benchmarks + POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "$" + "$" + "$" + VERBATIM) + endif () + add_custom_command(TARGET Aethera_Web_Event_Latency_Benchmarks POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + ${Aethera_FFmpeg_runtime_libraries} + "$" + COMMENT "Deploying FFmpeg for Aethera Web event latency benchmark" + VERBATIM) + add_custom_target(Aethera_Web_event_latency_benchmark + COMMAND Aethera_Web_Event_Latency_Benchmarks + --aethera_plots=all + --aethera_event_samples=3 + DEPENDS Aethera_Web_Event_Latency_Benchmarks + USES_TERMINAL) add_custom_target(Aethera_Web_check COMMAND "${CMAKE_CTEST_COMMAND}" --test-dir "${CMAKE_BINARY_DIR}" -C "$" -L "^Aethera_Web$" --output-on-failure diff --git a/web_server/src/Graph_WebSocket.cpp b/web_server/src/Graph_WebSocket.cpp index c6e64e0..2b22e5c 100644 --- a/web_server/src/Graph_WebSocket.cpp +++ b/web_server/src/Graph_WebSocket.cpp @@ -46,17 +46,23 @@ void Graph_WebSocket::start() { d->stream = d->plot->subscribe( [weak](std::shared_ptr frame) { if (const auto socket = weak.lock()) - socket->deliver_frame(std::move(frame)); + return socket->deliver_frame(std::move(frame)) + ? Plot_Frame_Publication::completed + : Plot_Frame_Publication::ignored; + return Plot_Frame_Publication::ignored; }); } -void Graph_WebSocket::deliver_frame( +bool Graph_WebSocket::deliver_frame( std::shared_ptr frame) { if (!frame || frame->notification.empty() || !d->attached.load(std::memory_order_acquire)) - return; - try { d->send_handler(frame->notification); } - catch (...) {} + return false; + try { + d->send_handler(frame->notification); + return true; + } + catch (...) { return false; } } void Graph_WebSocket::receive(std::string_view message) { diff --git a/web_server/src/Graph_WebSocket.hpp b/web_server/src/Graph_WebSocket.hpp index ed68c00..841c1cf 100644 --- a/web_server/src/Graph_WebSocket.hpp +++ b/web_server/src/Graph_WebSocket.hpp @@ -22,7 +22,8 @@ public: void close() noexcept; private: - void deliver_frame(std::shared_ptr frame); + [[nodiscard]] bool deliver_frame( + std::shared_ptr frame); struct Private; std::unique_ptr d; }; diff --git a/web_server/src/media/Gallery_Video_Stream.cpp b/web_server/src/media/Gallery_Video_Stream.cpp index 8e4042a..adc748c 100644 --- a/web_server/src/media/Gallery_Video_Stream.cpp +++ b/web_server/src/media/Gallery_Video_Stream.cpp @@ -135,18 +135,24 @@ bool Gallery_Video_Stream::Private::remove_consumer( } return false; } -void Gallery_Video_Stream::Private::publish( +bool Gallery_Video_Stream::Private::publish( std::shared_ptr video, std::string notification) noexcept { - if (!video && notification.empty()) return; + if (!video && notification.empty()) return false; + bool succeeded{}; try { std::vector failed_consumers; for (const auto& slot : consumers) { const auto consumer = slot.load(std::memory_order_acquire); if (!consumer || !consumer->handler) continue; try { - (*consumer->handler)(Gallery_Stream_Frame{ - video, notification}); + auto accepted_video = video; + if (accepted_video && consumer->readiness && + !(*consumer->readiness)()) + accepted_video.reset(); + if (!accepted_video && notification.empty()) continue; + succeeded |= (*consumer->handler)(Gallery_Stream_Frame{ + std::move(accepted_video), notification}); } catch (...) { failed_consumers.push_back(consumer->id); @@ -156,10 +162,39 @@ void Gallery_Video_Stream::Private::publish( static_cast(remove_consumer(id)); } catch (...) {} + return succeeded; +} +void Gallery_Video_Stream::Private::release_pending_frames() noexcept { + Pending_Plot_Frame pending; + while (pending_frames.try_dequeue(pending)) { + try { + if (pending.slot < sources.size() && pending.frame && + pending.frame->pixels) + sources[pending.slot].entry.plot->submit_publication_feedback({ + .completed_at = std::chrono::steady_clock::now(), + .sequence = pending.frame->pixels->sequence, + .succeeded = false}); + } + catch (...) {} + pending = {}; + } } void Gallery_Video_Stream::Private::fail( std::exception_ptr failure) noexcept { if (failed.exchange(true, std::memory_order_acq_rel)) return; + if (active_frame && !active_frame->publication_feedback_submitted && + active_frame->source_slot < sources.size()) { + try { + sources[active_frame->source_slot].entry.plot + ->submit_publication_feedback({ + .completed_at = std::chrono::steady_clock::now(), + .sequence = active_frame->plot_frame_sequence, + .succeeded = false}); + active_frame->publication_feedback_submitted = true; + } + catch (...) {} + } + release_pending_frames(); try { terminal_failure = exception_description(failure); const auto notification = nlohmann::json{ @@ -168,24 +203,31 @@ void Gallery_Video_Stream::Private::fail( {"version", 5}, {"message", *terminal_failure} }.dump(); - publish({}, notification); + static_cast(publish({}, notification)); } catch (...) {} } -void Gallery_Video_Stream::Private::accept_frame( +Plot_Frame_Publication Gallery_Video_Stream::Private::accept_frame( std::size_t slot, std::shared_ptr frame) { if (stopping.load(std::memory_order_acquire) || failed.load(std::memory_order_acquire) || !frame || !frame->pixels || !frame->pixels->pixels || !consumer_accepts()) - return; + return Plot_Frame_Publication::ignored; Pending_Plot_Frame pending{ - slot, std::move(frame->pixels), std::chrono::steady_clock::now(), + slot, std::move(frame), + std::chrono::steady_clock::now(), std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch())}; if (!pending_frames.enqueue(std::move(pending))) throw std::bad_alloc{}; + if (stopping.load(std::memory_order_acquire) || + failed.load(std::memory_order_acquire)) { + release_pending_frames(); + return Plot_Frame_Publication::ignored; + } received_frame_count.fetch_add(1, std::memory_order_relaxed); media_work_generation.fetch_add(1, std::memory_order_release); arm_media_task(object->weak_from_this()); + return Plot_Frame_Publication::asynchronous; } void Gallery_Video_Stream::Private::update_metrics( const Active_Media_Frame& frame, @@ -322,11 +364,16 @@ void Gallery_Video_Stream::Private::begin_media_frame() { ++processed_frame_count; const auto started = std::chrono::steady_clock::now(); const auto accepted = atlas->accept_frame( - pending.slot, std::move(pending.frame)); + pending.slot, pending.frame->pixels); if (accepted == - detail::Gallery_Frame_Atlas::Accept_Frame_Result::invalid_frame) + detail::Gallery_Frame_Atlas::Accept_Frame_Result::invalid_frame) { + sources[pending.slot].entry.plot->submit_publication_feedback({ + .completed_at = std::chrono::steady_clock::now(), + .sequence = pending.frame->pixels->sequence, + .succeeded = false}); throw std::logic_error( "Plot published an invalid gallery pixel frame"); + } auto presentation_time = std::chrono::duration_cast< std::chrono::microseconds>(started - media_origin); if (presentation_time <= last_presentation_time) @@ -337,7 +384,8 @@ void Gallery_Video_Stream::Private::begin_media_frame() { atlas->compose(), next_media_sequence++, presentation_time, pending.source_time_unix, std::chrono::duration( - started - pending.enqueued_at).count()}; + started - pending.enqueued_at).count(), + pending.slot, pending.frame->pixels->sequence, false, false}; static_cast(compose_ms.submit( std::chrono::duration( std::chrono::steady_clock::now() - started).count())); @@ -371,13 +419,27 @@ void Gallery_Video_Stream::Private::publish_video_frame() { if (!active_video) return; ++encoded_frame_count; const auto started = std::chrono::steady_clock::now(); - publish(active_video, {}); + active_frame->publication_succeeded = publish(active_video, {}); static_cast(publish_ms.submit( std::chrono::duration( std::chrono::steady_clock::now() - started).count())); } +void Gallery_Video_Stream::Private::submit_publication_feedback() { + if (!active_frame || active_frame->publication_feedback_submitted) return; + if (active_frame->source_slot >= sources.size()) + throw std::logic_error( + "gallery publication feedback source is invalid"); + sources[active_frame->source_slot].entry.plot->submit_publication_feedback({ + .completed_at = std::chrono::steady_clock::now(), + .sequence = active_frame->plot_frame_sequence, + .succeeded = active_frame->publication_succeeded}); + active_frame->publication_feedback_submitted = true; +} void Gallery_Video_Stream::Private::complete_media_frame() { if (!active_frame) return; + if (!active_frame->publication_feedback_submitted) + throw std::logic_error( + "gallery media completion preceded policy feedback task"); const auto encoded_bytes = active_video ? active_video->annex_b.size() : 0U; const auto encoder_backend = active_video ? std::optional{active_video->backend} @@ -503,6 +565,17 @@ void Gallery_Video_Stream::bind_plots() { publish_video.describe("owner", "gallery") .describe("transport", "Drogon WebSocket") .describe("stage", "H.264 access unit enqueue"); + auto publication_feedback = media->add( + "frame.policy.publish.feedback", [weak] { + if (const auto owner = weak.lock()) { + auto& owner_data = static_cast(*owner->d); + try { owner_data.submit_publication_feedback(); } + catch (...) { owner_data.fail(std::current_exception()); } + } + }); + publication_feedback.describe("owner", "frame_policy") + .describe("stage", "media publication feedback") + .describe("execution", "Taskflow worker"); auto complete = media->add("gallery.media.complete", [weak] { if (const auto owner = weak.lock()) { auto& owner_data = static_cast(*owner->d); @@ -511,10 +584,12 @@ void Gallery_Video_Stream::bind_plots() { } }); complete.describe("owner", "gallery") - .describe("stage", "metrics and frame ownership release"); + .describe("stage", "metrics and Gallery stage ownership release") + .describe("policy_lifecycle", "already reported by preceding task"); compose.precede(encode); encode.precede(publish_video); - publish_video.precede(complete); + publish_video.precede(publication_feedback); + publication_feedback.precede(complete); data.media_graph = media; for (std::size_t slot = 0; slot < data.sources.size(); ++slot) { @@ -522,11 +597,17 @@ void Gallery_Video_Stream::bind_plots() { source.stream = source.entry.plot->subscribe( [weak, slot](std::shared_ptr frame) { const auto owner = weak.lock(); - if (!owner) return; + if (!owner) return Plot_Frame_Publication::ignored; auto& owner_data = static_cast(*owner->d); - if (owner_data.stopping.load(std::memory_order_acquire)) return; - try { owner_data.accept_frame(slot, std::move(frame)); } - catch (...) { owner_data.fail(std::current_exception()); } + if (owner_data.stopping.load(std::memory_order_acquire)) + return Plot_Frame_Publication::ignored; + try { + return owner_data.accept_frame(slot, std::move(frame)); + } + catch (...) { + owner_data.fail(std::current_exception()); + return Plot_Frame_Publication::ignored; + } }); source.entry.plot->configure_stream( source.stream, tile_width, tile_height); @@ -614,6 +695,7 @@ void Gallery_Video_Stream::shutdown() noexcept { } for (auto& consumer : data.consumers) consumer.store({}, std::memory_order_release); + data.release_pending_frames(); } std::string Gallery_Video_Stream::layout_description() const { const auto& data = static_cast(*d); diff --git a/web_server/src/media/Gallery_Video_Stream.hpp b/web_server/src/media/Gallery_Video_Stream.hpp index 5ad6bdf..0e78af5 100644 --- a/web_server/src/media/Gallery_Video_Stream.hpp +++ b/web_server/src/media/Gallery_Video_Stream.hpp @@ -29,7 +29,7 @@ struct Gallery_Video_Stream : Def, struct Private; using Stream_Id = std::uint64_t; - using Stream_Handler = std::function; + using Stream_Handler = std::function; using Transport_Readiness = std::function; using Transport_Diagnostics = std::function; diff --git a/web_server/src/media/Gallery_Video_Stream.ipp b/web_server/src/media/Gallery_Video_Stream.ipp index 49410b1..338d1a2 100644 --- a/web_server/src/media/Gallery_Video_Stream.ipp +++ b/web_server/src/media/Gallery_Video_Stream.ipp @@ -29,7 +29,7 @@ struct Gallery_Video_Stream::Private : Prev_Private { struct Pending_Plot_Frame { std::size_t slot{}; /* 该完成帧所属的稳定图集槽位。 */ - std::shared_ptr frame{}; /* 已通过所属 Plot 帧策略的不可变像素帧。 */ + std::shared_ptr frame{}; /* 持有像素帧以及其隐藏的 Plot 交付生命周期。 */ std::chrono::steady_clock::time_point enqueued_at{}; /* 进入媒体串行队列的单调时刻。 */ std::chrono::nanoseconds source_time_unix{}; /* 进入媒体串行队列的 Unix 时间。 */ }; @@ -40,6 +40,10 @@ struct Gallery_Video_Stream::Private : Prev_Private { std::chrono::microseconds presentation_time{}; /* 媒体流起点以来的单调显示时间。 */ std::chrono::nanoseconds source_time_unix{}; /* 对应 Plot 像素进入媒体流水线的 Unix 时间。 */ double queue_delay_ms{}; /* Plot 发布到开始合成的队列时间。 */ + std::size_t source_slot{}; /* 发布反馈路由到唯一来源 Plot 的槽位。 */ + std::uint64_t plot_frame_sequence{}; /* Frame_Policy 当前管理的来源帧序号。 */ + bool publication_succeeded{}; /* 媒体传输是否至少成功接收一次发布。 */ + bool publication_feedback_submitted{}; /* 防止失败清理重复提交策略反馈。 */ }; static constexpr std::size_t maximum_consumers{32}; @@ -57,7 +61,7 @@ struct Gallery_Video_Stream::Private : Prev_Private { std::atomic_uint64_t media_work_generation{}; /* 新完成帧入队后推进,封闭任务退出竞争窗口。 */ detail::FFmpeg_Frame_Transport ffmpeg_transport; /* 仅由媒体 DAG 串行访问的编码上下文。 */ std::optional active_frame{}; /* 当前媒体 DAG 独占的图集帧。 */ - std::shared_ptr active_video{}; /* 当前编码结果的共享所有权。 */ + std::shared_ptr active_video{}; /* 当前编码结果的共享媒体所有权,不承担帧策略状态。 */ std::atomic_uint64_t next_consumer_id{1}; /* 订阅身份生成器。 */ std::array>, maximum_consumers> consumers{}; /* 订阅关系的权威原子槽位。 */ @@ -94,15 +98,18 @@ struct Gallery_Video_Stream::Private : Prev_Private { void initialize(std::vector plots); [[nodiscard]] bool consumer_accepts() const noexcept; [[nodiscard]] bool remove_consumer(Stream_Id stream) noexcept; - void publish(std::shared_ptr video, - std::string notification) noexcept; + [[nodiscard]] bool publish( + std::shared_ptr video, + std::string notification) noexcept; + void release_pending_frames() noexcept; void fail(std::exception_ptr failure) noexcept; - void accept_frame(std::size_t slot, - std::shared_ptr frame); + [[nodiscard]] Plot_Frame_Publication accept_frame( + std::size_t slot, std::shared_ptr frame); void arm_media_task(std::weak_ptr lifetime); void begin_media_frame(); void encode_media_frame(); void publish_video_frame(); + void submit_publication_feedback(); void complete_media_frame(); void update_metrics(const Active_Media_Frame& frame, std::size_t encoded_bytes, diff --git a/web_server/src/media/Gallery_Video_WebSocket.cpp b/web_server/src/media/Gallery_Video_WebSocket.cpp index e3af176..11a3222 100644 --- a/web_server/src/media/Gallery_Video_WebSocket.cpp +++ b/web_server/src/media/Gallery_Video_WebSocket.cpp @@ -1,4 +1,5 @@ #include "Gallery_Video_WebSocket.hpp" +#include #include #include #include @@ -63,12 +64,19 @@ struct Gallery_Video_WebSocket::Private final { std::string connection_id{}; std::atomic_bool attached{}; std::atomic_bool decoder_ready{}; - std::atomic_size_t outstanding_frames{}; + moodycamel::ConcurrentQueue< + std::shared_ptr> outstanding_frames{}; std::atomic_uint64_t latest_sent_sequence{}; std::atomic_uint64_t latest_acknowledged_sequence{}; std::atomic_uint64_t sent_count{}; std::atomic_uint64_t acknowledged_count{}; std::atomic_uint64_t rejected_count{}; + + void release_outstanding_frames() noexcept { + std::shared_ptr outstanding; + while (outstanding_frames.try_dequeue(outstanding)) + outstanding.reset(); + } }; Gallery_Video_WebSocket::Gallery_Video_WebSocket( @@ -93,7 +101,8 @@ void Gallery_Video_WebSocket::start() { d->connection_id, [weak](Gallery_Stream_Frame frame) { if (const auto socket = weak.lock()) - socket->deliver(std::move(frame)); + return socket->deliver(std::move(frame)); + return false; }, [weak] { const auto socket = weak.lock(); @@ -111,8 +120,8 @@ void Gallery_Video_WebSocket::start() { {"kind", "drogon_h264_state"}, {"decoder_ready", socket->d->decoder_ready.load( std::memory_order_acquire)}, - {"outstanding_frames", socket->d->outstanding_frames.load( - std::memory_order_acquire)}, + {"outstanding_frames", + socket->d->outstanding_frames.size_approx()}, {"latest_sent_sequence", socket->d->latest_sent_sequence.load( std::memory_order_relaxed)}, {"latest_acknowledged_sequence", @@ -132,30 +141,44 @@ void Gallery_Video_WebSocket::start() { drogon::WebSocketMessageType::Text); } -void Gallery_Video_WebSocket::deliver(Gallery_Stream_Frame frame) { - if (!d->attached.load(std::memory_order_acquire)) return; - if (frame.video && !queue_video_frame(std::move(frame.video))) { +bool Gallery_Video_WebSocket::deliver(Gallery_Stream_Frame frame) { + if (!d->attached.load(std::memory_order_acquire)) return false; + bool succeeded{}; + const bool has_video = static_cast(frame.video); + if (frame.video) { + succeeded = queue_video_frame(std::move(frame.video)); + } + if (has_video && !succeeded) { d->rejected_count.fetch_add(1, std::memory_order_relaxed); d->stream->request_video_key_frame(); } if (!frame.notification.empty()) { if (const auto connection = d->connection.lock(); - connection && connection->connected()) + connection && connection->connected()) { connection->send(std::move(frame.notification), drogon::WebSocketMessageType::Text); + succeeded = true; + } } + return succeeded; } bool Gallery_Video_WebSocket::queue_video_frame( std::shared_ptr frame) { - if (!frame || !d->attached.load(std::memory_order_acquire)) + if (!frame || !d->attached.load(std::memory_order_acquire) || + !d->decoder_ready.load(std::memory_order_acquire)) return false; const auto connection = d->connection.lock(); if (!connection || !connection->connected()) return false; const auto sequence = frame->sequence; try { auto packet = websocket_packet(*frame); - d->outstanding_frames.fetch_add(1, std::memory_order_acq_rel); + if (!d->outstanding_frames.enqueue(frame)) throw std::bad_alloc{}; + if (!d->attached.load(std::memory_order_acquire) || + !d->decoder_ready.load(std::memory_order_acquire)) { + d->release_outstanding_frames(); + return false; + } connection->send( reinterpret_cast(packet.data()), packet.size(), drogon::WebSocketMessageType::Binary); @@ -164,30 +187,25 @@ bool Gallery_Video_WebSocket::queue_video_frame( return true; } catch (...) { - d->outstanding_frames.fetch_sub(1, std::memory_order_release); close(); return false; } } void Gallery_Video_WebSocket::acknowledge_video_frame(std::uint64_t sequence) { - auto acknowledged = d->latest_acknowledged_sequence.load( - std::memory_order_acquire); - while (sequence > acknowledged) { - if (!d->latest_acknowledged_sequence.compare_exchange_weak( - acknowledged, sequence, std::memory_order_acq_rel, - std::memory_order_acquire)) - continue; - auto outstanding = d->outstanding_frames.load( - std::memory_order_acquire); - while (outstanding != 0 && - !d->outstanding_frames.compare_exchange_weak( - outstanding, outstanding - 1U, - std::memory_order_acq_rel, - std::memory_order_acquire)) {} - d->acknowledged_count.fetch_add(1, std::memory_order_relaxed); + if (sequence == 0 || sequence <= d->latest_acknowledged_sequence.load( + std::memory_order_acquire)) + return; + std::shared_ptr acknowledged; + if (!d->outstanding_frames.try_dequeue(acknowledged) || + !acknowledged || acknowledged->sequence != sequence) { + d->rejected_count.fetch_add(1, std::memory_order_relaxed); + close(); return; } + d->latest_acknowledged_sequence.store( + sequence, std::memory_order_release); + d->acknowledged_count.fetch_add(1, std::memory_order_relaxed); } void Gallery_Video_WebSocket::receive(std::string_view message) { @@ -202,6 +220,7 @@ void Gallery_Video_WebSocket::receive(std::string_view message) { } if (kind == "h264_stream_not_ready") { d->decoder_ready.store(false, std::memory_order_release); + d->release_outstanding_frames(); return; } if (kind == "h264_frame_accepted") { @@ -213,14 +232,16 @@ void Gallery_Video_WebSocket::receive(std::string_view message) { } void Gallery_Video_WebSocket::close() noexcept { - if (!d->attached.exchange(false, std::memory_order_acq_rel)) return; - try { - if (d->subscription != 0) d->stream->unsubscribe(d->subscription); + if (d->attached.exchange(false, std::memory_order_acq_rel)) { + try { + if (d->subscription != 0) + d->stream->unsubscribe(d->subscription); + } + catch (...) {} + d->subscription = 0; } - catch (...) {} - d->subscription = 0; d->decoder_ready.store(false, std::memory_order_release); - d->outstanding_frames.store(0, std::memory_order_release); + d->release_outstanding_frames(); } Gallery_Video_WebSocket_Controller::Gallery_Video_WebSocket_Controller( diff --git a/web_server/src/media/Gallery_Video_WebSocket.hpp b/web_server/src/media/Gallery_Video_WebSocket.hpp index b970de4..bfb9d7e 100644 --- a/web_server/src/media/Gallery_Video_WebSocket.hpp +++ b/web_server/src/media/Gallery_Video_WebSocket.hpp @@ -22,7 +22,7 @@ struct Gallery_Video_WebSocket final void close() noexcept; private: - void deliver(Gallery_Stream_Frame frame); + [[nodiscard]] bool deliver(Gallery_Stream_Frame frame); [[nodiscard]] bool queue_video_frame( std::shared_ptr frame); void acknowledge_video_frame(std::uint64_t sequence); diff --git a/web_server/tests/Event_Latency_Benchmarks.cpp b/web_server/tests/Event_Latency_Benchmarks.cpp new file mode 100644 index 0000000..16ef758 --- /dev/null +++ b/web_server/tests/Event_Latency_Benchmarks.cpp @@ -0,0 +1,560 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace aethera::web::event_latency_benchmarks { +namespace { + +using Steady_Clock = std::chrono::steady_clock; +using System_Clock = std::chrono::system_clock; +inline constexpr auto event_types = magic_enum::enum_values(); +inline constexpr std::size_t event_count = event_types.size(); + +struct Configuration { + std::uint32_t width{720}; + std::uint32_t height{420}; + std::uint32_t samples{3}; + std::string plots{"all"}; +}; + +struct Summary { + double p50{}; + double p95{}; + double p99{}; + double maximum{}; +}; + +struct Distribution { + std::vector samples{}; + + void add(double value) { samples.push_back(std::max(0.0, value)); } + + [[nodiscard]] Summary summarize() const { + if (samples.empty()) return {}; + auto ordered = samples; + std::ranges::sort(ordered); + const auto percentile = [&](double probability) { + const auto rank = probability * + static_cast(ordered.size() - 1U); + const auto lower = static_cast(rank); + const auto upper = std::min(lower + 1U, ordered.size() - 1U); + const auto fraction = rank - static_cast(lower); + return ordered[lower] + + (ordered[upper] - ordered[lower]) * fraction; + }; + return {percentile(0.50), percentile(0.95), percentile(0.99), + ordered.back()}; + } +}; + +struct Event_Result { + Distribution websocket_receive_ms{}; + Distribution scene_queue_ms{}; + Distribution scene_dispatch_ms{}; + Distribution scene_total_ms{}; + Distribution receive_to_pixel_ms{}; + Distribution media_pipeline_ms{}; + Distribution end_to_end_ms{}; +}; + +struct Plot_Result { + std::string id{}; + Plot_Dimension dimension{Plot_Dimension::two_d}; + std::array events{}; +}; + +struct Media_Probe { + std::atomic_uint64_t frame_count{}; + std::atomic_int64_t source_time_unix_ns{}; + std::atomic_int64_t published_time_unix_ns{}; +}; + +struct Active_Plot { + std::shared_ptr plot{}; + std::shared_ptr input{}; + std::shared_ptr video{}; + media::Gallery_Video_Stream::Stream_Id video_subscription{}; + std::shared_ptr probe{}; +}; + +struct Event_Timing { + std::int64_t submitted_unix_ns{}; + std::int64_t accepted_unix_ns{}; + std::uint64_t encoded_count_before{}; + std::uint64_t statistic_count_before{}; +}; + +Configuration configuration{}; +std::vector published_results{}; + +[[nodiscard]] std::int64_t system_time_ns() noexcept { + return std::chrono::duration_cast( + System_Clock::now().time_since_epoch()).count(); +} + +[[nodiscard]] double milliseconds(std::int64_t begin, + std::int64_t end) noexcept { + return end >= begin + ? static_cast(end - begin) / 1'000'000.0 : 0.0; +} + +[[nodiscard]] const nlohmann::json* find_event_statistic( + const nlohmann::json& diagnostics, Event_Type type, + std::string_view statistic) { + const auto input = diagnostics.find("input_statistics"); + if (input == diagnostics.end() || !input->is_object()) return nullptr; + const auto event = input->find( + std::string{magic_enum::enum_name(type)}); + if (event == input->end() || !event->is_object()) return nullptr; + const auto value = event->find(std::string{statistic}); + return value == event->end() || !value->is_object() + ? nullptr : &*value; +} + +[[nodiscard]] std::uint64_t event_statistic_count( + const std::shared_ptr& plot, Event_Type type) { + const auto diagnostics = plot->diagnostics(); + const auto* statistic = find_event_statistic( + diagnostics, type, "total_ms"); + return statistic ? statistic->value("count", 0ULL) : 0ULL; +} + +[[nodiscard]] double event_statistic_latest( + const nlohmann::json& diagnostics, Event_Type type, + std::string_view statistic) { + const auto* value = find_event_statistic(diagnostics, type, statistic); + if (!value) + throw std::runtime_error( + "missing Scene event statistic " + + std::string{magic_enum::enum_name(type)} + "/" + + std::string{statistic}); + return value->at("latest").get(); +} + +[[nodiscard]] std::string input_message(Event_Type type, + std::size_t plot_index, + std::uint64_t sample) { + const auto x = 120.0 + static_cast( + (sample * 13U + plot_index * 7U) % 400U); + const auto y = 80.0 + static_cast( + (sample * 11U + plot_index * 5U) % 240U); + return nlohmann::json{ + {"kind", "input"}, + {"event", { + {"type", magic_enum::enum_name(type)}, + {"time_milliseconds", static_cast( + std::chrono::duration_cast( + Steady_Clock::now().time_since_epoch()).count()) / + 1'000'000.0}, + {"position", {{"x", x}, {"y", y}}}, + {"global_position", {{"x", x}, {"y", y}}}, + {"button", "left"}, + {"buttons", type == Event_Type::pointer_release ? 0 : 1}, + {"modifiers", static_cast(Keyboard_Modifier::control)}, + {"pixel_delta_x", 3.0}, + {"pixel_delta_y", (sample & 1U) == 0U ? 120.0 : -120.0}, + {"angle_delta_x", 3.0}, + {"angle_delta_y", (sample & 1U) == 0U ? 120.0 : -120.0}, + {"key", "space"}, + {"native_key", 32}, + {"auto_repeat", (sample & 1U) != 0U} + }} + }.dump(); +} + +void configure_plot(const std::shared_ptr& plot) { + if (!plot) throw std::invalid_argument("web benchmark Plot is null"); + if (!plot->write_prop( + "frame-analysis", "pacing_mode", "manual").value( + "success", false)) + throw std::runtime_error("failed to select manual Plot policy"); + if (!plot->write_prop( + "frame-analysis", "pixel_delivery_enabled", true).value( + "success", false)) + throw std::runtime_error("failed to enable Plot pixel delivery"); +} + +[[nodiscard]] bool manual_pixel_policy_applied( + const std::shared_ptr& plot) { + const auto diagnostics = plot->diagnostics(); + const auto policy = diagnostics.find("frame_policy"); + if (policy == diagnostics.end() || !policy->is_object()) return false; + const auto state = policy->find("configuration"); + if (state == policy->end() || !state->is_object()) return false; + const auto mode = state->find("mode"); + const auto pixels = state->find("pixel_delivery_enabled"); + return mode != state->end() && mode->is_string() && + *mode == "manual" && pixels != state->end() && + pixels->is_boolean() && pixels->get(); +} + +void cooperatively_wait(std::string_view operation, + const std::function& completed, + std::chrono::seconds timeout) { + std::string failure; + Task_Graph graph{"web.event-latency.await"}; + graph.add("await", [&] { + const auto deadline = Steady_Clock::now() + timeout; + Task_Graph::corun_until([&] { + if (completed()) return true; + if (Steady_Clock::now() < deadline) return false; + failure = std::string{operation} + " timed out"; + return true; + }); + }); + aethera::detail::run_taskflow(graph); + if (!failure.empty()) throw std::runtime_error(failure); +} + +[[nodiscard]] std::vector selected_definitions() { + const auto definitions = gallery_plot_definitions(); + if (configuration.plots == "all") + return {definitions.begin(), definitions.end()}; + std::vector selected; + std::size_t begin{}; + while (begin <= configuration.plots.size()) { + const auto end = configuration.plots.find(',', begin); + const auto token = std::string_view{configuration.plots}.substr( + begin, end == std::string::npos + ? std::string::npos : end - begin); + for (const auto& definition : definitions) { + const bool matches_group = + (token == "2d" && + definition.dimension == Plot_Dimension::two_d) || + (token == "3d" && + definition.dimension == Plot_Dimension::three_d); + if (!matches_group && token != definition.id) continue; + if (std::ranges::find(selected, definition.id, + &Plot_Definition::id) == selected.end()) + selected.push_back(definition); + } + if (end == std::string::npos) break; + begin = end + 1U; + } + if (selected.empty()) + throw std::invalid_argument( + "--aethera_plots did not select a Gallery Plot"); + return selected; +} + +void print_results() { + std::cout << std::fixed << std::setprecision(3) + << "\nWeb event -> H.264 backend timeline (milliseconds)\n" + << "WS = JSON decode + Plot submit; Pixel = WS return to Plot " + "pixel entering Gallery; Media = Gallery queue/atlas/FFmpeg/" + "publish; E2E excludes network, WebCodecs and Canvas.\n\n" + << std::left << std::setw(24) << "Plot" + << std::setw(5) << "Dim" + << std::setw(18) << "Event" + << std::right << std::setw(10) << "WS P95" + << std::setw(11) << "Queue P95" + << std::setw(11) << "Disp P95" + << std::setw(11) << "Pixel P95" + << std::setw(11) << "Media P95" + << std::setw(11) << "E2E P50" + << std::setw(11) << "E2E P95" + << std::setw(11) << "E2E P99" << '\n'; + for (const auto& plot : published_results) { + for (std::size_t index = 0; index < event_count; ++index) { + const auto& result = plot.events[index]; + const auto websocket = result.websocket_receive_ms.summarize(); + const auto queue = result.scene_queue_ms.summarize(); + const auto dispatch = result.scene_dispatch_ms.summarize(); + const auto pixel = result.receive_to_pixel_ms.summarize(); + const auto media = result.media_pipeline_ms.summarize(); + const auto end_to_end = result.end_to_end_ms.summarize(); + std::cout << std::left << std::setw(24) << plot.id + << std::setw(5) << plot_dimension_name(plot.dimension) + << std::setw(18) << magic_enum::enum_name( + event_types[index]) + << std::right << std::setw(10) << websocket.p95 + << std::setw(11) << queue.p95 + << std::setw(11) << dispatch.p95 + << std::setw(11) << pixel.p95 + << std::setw(11) << media.p95 + << std::setw(11) << end_to_end.p50 + << std::setw(11) << end_to_end.p95 + << std::setw(11) << end_to_end.p99 << '\n'; + } + } +} + +void run_event_latency(benchmark::State& state) { + published_results.clear(); + auto control = mcp::Control_Service::create(); + std::vector active; + + try { + const auto definitions = selected_definitions(); + active.reserve(definitions.size()); + published_results.reserve(definitions.size()); + for (const auto& definition : definitions) { + auto plot = control->find_plot(definition.id); + configure_plot(plot); + auto probe = std::make_shared(); + auto video = media::Gallery_Video_Stream::create({ + {std::string{definition.id}, plot}}); + const auto video_subscription = video->subscribe( + "web-event-latency-" + std::string{definition.id}, + [probe](media::Gallery_Stream_Frame frame) { + if (!frame.video) return false; + probe->source_time_unix_ns.store( + frame.video->source_time_unix.count(), + std::memory_order_relaxed); + probe->published_time_unix_ns.store( + system_time_ns(), std::memory_order_relaxed); + probe->frame_count.fetch_add( + 1, std::memory_order_release); + return true; + }, + [] { return true; }, + [] { return nlohmann::json::object(); }); + auto input = std::make_shared( + plot, [](std::string) {}); + input->start(); + input->receive(nlohmann::json{ + {"kind", "stream"}, + {"viewport", { + {"width", configuration.width}, + {"height", configuration.height}}} + }.dump()); + active.push_back({std::move(plot), std::move(input), + std::move(video), video_subscription, + std::move(probe)}); + published_results.push_back({ + std::string{definition.id}, definition.dimension, {}}); + } + + cooperatively_wait("manual Plot policies", [&] { + return std::ranges::all_of(active, [](const Active_Plot& item) { + return manual_pixel_policy_applied(item.plot); + }); + }, std::chrono::seconds{10}); + + std::vector warmup_counts; + warmup_counts.reserve(active.size()); + for (auto& item : active) { + warmup_counts.push_back(item.probe->frame_count.load( + std::memory_order_acquire)); + item.input->receive(R"({"kind":"manual_render"})"); + } + cooperatively_wait("media warm-up", [&] { + for (std::size_t index = 0; index < active.size(); ++index) + if (active[index].probe->frame_count.load( + std::memory_order_acquire) <= warmup_counts[index]) + return false; + return true; + }, std::chrono::seconds{60}); + for (auto& item : active) item.plot->reset_diagnostics(); + + std::vector timings(active.size()); + std::string failure; + Task_Graph coordinator{"web.event-latency.measure"}; + coordinator.add("measure", [&] { + for (std::uint64_t sample = 0; + sample < configuration.samples; ++sample) { + for (std::size_t event_index = 0; + event_index < event_count; ++event_index) { + const auto type = event_types[event_index]; + for (std::size_t plot_index = 0; + plot_index < active.size(); ++plot_index) { + auto& item = active[plot_index]; + auto& timing = timings[plot_index]; + timing.encoded_count_before = + item.probe->frame_count.load( + std::memory_order_acquire); + timing.statistic_count_before = event_statistic_count( + item.plot, type); + const auto message = input_message( + type, plot_index, sample); + timing.submitted_unix_ns = system_time_ns(); + item.input->receive(message); + timing.accepted_unix_ns = system_time_ns(); + item.input->receive( + R"({"kind":"manual_render"})"); + } + + const auto deadline = Steady_Clock::now() + + std::chrono::seconds{60}; + Task_Graph::corun_until([&] { + bool complete{true}; + for (std::size_t index = 0; + index < active.size(); ++index) { + const auto encoded = active[index].probe-> + frame_count.load(std::memory_order_acquire) > + timings[index].encoded_count_before; + const auto observed = event_statistic_count( + active[index].plot, type) > + timings[index].statistic_count_before; + complete = complete && encoded && observed; + } + if (complete) return true; + if (Steady_Clock::now() < deadline) return false; + failure = "timed out waiting for " + + std::string{magic_enum::enum_name(type)} + + " H.264 frames"; + return true; + }); + if (!failure.empty()) return; + + for (std::size_t plot_index = 0; + plot_index < active.size(); ++plot_index) { + const auto& timing = timings[plot_index]; + const auto source = active[plot_index].probe-> + source_time_unix_ns.load(std::memory_order_acquire); + const auto published = active[plot_index].probe-> + published_time_unix_ns.load( + std::memory_order_acquire); + const auto diagnostics = + active[plot_index].plot->diagnostics(); + auto& result = published_results[plot_index]. + events[event_index]; + result.websocket_receive_ms.add(milliseconds( + timing.submitted_unix_ns, + timing.accepted_unix_ns)); + result.receive_to_pixel_ms.add(milliseconds( + timing.accepted_unix_ns, source)); + result.media_pipeline_ms.add(milliseconds( + source, published)); + result.end_to_end_ms.add(milliseconds( + timing.submitted_unix_ns, published)); + result.scene_queue_ms.add(event_statistic_latest( + diagnostics, type, "queue_wait_ms")); + result.scene_dispatch_ms.add(event_statistic_latest( + diagnostics, type, "dispatch_ms")); + result.scene_total_ms.add(event_statistic_latest( + diagnostics, type, "total_ms")); + } + } + } + }); + + for ([[maybe_unused]] auto iteration : state) + aethera::detail::run_taskflow(coordinator); + if (!failure.empty()) state.SkipWithError(failure); + + Distribution aggregate; + for (const auto& plot : published_results) + for (const auto& event : plot.events) + aggregate.samples.insert( + aggregate.samples.end(), event.end_to_end_ms.samples.begin(), + event.end_to_end_ms.samples.end()); + const auto summary = aggregate.summarize(); + state.counters["all/e2e_p50_ms"] = summary.p50; + state.counters["all/e2e_p95_ms"] = summary.p95; + state.counters["all/e2e_p99_ms"] = summary.p99; + state.SetItemsProcessed(static_cast( + configuration.samples * active.size() * event_count)); + print_results(); + } + catch (const std::exception& error) { + state.SkipWithError(error.what()); + } + + for (auto& item : active) { + item.input->close(); + item.video->unsubscribe(item.video_subscription); + item.video->shutdown(); + } +} + +BENCHMARK(run_event_latency)->Iterations(1)->UseRealTime(); + +[[nodiscard]] bool parse_unsigned(std::string_view value, + std::uint32_t& output) { + const auto* begin = value.data(); + const auto* end = begin + value.size(); + const auto parsed = std::from_chars(begin, end, output); + return parsed.ec == std::errc{} && parsed.ptr == end; +} + +[[nodiscard]] bool configure(int& argc, char** argv) { + int retained{1}; + for (int index = 1; index < argc; ++index) { + const std::string_view argument{argv[index]}; + constexpr std::string_view samples_prefix{ + "--aethera_event_samples="}; + constexpr std::string_view size_prefix{"--aethera_size="}; + constexpr std::string_view plots_prefix{"--aethera_plots="}; + if (argument.starts_with(samples_prefix)) { + if (!parse_unsigned(argument.substr(samples_prefix.size()), + configuration.samples) || + configuration.samples == 0 || configuration.samples > 600) { + std::cerr << "--aethera_event_samples must be in [1, 600]\n"; + return false; + } + continue; + } + if (argument.starts_with(size_prefix)) { + const auto value = argument.substr(size_prefix.size()); + const auto separator = value.find('x'); + if (separator == std::string_view::npos || + !parse_unsigned(value.substr(0, separator), + configuration.width) || + !parse_unsigned(value.substr(separator + 1U), + configuration.height) || + configuration.width == 0 || configuration.height == 0) { + std::cerr << "--aethera_size requires WIDTHxHEIGHT\n"; + return false; + } + continue; + } + if (argument.starts_with(plots_prefix)) { + configuration.plots = argument.substr(plots_prefix.size()); + if (configuration.plots.empty()) { + std::cerr << "--aethera_plots requires all, 2d, 3d or IDs\n"; + return false; + } + continue; + } + argv[retained++] = argv[index]; + } + argc = retained; + return true; +} + +} // namespace +} // namespace aethera::web::event_latency_benchmarks + +int main(int argc, char** argv) { + if (!aethera::web::event_latency_benchmarks::configure(argc, argv)) + return 2; + aethera::initialize_runtime({}); + benchmark::Initialize(&argc, argv); + if (benchmark::ReportUnrecognizedArguments(argc, argv)) return 2; + benchmark::AddCustomContext( + "aethera_plots", + aethera::web::event_latency_benchmarks::configuration.plots); + benchmark::AddCustomContext( + "aethera_event_samples", + std::to_string( + aethera::web::event_latency_benchmarks::configuration.samples)); + benchmark::AddCustomContext( + "aethera_size", + std::to_string( + aethera::web::event_latency_benchmarks::configuration.width) + + "x" + std::to_string( + aethera::web::event_latency_benchmarks::configuration.height)); + benchmark::RunSpecifiedBenchmarks(); + benchmark::Shutdown(); + return 0; +} diff --git a/web_server/tests/Gallery_Video_Stream_Tests.cpp b/web_server/tests/Gallery_Video_Stream_Tests.cpp index 8d55414..498ff30 100644 --- a/web_server/tests/Gallery_Video_Stream_Tests.cpp +++ b/web_server/tests/Gallery_Video_Stream_Tests.cpp @@ -51,7 +51,7 @@ void configure_manual_pixel_delivery(const std::shared_ptr& plot) { void schedule_manual_frame(const std::shared_ptr& plot, std::uint64_t correlation_sequence) { const auto now = Clock::now(); - plot->schedule_render(Plot_Render_Tick{ + plot->schedule_render(Frame_Request{ .issued_at = now, .sequence = correlation_sequence, .time_milliseconds = @@ -64,7 +64,7 @@ void schedule_manual_frame(const std::shared_ptr& plot, } TEST(Gallery_Video_Stream, - Encodes_Each_Plots_Independent_Policy_Frames_Without_Grouping) { + Publication_Feedback_Task_Releases_Each_Plots_Independent_Frame) { const auto definitions = gallery_plot_definitions(); const auto found = std::ranges::find_if( definitions, [](const Plot_Definition& definition) { @@ -82,26 +82,42 @@ TEST(Gallery_Video_Stream, constexpr std::size_t frame_count{5}; std::array sequences{}; std::atomic_size_t received{}; + std::atomic> outstanding{}; const auto subscription = stream->subscribe( "gallery-video-test", [&](Gallery_Stream_Frame frame) { - if (!frame.video) return; + if (!frame.video) return false; const auto index = received.fetch_add( 1, std::memory_order_acq_rel); if (index < sequences.size()) sequences[index].store( frame.video->sequence, std::memory_order_release); + outstanding.store(std::move(frame.video), + std::memory_order_release); + return true; }, [] { return true; }, [] { return nlohmann::json::object(); }); const std::array, frame_count> schedule{ plot_a, plot_a, plot_b, plot_a, plot_b}; + stream->request_taskflow_trace(1); for (std::size_t index = 0; index < schedule.size(); ++index) { + const auto published_before = schedule[index]->diagnostics() + .at("frame_policy").at("lifecycle") + .at("published_frame_count").get(); schedule_manual_frame(schedule[index], index + 1U); ASSERT_NO_THROW(corun_until_or_throw("encoded Plot frame", [&] { return received.load(std::memory_order_acquire) > index; })); + ASSERT_NO_THROW(corun_until_or_throw( + "frame policy publication feedback", [&] { + const auto diagnostics = schedule[index]->diagnostics(); + return diagnostics.at("frame_lifecycle") == "ready" && + diagnostics.at("frame_policy").at("lifecycle") + .at("published_frame_count") > published_before; + })); + outstanding.store({}, std::memory_order_release); } EXPECT_EQ(received.load(std::memory_order_acquire), frame_count); @@ -114,6 +130,24 @@ TEST(Gallery_Video_Stream, EXPECT_EQ(diagnostics.at("encoded_frame_count"), frame_count); EXPECT_EQ(diagnostics.at("pending_frame_count"), 0U); + ASSERT_NO_THROW(corun_until_or_throw("gallery Taskflow trace", [&] { + return stream->taskflow_trace().value("complete", false); + })); + const auto trace = stream->taskflow_trace(); + ASSERT_EQ(trace.at("frames").size(), 1U); + const auto& graphs = trace.at("frames").front().at("graphs"); + const auto graph = std::ranges::find_if(graphs, [](const auto& value) { + return value.value("name", std::string{}) == "gallery.video.frame"; + }); + ASSERT_NE(graph, graphs.end()); + const auto& nodes = graph->at("nodes"); + const auto feedback = std::ranges::find_if(nodes, [](const auto& value) { + return value.value("name", std::string{}) == + "frame.policy.publish.feedback"; + }); + ASSERT_NE(feedback, nodes.end()); + EXPECT_EQ(feedback->at("attributes").at("owner"), "frame_policy"); + stream->unsubscribe(subscription); stream->shutdown(); }