diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp index 2b77ae4..f8eb0b3 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -1,5 +1,6 @@ #include "Scene_Base.hpp" #include +#include #include #include #include @@ -11,6 +12,12 @@ #include "renderive/state/base/State_Strategy_Base.hpp" static_assert(TF_VERSION == 400100, "Renderive requires Taskflow 4.1.0"); namespace { +std::uint64_t steady_now_ns() noexcept { + return static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); +} + template Scene_Base::Renderable_List topological_order( const Scene_Base::Renderable_List& renderables, @@ -331,6 +338,50 @@ std::vector Scene_Base::paint_order_snapshot() con const auto ordered = display_order(*cache_renderables_); return {ordered.begin(), ordered.end()}; } +Task_Execution_Snapshot Scene_Base::task_execution_snapshot() const noexcept { + Task_Execution_Snapshot snapshot; + snapshot.worker_count = task_executor_worker_count(); + snapshot.render_sequence = + task_execution_render_sequence_.load(std::memory_order_acquire); + snapshot.graph_node_count = + task_execution_node_count_.load(std::memory_order_acquire); + snapshot.completed_count = + task_execution_completed_count_.load(std::memory_order_acquire); + snapshot.running_count = + task_execution_running_count_.load(std::memory_order_acquire); + snapshot.peak_parallelism = + task_execution_peak_parallelism_.load(std::memory_order_acquire); + snapshot.failed_count = + task_execution_failed_count_.load(std::memory_order_acquire); + const std::size_t accounted = snapshot.completed_count + snapshot.running_count; + snapshot.pending_count = accounted < snapshot.graph_node_count + ? snapshot.graph_node_count - accounted + : 0; + { + std::lock_guard lock(task_mutex_); + if (task_pending_) + snapshot.state = Task_Execution_State::Queued; + else if (rendering_) + snapshot.state = Task_Execution_State::Running; + else if (current_completion_ && current_completion_->completed) + snapshot.state = current_completion_->exception + ? Task_Execution_State::Failed + : Task_Execution_State::Completed; + } + snapshot.duration_ns = + task_execution_duration_ns_.load(std::memory_order_acquire); + if (snapshot.state == Task_Execution_State::Running) { + const auto started = + task_execution_started_ns_.load(std::memory_order_acquire); + const auto now = steady_now_ns(); + if (started != 0 && now >= started) + snapshot.duration_ns = now - started; + } + return snapshot; +} +std::size_t Scene_Base::task_executor_worker_count() noexcept { + return Execution_Context::executor().num_workers(); +} std::pmr::memory_resource& Scene_Base::memory_resource() const noexcept { return memory_domain_->resource(); } @@ -459,6 +510,20 @@ void Scene_Base::execute_taskflow(Render_Task& task) { }; std::pmr::monotonic_buffer_resource scratch_resource(&memory_resource()); tf::Taskflow scene_graph; + std::size_t planned_task_count{}; + const auto tracked = [this, &planned_task_count](auto function) { + ++planned_task_count; + return [this, function = std::move(function)]() mutable { + task_execution_started(); + try { + function(); + task_execution_finished(false); + } catch (...) { + task_execution_finished(true); + throw; + } + }; + }; std::pmr::unordered_map indices(&scratch_resource); indices.reserve(task.render_order.size()); std::pmr::vector render_flags(task.render_order.size(), false, &scratch_resource); @@ -500,21 +565,21 @@ void Scene_Base::execute_taskflow(Render_Task& task) { std::pmr::vector internal_tasks(&scratch_resource); internal_tasks.reserve(nodes.size()); for (const auto& node : nodes) { - internal_tasks.push_back(module.graph.emplace([this, function = node.function, context] { + internal_tasks.push_back(module.graph.emplace(tracked([this, function = node.function, context] { Render_Execution_Scope scope(*this); function(context); - }).name(std::string(node.name))); + })).name(std::string(node.name))); } for (std::size_t node_index = 0; node_index < nodes.size(); ++node_index) { for (std::size_t successor : nodes[node_index].successors) { internal_tasks[node_index].precede(internal_tasks.at(successor)); } } - tf::Task mark_rendered = module.graph.emplace([&renderable, render_revision] { + tf::Task mark_rendered = module.graph.emplace(tracked([&renderable, render_revision] { renderable.mark_rendered(render_revision); - }).name("cache_publish"); + })).name("cache_publish"); if (internal_tasks.empty()) { - module.graph.emplace([] {}).precede(mark_rendered); + module.graph.emplace(tracked([] {})).precede(mark_rendered); } else { for (std::size_t node_index = 0; node_index < nodes.size(); ++node_index) { if (nodes[node_index].successors.empty()) { @@ -523,7 +588,7 @@ void Scene_Base::execute_taskflow(Render_Task& task) { } } } else { - module.graph.emplace([] {}).name("cache_hit"); + module.graph.emplace(tracked([] {})).name("cache_hit"); } module.module_task = scene_graph.composed_of(module.graph).name("renderable"); } @@ -538,18 +603,62 @@ void Scene_Base::execute_taskflow(Render_Task& task) { } } const Scene_Render_Context final_context{this, task.frame_control_state, task.scene_state_revision, task.render_sequence, nullptr, nullptr}; - tf::Task final_task = scene_graph.emplace([this, final_context, &task] { + tf::Task final_task = scene_graph.emplace(tracked([this, final_context, &task] { Render_Execution_Scope scope(*this); generate_final_color_cache(final_context, task.display_order); - }).name("final_color_cache"); + })).name("final_color_cache"); if (modules.empty()) { - scene_graph.emplace([] {}).precede(final_task); + scene_graph.emplace(tracked([] {})).precede(final_task); } else { for (Module& module : modules) { module.module_task.precede(final_task); } } - Execution_Context::executor().run(scene_graph).get(); + begin_task_execution(task.render_sequence); + set_task_execution_node_count(planned_task_count); + try { + Execution_Context::executor().run(scene_graph).get(); + } catch (...) { + finish_task_execution(); + throw; + } + finish_task_execution(); +} +void Scene_Base::begin_task_execution(std::uint64_t render_sequence) noexcept { + task_execution_render_sequence_.store(render_sequence, std::memory_order_release); + task_execution_node_count_.store(0, std::memory_order_release); + task_execution_completed_count_.store(0, std::memory_order_release); + task_execution_running_count_.store(0, std::memory_order_release); + task_execution_peak_parallelism_.store(0, std::memory_order_release); + task_execution_failed_count_.store(0, std::memory_order_release); + task_execution_duration_ns_.store(0, std::memory_order_release); + task_execution_started_ns_.store(steady_now_ns(), std::memory_order_release); +} +void Scene_Base::set_task_execution_node_count(std::size_t count) noexcept { + task_execution_node_count_.store(count, std::memory_order_release); +} +void Scene_Base::task_execution_started() noexcept { + const std::size_t active = + task_execution_running_count_.fetch_add(1, std::memory_order_acq_rel) + 1; + std::size_t peak = + task_execution_peak_parallelism_.load(std::memory_order_acquire); + while (peak < active && + !task_execution_peak_parallelism_.compare_exchange_weak( + peak, active, std::memory_order_acq_rel, std::memory_order_acquire)) {} +} +void Scene_Base::task_execution_finished(bool failed) noexcept { + if (failed) + task_execution_failed_count_.fetch_add(1, std::memory_order_acq_rel); + task_execution_completed_count_.fetch_add(1, std::memory_order_acq_rel); + task_execution_running_count_.fetch_sub(1, std::memory_order_acq_rel); +} +void Scene_Base::finish_task_execution() noexcept { + const auto started = + task_execution_started_ns_.load(std::memory_order_acquire); + const auto now = steady_now_ns(); + task_execution_duration_ns_.store( + started != 0 && now >= started ? now - started : 0, + std::memory_order_release); } void Scene_Base::validate_renderable_scene(const Renderable_Base& renderable) const { if (renderable.real_time_data_state_->scene_lifetime.get() != scene_lifetime_.get()) { diff --git a/Kernel/src/renderive/scene/base/Scene_Base.hpp b/Kernel/src/renderive/scene/base/Scene_Base.hpp index ff975dd..22fb814 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.hpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -15,6 +16,7 @@ #include "renderive/renderable/base/Renderable_Base.hpp" #include "Scene_Lifetime.hpp" #include "Scene_Render_Context.hpp" +#include "Task_Execution_Snapshot.hpp" class Color_Cache; class Scene_Base { private: @@ -65,6 +67,8 @@ public: std::size_t renderable_count() const; Topology_Snapshot topology_snapshot() const; std::vector paint_order_snapshot() const; + [[nodiscard]] Task_Execution_Snapshot task_execution_snapshot() const noexcept; + [[nodiscard]] static std::size_t task_executor_worker_count() noexcept; std::pmr::memory_resource& memory_resource() const noexcept; std::pmr::memory_resource& upstream_memory_resource() const noexcept; Frame_Control_Strategy_Base& frame_control_strategy(); @@ -114,6 +118,11 @@ private: }; void render_loop(); void execute_taskflow(Render_Task& task); + void begin_task_execution(std::uint64_t render_sequence) noexcept; + void set_task_execution_node_count(std::size_t count) noexcept; + void task_execution_started() noexcept; + void task_execution_finished(bool failed) noexcept; + void finish_task_execution() noexcept; void validate_renderable_scene(const Renderable_Base& renderable) const; bool is_renderable_attached_locked(const Renderable_Base& renderable) const; void validate_renderable_attached_locked(const Renderable_Base& renderable) const; @@ -125,7 +134,7 @@ private: Renderable_List* render_renderables_; Renderable_List* cache_renderables_; mutable std::mutex renderable_mutex_; - std::recursive_mutex task_mutex_; + mutable std::recursive_mutex task_mutex_; std::condition_variable_any task_ready_; std::condition_variable_any render_completed_; std::thread worker_; @@ -138,6 +147,14 @@ private: bool task_pending_{}; bool rendering_{}; bool stop_{}; + std::atomic task_execution_render_sequence_{}; + std::atomic task_execution_node_count_{}; + std::atomic task_execution_completed_count_{}; + std::atomic task_execution_running_count_{}; + std::atomic task_execution_peak_parallelism_{}; + std::atomic task_execution_failed_count_{}; + std::atomic task_execution_started_ns_{}; + std::atomic task_execution_duration_ns_{}; }; class Scene_2D_Base : public Scene_Base { public: diff --git a/Kernel/src/renderive/scene/base/Task_Execution_Snapshot.hpp b/Kernel/src/renderive/scene/base/Task_Execution_Snapshot.hpp new file mode 100644 index 0000000..337de0e --- /dev/null +++ b/Kernel/src/renderive/scene/base/Task_Execution_Snapshot.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +enum class Task_Execution_State : std::uint8_t { + Idle, + Queued, + Running, + Completed, + Failed +}; + +struct Task_Execution_Snapshot { + Task_Execution_State state{Task_Execution_State::Idle}; + std::size_t worker_count{}; + std::uint64_t render_sequence{}; + std::size_t graph_node_count{}; + std::size_t completed_count{}; + std::size_t running_count{}; + std::size_t pending_count{}; + std::size_t peak_parallelism{}; + std::size_t failed_count{}; + std::uint64_t duration_ns{}; +}; diff --git a/Kernel/tests/renderive/renderable/Renderable_Task_Graph_Test.cpp b/Kernel/tests/renderive/renderable/Renderable_Task_Graph_Test.cpp index 93c3af9..74ae025 100644 --- a/Kernel/tests/renderive/renderable/Renderable_Task_Graph_Test.cpp +++ b/Kernel/tests/renderive/renderable/Renderable_Task_Graph_Test.cpp @@ -129,8 +129,25 @@ TEST(renderable_task_graph_concurrency_test, runs_independent_internal_tasks_con auto renderable = std::make_shared(scene, active, maximum); scene.attach_renderable(renderable); scene.render(); + while (active.load(std::memory_order_acquire) == 0) + std::this_thread::yield(); + const auto running = scene.task_execution_snapshot(); + EXPECT_EQ(running.state, Task_Execution_State::Running); + EXPECT_EQ(running.graph_node_count, 4u); + EXPECT_GT(running.running_count, 0u); + EXPECT_LT(running.completed_count, running.graph_node_count); scene.wait_for_render(); EXPECT_GE(maximum.load(std::memory_order_acquire), 2); + const auto execution = scene.task_execution_snapshot(); + EXPECT_EQ(execution.state, Task_Execution_State::Completed); + EXPECT_EQ(execution.worker_count, Scene_Base::task_executor_worker_count()); + EXPECT_EQ(execution.graph_node_count, 4u); + EXPECT_EQ(execution.completed_count, execution.graph_node_count); + EXPECT_EQ(execution.running_count, 0u); + EXPECT_EQ(execution.pending_count, 0u); + EXPECT_GE(execution.peak_parallelism, 2u); + EXPECT_EQ(execution.failed_count, 0u); + EXPECT_GT(execution.duration_ns, 0u); } #endif TEST(renderable_task_graph_test, rejects_tasks_from_different_graphs) { diff --git a/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp index 55b6e4b..1ce7c19 100644 --- a/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp +++ b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp @@ -38,6 +38,12 @@ TEST(scene_base_test, wait_for_render_propagates_background_render_failure) { scene.attach_renderable(renderable); scene.render(); EXPECT_THROW(scene.wait_for_render(), std::runtime_error); + const auto execution = scene.task_execution_snapshot(); + EXPECT_EQ(execution.state, Task_Execution_State::Failed); + EXPECT_EQ(execution.graph_node_count, 3u); + EXPECT_EQ(execution.failed_count, 1u); + EXPECT_EQ(execution.running_count, 0u); + EXPECT_GT(execution.duration_ns, 0u); } TEST(scene_base_test, scene_destruction_does_not_throw_after_background_render_failure) { { diff --git a/render_2D/plot/Plot_Core.cpp b/render_2D/plot/Plot_Core.cpp index 5007707..d970abc 100644 --- a/render_2D/plot/Plot_Core.cpp +++ b/render_2D/plot/Plot_Core.cpp @@ -553,6 +553,18 @@ Frame_Observer_Snapshot Plot_Core::frame_observer_snapshot() const { snapshot.next_refresh_interval_ns = state.next_refresh_interval_ns; snapshot.end_to_end_ns = state.end_to_end_ns; } + const auto task_execution = with_scene( + impl_->scene, + [](const auto& scene) { return scene.task_execution_snapshot(); }); + snapshot.task_execution_state = task_execution.state; + snapshot.task_worker_count = task_execution.worker_count; + snapshot.task_graph_node_count = task_execution.graph_node_count; + snapshot.task_completed_node_count = task_execution.completed_count; + snapshot.task_running_node_count = task_execution.running_count; + snapshot.task_pending_node_count = task_execution.pending_count; + snapshot.task_peak_parallelism = task_execution.peak_parallelism; + snapshot.task_failed_node_count = task_execution.failed_count; + snapshot.task_graph_duration_ns = task_execution.duration_ns; return snapshot; } diff --git a/render_2D/plot/Plot_Core.h b/render_2D/plot/Plot_Core.h index c10a5b4..392699a 100644 --- a/render_2D/plot/Plot_Core.h +++ b/render_2D/plot/Plot_Core.h @@ -3,6 +3,7 @@ #include "../base/Types.h" #include "../event/Event.h" #include +#include #include #include @@ -69,6 +70,15 @@ struct Frame_Observer_Snapshot { std::uint64_t render_finish_state_wait_ns{}; std::uint64_t queue_wait_ns{}; std::uint64_t end_to_end_ns{}; + Task_Execution_State task_execution_state{Task_Execution_State::Idle}; + std::size_t task_worker_count{}; + std::size_t task_graph_node_count{}; + std::size_t task_completed_node_count{}; + std::size_t task_running_node_count{}; + std::size_t task_pending_node_count{}; + std::size_t task_peak_parallelism{}; + std::size_t task_failed_node_count{}; + std::uint64_t task_graph_duration_ns{}; }; class Presentation_Sink { diff --git a/render_2D/plottable/Afterglow.cpp b/render_2D/plottable/Afterglow.cpp index 23c1858..ce72e9c 100644 --- a/render_2D/plottable/Afterglow.cpp +++ b/render_2D/plottable/Afterglow.cpp @@ -4,7 +4,6 @@ #include "../render/Blend2D_Cache.h" #include "../renderable/Render_Partition.h" #include -#include #include namespace renderive { namespace detail { @@ -66,31 +65,39 @@ void Afterglow_Control::publish() { } void Afterglow_Control::build_paint_task_graph(Renderable_Task_Graph& graph) { - const auto prepare = graph.emplace([this](const Scene_Render_Context&) { - prepare_render_frame(render_state_view()); + auto& output = impl_->render_frame; + const int partition_count = output.partitioner.graph_partition_count( + render_properties(render_state_view()).partition_count.get(), + static_cast(Scene_Base::task_executor_worker_count())); + const auto prepare = graph.emplace([this, partition_count](const Scene_Render_Context&) { + prepare_render_frame(render_state_view(), partition_count); }, "prepare afterglow"); - std::array accumulation; - std::array coloring; - for (int index = 0; index < maximum_render_partitions; ++index) { - accumulation[static_cast(index)] = graph.emplace( + std::vector accumulation; + accumulation.reserve(static_cast(partition_count)); + for (int index = 0; index < partition_count; ++index) { + const auto task = graph.emplace( [this, index](const Scene_Render_Context&) { accumulate_partition(render_state_view(), index); }, - "accumulate afterglow partition"); - graph.precede(prepare, accumulation[static_cast(index)]); + "accumulate afterglow partition " + std::to_string(index + 1)); + graph.precede(prepare, task); + accumulation.push_back(task); } const auto normalize = graph.emplace([this](const Scene_Render_Context&) { normalize_render_frame(); }, "normalize afterglow"); for (const auto task : accumulation) graph.precede(task, normalize); - for (int index = 0; index < maximum_render_partitions; ++index) { - coloring[static_cast(index)] = graph.emplace( + std::vector coloring; + coloring.reserve(static_cast(partition_count)); + for (int index = 0; index < partition_count; ++index) { + const auto task = graph.emplace( [this, index](const Scene_Render_Context&) { color_partition(render_state_view(), index); }, - "color afterglow partition"); - graph.precede(normalize, coloring[static_cast(index)]); + "color afterglow partition " + std::to_string(index + 1)); + graph.precede(normalize, task); + coloring.push_back(task); } const auto compose = add_paint_task( graph, "compose afterglow", @@ -99,7 +106,8 @@ void Afterglow_Control::build_paint_task_graph(Renderable_Task_Graph& graph) { graph.precede(task, compose); } -void Afterglow_Control::prepare_render_frame(const Render_State_View& view) { +void Afterglow_Control::prepare_render_frame(const Render_State_View& view, + int graph_partition_count) { const auto& state = render_properties(view); const auto& history = view.get(impl_->history); auto& output = impl_->render_frame; @@ -125,8 +133,8 @@ void Afterglow_Control::prepare_render_frame(const Render_State_View& view) { output.work_size = static_cast(width) * height; output.intensity.resize(output.work_size); output.pixels.resize(output.work_size); - output.active_partitions = - output.partitioner.begin(state.partition_count.get(), output.work_size); + output.active_partitions = output.partitioner.begin(graph_partition_count, + output.work_size); output.valid = true; } @@ -189,7 +197,12 @@ void Afterglow_Control::paint(Painter& painter, const Render_State_View& view) { return; painter.heatmap(output.layout.target, output.layout.width, output.layout.height, output.pixels, Image_Interpolation_Mode::Bilinear); - output.partitioner.finish(output.work_size); + const auto& state = render_properties(view); + if (output.partitioner.finish( + state.partition_count.get(), output.active_partitions, + static_cast(Scene_Base::task_executor_worker_count()), + output.work_size)) + task_graph_changed(); } } } diff --git a/render_2D/plottable/Afterglow.h b/render_2D/plottable/Afterglow.h index 4e31c54..daf5163 100644 --- a/render_2D/plottable/Afterglow.h +++ b/render_2D/plottable/Afterglow.h @@ -34,7 +34,7 @@ protected: private: struct Impl; std::unique_ptr impl_; - void prepare_render_frame(const Render_State_View& state); + void prepare_render_frame(const Render_State_View& state, int graph_partition_count); void accumulate_partition(const Render_State_View& state, int partition_index); void normalize_render_frame(); void color_partition(const Render_State_View& state, int partition_index); diff --git a/render_2D/plottable/Plottable.h b/render_2D/plottable/Plottable.h index f53d63a..6e5f4ab 100644 --- a/render_2D/plottable/Plottable.h +++ b/render_2D/plottable/Plottable.h @@ -60,6 +60,15 @@ public: template Value> Plottable_State& set(Value&& value) { Base::template set(std::forward(value)); + if constexpr (requires { &Properties::partition_count; }) { + if constexpr (std::same_as) { + if constexpr (Member == &Properties::partition_count) { + this->task_graph_changed(); + return *this; + } + } + } this->changed(); return *this; } diff --git a/render_2D/plottable/Spectrum.cpp b/render_2D/plottable/Spectrum.cpp index b6b8a94..045a4b5 100644 --- a/render_2D/plottable/Spectrum.cpp +++ b/render_2D/plottable/Spectrum.cpp @@ -4,7 +4,6 @@ #include "../render/Blend2D_Cache.h" #include "../renderable/Render_Partition.h" #include -#include #include #include #include @@ -25,7 +24,7 @@ struct Spectrum_Interaction { using Spectrum_Interaction_State = Double_State_Strategy; struct Spectrum_Render_Frame { Adaptive_Render_Partitioner partitioner; - std::array layers; + std::vector layers; int active_partitions{1}; std::size_t work_size{}; bool valid{}; @@ -244,17 +243,24 @@ void Spectrum_Control::publish() { } void Spectrum_Control::build_paint_task_graph(Renderable_Task_Graph& graph) { - const auto prepare = graph.emplace([this](const Scene_Render_Context&) { - prepare_render_frame(render_state_view()); + auto& output = impl_->render_frame; + const int partition_count = output.partitioner.graph_partition_count( + render_properties(render_state_view()).partition_count.get(), + static_cast(Scene_Base::task_executor_worker_count())); + output.layers.resize(static_cast(partition_count)); + const auto prepare = graph.emplace([this, partition_count](const Scene_Render_Context&) { + prepare_render_frame(render_state_view(), partition_count); }, "prepare spectrum"); - std::array partitions; - for (int index = 0; index < maximum_render_partitions; ++index) { - partitions[static_cast(index)] = graph.emplace( + std::vector partitions; + partitions.reserve(static_cast(partition_count)); + for (int index = 0; index < partition_count; ++index) { + const auto partition = graph.emplace( [this, index](const Scene_Render_Context&) { render_partition(render_state_view(), index); }, - "paint spectrum partition"); - graph.precede(prepare, partitions[static_cast(index)]); + "paint spectrum partition " + std::to_string(index + 1)); + graph.precede(prepare, partition); + partitions.push_back(partition); } const auto compose = add_paint_task( graph, "compose spectrum", @@ -263,7 +269,8 @@ void Spectrum_Control::build_paint_task_graph(Renderable_Task_Graph& graph) { graph.precede(task, compose); } -void Spectrum_Control::prepare_render_frame(const Render_State_View& view) { +void Spectrum_Control::prepare_render_frame(const Render_State_View& view, + int graph_partition_count) { const auto& state = render_properties(view); const auto& published_frame = view.get(impl_->frame); auto& output = impl_->render_frame; @@ -271,8 +278,8 @@ void Spectrum_Control::prepare_render_frame(const Render_State_View& view) { const Axis_Transform power_axis = impl_->power_axis->transform(view); output.valid = axes_are_orthogonal(frequency_axis, power_axis); output.work_size = published_frame ? published_frame->samples.size() : 0; - output.active_partitions = - output.partitioner.begin(state.partition_count.get(), output.work_size); + output.active_partitions = output.partitioner.begin(graph_partition_count, + output.work_size); for (int index = 0; index < output.active_partitions; ++index) output.layers[static_cast(index)].clear(); } @@ -380,7 +387,11 @@ void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) { painter.text({box.x + 4.0, box.y + 3.0}, text.str(), state.tooltip_font, state.tooltip_text_pen); } } - output.partitioner.finish(output.work_size); + if (output.partitioner.finish( + state.partition_count.get(), output.active_partitions, + static_cast(Scene_Base::task_executor_worker_count()), + output.work_size)) + task_graph_changed(); } } } diff --git a/render_2D/plottable/Spectrum.h b/render_2D/plottable/Spectrum.h index 2d9378e..a342067 100644 --- a/render_2D/plottable/Spectrum.h +++ b/render_2D/plottable/Spectrum.h @@ -65,7 +65,7 @@ protected: private: struct Impl; std::unique_ptr impl_; - void prepare_render_frame(const Render_State_View& state); + void prepare_render_frame(const Render_State_View& state, int graph_partition_count); void render_partition(const Render_State_View& state, int partition_index); void publish() override; }; diff --git a/render_2D/plottable/Waterfall.cpp b/render_2D/plottable/Waterfall.cpp index 57aa686..3a13198 100644 --- a/render_2D/plottable/Waterfall.cpp +++ b/render_2D/plottable/Waterfall.cpp @@ -3,7 +3,6 @@ #include "Plottable_Real_Time_Data.h" #include "../renderable/Render_Partition.h" #include -#include #include #include #include @@ -98,17 +97,23 @@ void Waterfall_Control::publish() { } void Waterfall_Control::build_paint_task_graph(Renderable_Task_Graph& graph) { - const auto prepare = graph.emplace([this](const Scene_Render_Context&) { - prepare_render_frame(render_state_view()); + auto& output = impl_->render_frame; + const int partition_count = output.partitioner.graph_partition_count( + render_properties(render_state_view()).partition_count.get(), + static_cast(Scene_Base::task_executor_worker_count())); + const auto prepare = graph.emplace([this, partition_count](const Scene_Render_Context&) { + prepare_render_frame(render_state_view(), partition_count); }, "prepare waterfall"); - std::array partitions; - for (int index = 0; index < maximum_render_partitions; ++index) { - partitions[static_cast(index)] = graph.emplace( + std::vector partitions; + partitions.reserve(static_cast(partition_count)); + for (int index = 0; index < partition_count; ++index) { + const auto partition = graph.emplace( [this, index](const Scene_Render_Context&) { render_partition(render_state_view(), index); }, - "raster waterfall partition"); - graph.precede(prepare, partitions[static_cast(index)]); + "raster waterfall partition " + std::to_string(index + 1)); + graph.precede(prepare, partition); + partitions.push_back(partition); } const auto compose = add_paint_task( graph, "compose waterfall", @@ -117,7 +122,8 @@ void Waterfall_Control::build_paint_task_graph(Renderable_Task_Graph& graph) { graph.precede(partition, compose); } -void Waterfall_Control::prepare_render_frame(const Render_State_View& view) { +void Waterfall_Control::prepare_render_frame(const Render_State_View& view, + int graph_partition_count) { const auto& state = render_properties(view); const auto& rows = view.get(impl_->rows); auto& output = impl_->render_frame; @@ -150,8 +156,8 @@ void Waterfall_Control::prepare_render_frame(const Render_State_View& view) { output.source_height = height; output.work_size = static_cast(width) * height; output.pixels.resize(output.work_size); - output.active_partitions = - output.partitioner.begin(state.partition_count.get(), output.work_size); + output.active_partitions = output.partitioner.begin(graph_partition_count, + output.work_size); output.valid = true; } @@ -181,7 +187,11 @@ void Waterfall_Control::paint(Painter& painter, const Render_State_View& view) { return; painter.heatmap(output.layout.target, output.layout.width, output.layout.height, output.pixels, state.interpolation_mode); - output.partitioner.finish(output.work_size); + if (output.partitioner.finish( + state.partition_count.get(), output.active_partitions, + static_cast(Scene_Base::task_executor_worker_count()), + output.work_size)) + task_graph_changed(); if(state.tooltip_enabled && interaction.tooltip.active && output.layout.target.contains(interaction.tooltip.position)) { const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view); const double frequency = frequency_axis.point_to_coord(interaction.tooltip.position); diff --git a/render_2D/plottable/Waterfall.h b/render_2D/plottable/Waterfall.h index 2123ea0..197a249 100644 --- a/render_2D/plottable/Waterfall.h +++ b/render_2D/plottable/Waterfall.h @@ -41,7 +41,7 @@ protected: private: struct Impl; std::unique_ptr impl_; - void prepare_render_frame(const Render_State_View& state); + void prepare_render_frame(const Render_State_View& state, int graph_partition_count); void render_partition(const Render_State_View& state, int partition_index); void publish() override; }; diff --git a/render_2D/renderable/Render_Partition.h b/render_2D/renderable/Render_Partition.h index 7113339..11f3c62 100644 --- a/render_2D/renderable/Render_Partition.h +++ b/render_2D/renderable/Render_Partition.h @@ -3,12 +3,10 @@ #include #include #include -#include +#include namespace renderive::detail { -inline constexpr int maximum_render_partitions = 16; - struct Render_Partition_Range { std::size_t first{}; std::size_t last{}; @@ -25,45 +23,48 @@ inline Render_Partition_Range render_partition_range(std::size_t size, class Adaptive_Render_Partitioner { public: - Adaptive_Render_Partitioner() noexcept - : automatic_count_(std::clamp( - static_cast(std::thread::hardware_concurrency()), 1, - maximum_render_partitions)) {} - - int begin(int configured_count, std::size_t work_size) noexcept { - automatic_ = configured_count == 0; - const int requested = automatic_ ? automatic_count_ : configured_count; - const int useful = static_cast( - std::min(maximum_render_partitions, - std::max(1, work_size))); - active_count_ = std::clamp(requested, 1, - std::min(maximum_render_partitions, useful)); - started_at_ = Clock::now(); - return active_count_; + [[nodiscard]] int graph_partition_count(int configured_count, + int worker_count) noexcept { + if (configured_count > 0) + return configured_count; + if (automatic_count_ == 0) + automatic_count_ = std::max(1, worker_count); + automatic_count_ = std::clamp(automatic_count_, 1, + std::max(1, worker_count)); + return automatic_count_; } - void finish(std::size_t work_size) noexcept { - if (!automatic_) - return; + int begin(int graph_partition_count, std::size_t work_size) noexcept { + const int useful = static_cast(std::min( + static_cast(std::numeric_limits::max()), + std::max(1, work_size))); + started_at_ = Clock::now(); + return std::clamp(graph_partition_count, 1, useful); + } + + [[nodiscard]] bool finish(int configured_count, int active_count, + int worker_count, std::size_t work_size) noexcept { + if (configured_count != 0) + return false; const auto elapsed = std::chrono::duration_cast(Clock::now() - started_at_); constexpr auto target = std::chrono::microseconds(1500); - if (elapsed > target * 2 && active_count_ < maximum_render_partitions && - work_size / static_cast(active_count_) >= 512) { - automatic_count_ = std::min(maximum_render_partitions, active_count_ + 1); - } else if (elapsed < target / 2 && active_count_ > 1) { - automatic_count_ = active_count_ - 1; - } else { - automatic_count_ = active_count_; - } + const int previous = automatic_count_; + const int maximum = std::max(1, worker_count); + if (elapsed > target * 2 && active_count < maximum && + work_size / static_cast(active_count) >= 512) + automatic_count_ = std::min(maximum, active_count + 1); + else if (elapsed < target / 2 && active_count > 1) + automatic_count_ = active_count - 1; + else + automatic_count_ = active_count; + return automatic_count_ != previous; } private: using Clock = std::chrono::steady_clock; Clock::time_point started_at_{}; - int automatic_count_{1}; - int active_count_{1}; - bool automatic_{true}; + int automatic_count_{}; }; } // namespace renderive::detail diff --git a/render_2D/renderable/Renderable.cpp b/render_2D/renderable/Renderable.cpp index 602d803..c60c5a3 100644 --- a/render_2D/renderable/Renderable.cpp +++ b/render_2D/renderable/Renderable.cpp @@ -88,6 +88,11 @@ void Renderable::changed() noexcept { plot_.notify_model_dirty(); } +void Renderable::task_graph_changed() noexcept { + rebuild_task_graph(); + plot_.notify_model_dirty(); +} + Size Renderable::viewport_size() const noexcept { return plot_.viewport_size(); } diff --git a/render_2D/renderable/Renderable.h b/render_2D/renderable/Renderable.h index c415aef..39bc8ca 100644 --- a/render_2D/renderable/Renderable.h +++ b/render_2D/renderable/Renderable.h @@ -50,6 +50,7 @@ protected: std::string name, Paint_Task_Function function); void changed() noexcept; + void task_graph_changed() noexcept; [[nodiscard]] Size viewport_size() const noexcept; private: diff --git a/render_2D/tests/render_2D_Integration_Tests.cpp b/render_2D/tests/render_2D_Integration_Tests.cpp index 08dc810..c94bdcb 100644 --- a/render_2D/tests/render_2D_Integration_Tests.cpp +++ b/render_2D/tests/render_2D_Integration_Tests.cpp @@ -652,7 +652,7 @@ TEST(Renderive_Core2, AxisRasterLayoutCoversAxisSwapAndEveryReversal) { EXPECT_DOUBLE_EQ(points.back().x, 130.0); EXPECT_DOUBLE_EQ(points.back().y, 30.0); } -TEST(Renderive_Core2, PartitionedPlotsBuildExplicitInternalTaskGraphs) { +TEST(Renderive_Core2, PartitionedPlotsBuildPropertyDrivenTaskGraphs) { Plot_Core plot; plot.init(); const auto root = plot.root_renderable(); @@ -680,9 +680,26 @@ TEST(Renderive_Core2, PartitionedPlotsBuildExplicitInternalTaskGraphs) { EXPECT_EQ(spectrum->get<&Spectrum::Properties::partition_count>(), 4); EXPECT_EQ(waterfall->get<&Waterfall::Properties::partition_count>(), 4); EXPECT_EQ(afterglow->get<&Afterglow::Properties::partition_count>(), 4); - EXPECT_EQ(spectrum->task_graph()->nodes().size(), 18u); - EXPECT_EQ(waterfall->task_graph()->nodes().size(), 18u); - EXPECT_EQ(afterglow->task_graph()->nodes().size(), 35u); + EXPECT_EQ(spectrum->task_graph()->nodes().size(), 6u); + EXPECT_EQ(waterfall->task_graph()->nodes().size(), 6u); + EXPECT_EQ(afterglow->task_graph()->nodes().size(), 11u); + + spectrum->set<&Spectrum::Properties::partition_count>(1); + waterfall->set<&Waterfall::Properties::partition_count>(1); + afterglow->set<&Afterglow::Properties::partition_count>(1); + spectrum->scene().publish_frame_state(); + EXPECT_EQ(spectrum->task_graph()->nodes().size(), 3u); + EXPECT_EQ(waterfall->task_graph()->nodes().size(), 3u); + EXPECT_EQ(afterglow->task_graph()->nodes().size(), 5u); + + spectrum->set<&Spectrum::Properties::partition_count>(0); + waterfall->set<&Waterfall::Properties::partition_count>(0); + afterglow->set<&Afterglow::Properties::partition_count>(0); + spectrum->scene().publish_frame_state(); + const auto workers = Scene_Base::task_executor_worker_count(); + EXPECT_EQ(spectrum->task_graph()->nodes().size(), workers + 2u); + EXPECT_EQ(waterfall->task_graph()->nodes().size(), workers + 2u); + EXPECT_EQ(afterglow->task_graph()->nodes().size(), workers * 2u + 3u); } TEST(Renderive_Core2, PlottableAxesAreDataDependenciesAndPaintOverlays) { Plot_Core plot; diff --git a/web_server/app/Gallery_Observer_Adminive.h b/web_server/app/Gallery_Observer_Adminive.h index d2cd0c8..42ffaf5 100644 --- a/web_server/app/Gallery_Observer_Adminive.h +++ b/web_server/app/Gallery_Observer_Adminive.h @@ -81,7 +81,21 @@ struct Type_Descriptor { ADMINIVE_FIELD_LABEL(T, render_state_wait_ns, "Render State 等待"), ADMINIVE_FIELD_LABEL(T, render_finish_state_wait_ns, "Render Finish 等待"), ADMINIVE_FIELD_LABEL(T, queue_wait_ns, "回放队列等待"), - ADMINIVE_FIELD_LABEL(T, end_to_end_ns, "端到端延迟")) + ADMINIVE_FIELD_LABEL(T, end_to_end_ns, "端到端延迟"), + ADMINIVE_FIELD_LABEL(T, task_execution_state, "任务执行状态") + .enum_label("空闲") + .enum_label("已排队") + .enum_label("执行中") + .enum_label("已完成") + .enum_label("失败"), + ADMINIVE_FIELD_LABEL(T, task_worker_count, "Taskflow 工作线程"), + ADMINIVE_FIELD_LABEL(T, task_graph_node_count, "任务图节点"), + ADMINIVE_FIELD_LABEL(T, task_completed_node_count, "已完成节点"), + ADMINIVE_FIELD_LABEL(T, task_running_node_count, "运行中节点"), + ADMINIVE_FIELD_LABEL(T, task_pending_node_count, "待执行节点"), + ADMINIVE_FIELD_LABEL(T, task_peak_parallelism, "峰值并行度"), + ADMINIVE_FIELD_LABEL(T, task_failed_node_count, "失败节点"), + ADMINIVE_FIELD_LABEL(T, task_graph_duration_ns, "任务图耗时")) .label("Kernel Observer"); } }; diff --git a/web_server/app/Gallery_Protocol.cpp b/web_server/app/Gallery_Protocol.cpp index 90fa97f..fb24065 100644 --- a/web_server/app/Gallery_Protocol.cpp +++ b/web_server/app/Gallery_Protocol.cpp @@ -85,7 +85,7 @@ adminive::Table_View action_view(); namespace adminive { template <> -struct Type_Descriptor { +struct { static auto get() { using T = renderive::web::gallery_detail::Case_Model; return object("renderive_gallery_case", @@ -312,6 +312,20 @@ Json dashboard_contract() { std::move(point_pair), std::move(bottleneck), described_dashboard_field( "kernel_observer", "last_event"), + described_dashboard_field( + "kernel_observer", "task_execution_state"), + described_dashboard_field( + "kernel_observer", "task_worker_count"), + described_dashboard_field( + "kernel_observer", "task_graph_node_count"), + described_dashboard_field( + "kernel_observer", "task_running_node_count"), + described_dashboard_field( + "kernel_observer", "task_pending_node_count"), + described_dashboard_field( + "kernel_observer", "task_peak_parallelism"), + described_dashboard_field( + "kernel_observer", "task_graph_duration_ns"), dashboard_field("像素超时", "client_performance.frame_request_timeout_count"), dashboard_field("最近像素龄", "client_performance.last_pixel_receive_age_ms", "milliseconds", 0) })} diff --git a/web_server/app/Gallery_Renderables.h b/web_server/app/Gallery_Renderables.h index 289af82..97e111e 100644 --- a/web_server/app/Gallery_Renderables.h +++ b/web_server/app/Gallery_Renderables.h @@ -348,7 +348,8 @@ struct Type_Descriptor { RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Spectrum, Spectrum_Properties, "spectrum", "Spectrum", property_number("frequency_point_size", "Frequency points"), - property_number("partition_count", "Render partitions"), + property_number("partition_count", "Render partitions") + .description("0 = automatic, 1 = single task, N = N parallel partitions"), property_object("frequency_range", "Frequency range"), property_number("center_frequency", "Center frequency"), property_object("sweep_frequency_range", "Sweep range"), @@ -378,7 +379,8 @@ RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Waterfall, Waterfall_Properties, "waterfall property_object("frequency_range", "Frequency range"), property_object("power_range", "Power range"), property_number("frequency_bin_count", "Frequency bins"), - property_number("partition_count", "Render partitions"), + property_number("partition_count", "Render partitions") + .description("0 = automatic, 1 = single task, N = N parallel partitions"), property_boolean("visible_range_only", "Visible range only"), property_select("interpolation_mode", "Interpolation"), property_select("color_map", "Color map"), @@ -392,7 +394,8 @@ RENDERIVE_GALLERY_DESCRIPTOR(Gallery_Afterglow, Afterglow_Properties, "afterglow property_object("power_range", "Power range"), property_number("frequency_point_size", "Frequency points"), property_number("power_point_size", "Power points"), - property_number("partition_count", "Render partitions"), + property_number("partition_count", "Render partitions") + .description("0 = automatic, 1 = single task, N = N parallel partitions"), property_boolean("interpolate", "Interpolate power"), property_number("attenuation_rate", "Attenuation"), property_select("color_map", "Color map")); diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index a0bb4d8..503f1cf 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -232,7 +232,7 @@ TEST(RenderiveWebGallery, AdminiveCatalogCoversEveryControlCaseAndThreeModes) { TEST(RenderiveWebGallery, CatalogOwnsDashboardFieldsAndDynamicBehavior) { const nlohmann::json catalog = parse_json(Gallery_Protocol::catalog_json()); const auto& dashboard = catalog.at("dashboard"); - EXPECT_EQ(dashboard.at("performance").at("fields").size(), 15U); + EXPECT_EQ(dashboard.at("performance").at("fields").size(), 22U); EXPECT_EQ(dashboard.at("limits").at("fields").size(), 4U); EXPECT_EQ(dashboard.at("observer").at("sections").size(), 4U); std::set client_sources; @@ -291,6 +291,13 @@ TEST(RenderiveWebGallery, ObserverTelemetryAndDashboardUseAdminiveDescriptors) { for (const auto& field : feedback_descriptor.at("fields")) EXPECT_TRUE(feedback.contains(field.at("name").get())); EXPECT_EQ(observer.at("mode"), "manual"); + EXPECT_GT(observer.at("task_worker_count").get(), 0U); + EXPECT_TRUE(observer.contains("task_execution_state")); + EXPECT_TRUE(observer.contains("task_graph_node_count")); + EXPECT_TRUE(observer.contains("task_running_node_count")); + EXPECT_TRUE(observer.contains("task_pending_node_count")); + EXPECT_TRUE(observer.contains("task_peak_parallelism")); + EXPECT_TRUE(observer.contains("task_graph_duration_ns")); EXPECT_FALSE(telemetry.contains("low_latency_limit")); } @@ -344,6 +351,13 @@ TEST(RenderiveWebGallery, AdminiveResourcesPatchTheRealRenderableState) { EXPECT_EQ(spectrum.at("descriptor").at("name"), "spectrum"); EXPECT_TRUE(spectrum.at("data").at("frequency_range").is_object()); EXPECT_EQ(spectrum.at("data").at("frequency_point_size"), 512); + const auto partition_field = std::find_if( + spectrum.at("descriptor").at("fields").begin(), + spectrum.at("descriptor").at("fields").end(), + [](const auto& field) { return field.at("name") == "partition_count"; }); + ASSERT_NE(partition_field, spectrum.at("descriptor").at("fields").end()); + EXPECT_EQ(partition_field->at("presentation").at("description"), + "0 = automatic, 1 = single task, N = N parallel partitions"); response = response_json(session.handle(gallery_request( Gallery_Request_Kind::Patch, @@ -367,6 +381,39 @@ TEST(RenderiveWebGallery, AdminiveResourcesPatchTheRealRenderableState) { EXPECT_EQ(foreign.at("type"), "error"); } +TEST(RenderiveWebGallery, PartitionControlRebuildsTheObservedTaskGraph) { + Gallery_Plot_Session session; + ASSERT_EQ(response_json(session.handle(gallery_request( + Gallery_Request_Kind::Open, + open_message("spectrum", "manual")))).at("type"), + "case_state"); + + const auto render = [&session] { + EXPECT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state"); + EXPECT_EQ(invoke_action(session, "mode_refresh").at("type"), "case_state"); + return invoke_action(session, "mode_render").at("telemetry").at("kernel_observer"); + }; + + ASSERT_EQ(patch_controls(session, {{"partition_count", 1}}, "spectrum").at("type"), + "case_state"); + const auto single = render(); + EXPECT_EQ(single.at("task_execution_state"), "Completed"); + EXPECT_EQ(single.at("task_completed_node_count"), + single.at("task_graph_node_count")); + EXPECT_EQ(single.at("task_running_node_count"), 0); + EXPECT_EQ(single.at("task_pending_node_count"), 0); + + ASSERT_EQ(patch_controls(session, {{"partition_count", 4}}, "spectrum").at("type"), + "case_state"); + const auto partitioned = render(); + EXPECT_EQ(partitioned.at("task_graph_node_count").get(), + single.at("task_graph_node_count").get() + 3U); + EXPECT_EQ(partitioned.at("task_completed_node_count"), + partitioned.at("task_graph_node_count")); + EXPECT_GT(partitioned.at("task_peak_parallelism").get(), 0U); + EXPECT_GT(partitioned.at("task_graph_duration_ns").get(), 0U); +} + TEST(RenderiveWebGallery, AxisSwapAndEveryPlotDirectionAreEditableAndRenderable) { const auto patch = [](Gallery_Plot_Session& session, std::string_view target, nlohmann::json value) {