diff --git a/Kernel/src/renderive/render_graph/External_Operation.cpp b/Kernel/src/renderive/render_graph/External_Operation.cpp index 5c3dd97..3b0a9a6 100644 --- a/Kernel/src/renderive/render_graph/External_Operation.cpp +++ b/Kernel/src/renderive/render_graph/External_Operation.cpp @@ -42,8 +42,12 @@ void External_Operation::on_complete(Completion completion) const { error = state_->error; invoke = true; } - if (invoke) - completion(std::move(error)); + if (invoke) { + try { + completion(std::move(error)); + } catch (...) { + } + } } External_Operation::operator bool() const noexcept { diff --git a/Kernel/src/renderive/render_graph/External_Operation.hpp b/Kernel/src/renderive/render_graph/External_Operation.hpp index cdf5189..8028db8 100644 --- a/Kernel/src/renderive/render_graph/External_Operation.hpp +++ b/Kernel/src/renderive/render_graph/External_Operation.hpp @@ -13,6 +13,9 @@ public: External_Operation() = default; + // Completion callbacks are notification boundaries. Exceptions thrown by + // them are contained regardless of whether completion happened before or + // after subscription. void on_complete(Completion completion) const; [[nodiscard]] explicit operator bool() const noexcept; diff --git a/Kernel/src/renderive/render_graph/Frame_Analysis.cpp b/Kernel/src/renderive/render_graph/Frame_Analysis.cpp index d29ca88..e62b99b 100644 --- a/Kernel/src/renderive/render_graph/Frame_Analysis.cpp +++ b/Kernel/src/renderive/render_graph/Frame_Analysis.cpp @@ -26,6 +26,8 @@ double percentile(std::vector values, double probability) { struct Node_Samples { std::vector durations; + std::vector cpu_durations; + std::vector external_durations; std::vector waits; std::size_t critical_count{}; }; @@ -50,6 +52,18 @@ Node_Statistics statistics_for(Render_Node_Id id, const Node_Samples& samples) { result.p99_ns = percentile(samples.durations, 0.99); result.minimum_ns = *std::min_element(samples.durations.begin(), samples.durations.end()); result.maximum_ns = *std::max_element(samples.durations.begin(), samples.durations.end()); + const auto cpu_sum = std::accumulate( + samples.cpu_durations.begin(), samples.cpu_durations.end(), + std::uint64_t{}); + const auto external_sum = std::accumulate( + samples.external_durations.begin(), samples.external_durations.end(), + std::uint64_t{}); + result.average_cpu_ns = static_cast(cpu_sum) / + samples.cpu_durations.size(); + result.average_external_ns = static_cast(external_sum) / + samples.external_durations.size(); + result.p95_cpu_ns = percentile(samples.cpu_durations, 0.95); + result.p95_external_ns = percentile(samples.external_durations, 0.95); result.critical_path_frequency = samples.critical_count; result.average_scheduler_wait_ns = samples.waits.empty() ? 0.0 @@ -227,6 +241,8 @@ std::vector analyze_node_statistics( for (const auto& node : frame.nodes) { auto& values = samples[node.node_id]; values.durations.push_back(node.duration_ns); + values.cpu_durations.push_back(node.cpu_duration_ns); + values.external_durations.push_back(node.external_duration_ns); values.waits.push_back(node.scheduler_wait_ns); values.critical_count += node.on_critical_path ? 1 : 0; } diff --git a/Kernel/src/renderive/render_graph/Frame_Analysis.hpp b/Kernel/src/renderive/render_graph/Frame_Analysis.hpp index 794d0af..4577298 100644 --- a/Kernel/src/renderive/render_graph/Frame_Analysis.hpp +++ b/Kernel/src/renderive/render_graph/Frame_Analysis.hpp @@ -52,6 +52,10 @@ struct Node_Statistics { double p99_ns{}; std::uint64_t minimum_ns{}; std::uint64_t maximum_ns{}; + double average_cpu_ns{}; + double average_external_ns{}; + double p95_cpu_ns{}; + double p95_external_ns{}; std::size_t critical_path_frequency{}; double average_scheduler_wait_ns{}; }; diff --git a/Kernel/src/renderive/render_graph/Node_Diagnostics.hpp b/Kernel/src/renderive/render_graph/Node_Diagnostics.hpp new file mode 100644 index 0000000..ade669d --- /dev/null +++ b/Kernel/src/renderive/render_graph/Node_Diagnostics.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +struct Node_Diagnostic_Attachment { + std::string type; + std::string content; +}; + +class Scene_Base; + +class Node_Diagnostic_Sink { +public: + Node_Diagnostic_Sink() = default; + + [[nodiscard]] bool enabled() const noexcept { + return attachments_ != nullptr; + } + + void attach(std::string type, std::string content) const { + if (!attachments_) + return; + attachments_->push_back( + {std::move(type), std::move(content)}); + } + +private: + explicit Node_Diagnostic_Sink( + std::vector& attachments) noexcept + : attachments_(&attachments) {} + + std::vector* attachments_{}; + + friend class Scene_Base; +}; diff --git a/Kernel/src/renderive/scene/base/Abstract_Frame.hpp b/Kernel/src/renderive/scene/base/Abstract_Frame.hpp index 14fe141..bc5d71e 100644 --- a/Kernel/src/renderive/scene/base/Abstract_Frame.hpp +++ b/Kernel/src/renderive/scene/base/Abstract_Frame.hpp @@ -4,6 +4,7 @@ #include #include #include +#include "renderive/render_graph/Node_Diagnostics.hpp" #include "renderive/render_graph/Render_Plan.hpp" enum class Node_Execution_Status : std::uint8_t { @@ -28,7 +29,9 @@ enum class Node_Metric_Kind : std::uint8_t { submit_duration_ns, gpu_fence_wait_ns, gpu_render_duration_ns, + gpu_transition_duration_ns, gpu_copy_duration_ns, + gpu_total_duration_ns, readback_duration_ns, backend_resource_version, backend_frame_index, @@ -60,6 +63,7 @@ struct Node_Execution { std::uint32_t worker_id{}; Node_Execution_Status status{Node_Execution_Status::pending}; Node_Execution_Metrics metrics; + std::vector attachments; [[nodiscard]] std::uint64_t duration_ns() const noexcept; [[nodiscard]] std::uint64_t cpu_duration_ns() const noexcept; [[nodiscard]] std::uint64_t external_duration_ns() const noexcept; diff --git a/Kernel/src/renderive/scene/base/Render_Contexts.hpp b/Kernel/src/renderive/scene/base/Render_Contexts.hpp index 65c7830..aa18f7e 100644 --- a/Kernel/src/renderive/scene/base/Render_Contexts.hpp +++ b/Kernel/src/renderive/scene/base/Render_Contexts.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include "renderive/render_graph/Node_Diagnostics.hpp" #include "Frame_Render_Snapshot.hpp" class Color_Cache; @@ -10,6 +11,7 @@ struct Prepare_Render_Context { const Frame_Render_Snapshot& frame; const Renderable_Frame_State& renderable; Node_Execution_Metrics* metrics{}; + Node_Diagnostic_Sink diagnostics; template void publish(std::shared_ptr data) const { @@ -27,6 +29,7 @@ struct Paint_Render_Context { const Renderable_Frame_State& renderable; Color_Cache& color_cache; Node_Execution_Metrics* metrics{}; + Node_Diagnostic_Sink diagnostics; }; struct Composite_Render_Context { @@ -34,11 +37,13 @@ struct Composite_Render_Context { const Renderable_Frame_State* renderable{}; const Color_Cache* color_cache{}; Node_Execution_Metrics* metrics{}; + Node_Diagnostic_Sink diagnostics; }; struct Scene_Render_Context { const Frame_Render_Snapshot& frame; Node_Execution_Metrics* metrics{}; + Node_Diagnostic_Sink diagnostics; template [[nodiscard]] std::shared_ptr prepared( diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp index 08c5749..05a374a 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -1162,19 +1162,23 @@ void Scene_Base::execute_render_graph(Render_Task& task) { auto& executor = Execution_Context::executor(); renderive::render_graph::detail::Render_Graph_Runtime runtime( *task.plan, execution_slots, - [this, &task, &snapshot]( + [this, &task, &snapshot, &execution_slots]( std::size_t execution_index, Node_Execution_Metrics* metrics) -> Node_Execution_Result { const auto& node = task.plan->graph.nodes.at(execution_index); const auto& binding = task.execution_bindings.at(execution_index); + Node_Diagnostic_Sink diagnostics; + if (auto* execution = execution_slots.at(execution_index)) + diagnostics = Node_Diagnostic_Sink(execution->attachments); Render_Execution_Scope scope(*this); switch (node.kind) { case Render_Node_Kind::prepare: { const auto& state = snapshot.renderables.at( binding.renderable_index.value()); std::get(binding.function)( - Prepare_Render_Context{snapshot, state, metrics}); + Prepare_Render_Context{snapshot, state, metrics, + diagnostics}); return Node_Execution_Result::completed(); } case Render_Node_Kind::paint: { @@ -1185,7 +1189,8 @@ void Scene_Base::execute_render_graph(Render_Task& task) { "paint node has no frame color cache"); std::get(binding.function)( Paint_Render_Context{snapshot, state, - *state.paint_buffer_, metrics}); + *state.paint_buffer_, metrics, + diagnostics}); return Node_Execution_Result::completed(); } case Render_Node_Kind::composite: { @@ -1199,12 +1204,12 @@ void Scene_Base::execute_render_graph(Render_Task& task) { : nullptr; std::get(binding.function)( Composite_Render_Context{snapshot, state, cache, - metrics}); + metrics, diagnostics}); return Node_Execution_Result::completed(); } case Render_Node_Kind::render: return std::get(binding.function)( - Scene_Render_Context{snapshot, metrics}); + Scene_Render_Context{snapshot, metrics, diagnostics}); } throw std::logic_error("unknown render node kind"); }, diff --git a/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp b/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp index 53af8cc..f4b4217 100644 --- a/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp +++ b/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp @@ -181,6 +181,13 @@ TEST(render_dag_test, ASSERT_EQ(analysis.workers.size(), 1U); EXPECT_EQ(analysis.workers.front().work_duration_ns, 20U); EXPECT_DOUBLE_EQ(analysis.workers.front().utilization, 0.1); + const auto statistics = analyze_node_statistics( + std::span(&analysis, 1)); + ASSERT_EQ(statistics.size(), 1U); + EXPECT_DOUBLE_EQ(statistics.front().average_cpu_ns, 20.0); + EXPECT_DOUBLE_EQ(statistics.front().average_external_ns, 180.0); + EXPECT_DOUBLE_EQ(statistics.front().p95_cpu_ns, 20.0); + EXPECT_DOUBLE_EQ(statistics.front().p95_external_ns, 180.0); } TEST(render_dag_test, failed_capture_ticket_is_retried_until_the_requested_frame_completes) { diff --git a/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp b/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp index 158dfe8..88b4fe6 100644 --- a/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp +++ b/Kernel/tests/renderive/render_graph/Render_Graph_Runtime_Test.cpp @@ -106,6 +106,21 @@ TEST(external_operation_test, completion_before_subscription_is_delivered_once) EXPECT_EQ(completion_count, 1); } +TEST(external_operation_test, + callback_exceptions_are_contained_for_both_completion_orders) { + External_Operation_Source completed_first; + const auto completed_operation = completed_first.operation(); + ASSERT_TRUE(completed_first.complete()); + EXPECT_NO_THROW(completed_operation.on_complete( + [](std::exception_ptr) { throw std::runtime_error("late callback"); })); + + External_Operation_Source subscribed_first; + const auto subscribed_operation = subscribed_first.operation(); + subscribed_operation.on_complete( + [](std::exception_ptr) { throw std::runtime_error("early callback"); }); + EXPECT_TRUE(subscribed_first.complete()); +} + TEST(render_graph_runtime_test, external_successor_stays_blocked_until_operation_completes) { const auto plan = two_node_plan(); diff --git a/render_3D/CMakeLists.txt b/render_3D/CMakeLists.txt index 7b8db82..2885ef7 100644 --- a/render_3D/CMakeLists.txt +++ b/render_3D/CMakeLists.txt @@ -121,6 +121,9 @@ if (RENDERIVE_BUILD_TESTS) set(Renderive_render_3D_test_target "Renderive_render_3D_${Renderive_render_3D_test_name}_${Renderive_render_3D_test_hash}") add_executable("${Renderive_render_3D_test_target}" "${Renderive_render_3D_test_source}") target_link_libraries("${Renderive_render_3D_test_target}" PRIVATE Renderive_render_3D GTest::gtest_main) + if (Renderive_render_3D_test_name STREQUAL "Gpu_Completion_Service_Tests") + target_link_libraries("${Renderive_render_3D_test_target}" PRIVATE volk::volk_headers) + endif () renderive_stage_render_3D_runtime("${Renderive_render_3D_test_target}") add_test(NAME "${Renderive_render_3D_test_target}" COMMAND "${Renderive_render_3D_test_target}") endforeach () diff --git a/render_3D/render_3D/Point_Scene.cpp b/render_3D/render_3D/Point_Scene.cpp index bb55ca5..3566545 100644 --- a/render_3D/render_3D/Point_Scene.cpp +++ b/render_3D/render_3D/Point_Scene.cpp @@ -15,7 +15,9 @@ #include #include #include +#include #include +#include #include namespace renderive::render_3d { @@ -41,38 +43,46 @@ float datoviz_wheel_step(float pixel_delta, float angle_delta) noexcept { } void publish_trace(Node_Execution_Metrics* metrics, - const detail::Datoviz_Frame_Trace& trace) { - if (!metrics) - return; - metrics->set(Node_Metric_Kind::queue_wait_ns, - trace.render_domain_queue_wait_ns); - metrics->set(Node_Metric_Kind::apply_duration_ns, trace.apply_ns); - metrics->set(Node_Metric_Kind::plan_emit_duration_ns, trace.emit_ns); - metrics->set(Node_Metric_Kind::backend_execute_duration_ns, - trace.execute_ns); - metrics->set(Node_Metric_Kind::submit_duration_ns, trace.submit_ns); - metrics->set(Node_Metric_Kind::gpu_fence_wait_ns, - trace.gpu_fence_wait_ns); - metrics->set(Node_Metric_Kind::readback_duration_ns, - trace.readback_ns); - if (trace.gpu) { - metrics->set(Node_Metric_Kind::gpu_render_duration_ns, - trace.gpu->render_ns); - metrics->set(Node_Metric_Kind::gpu_copy_duration_ns, - trace.gpu->copy_ns); + Node_Diagnostic_Sink diagnostics, + detail::Datoviz_Frame_Trace trace) { + if (metrics) { + metrics->set(Node_Metric_Kind::queue_wait_ns, + trace.render_domain_queue_wait_ns); + metrics->set(Node_Metric_Kind::apply_duration_ns, trace.apply_ns); + metrics->set(Node_Metric_Kind::plan_emit_duration_ns, trace.emit_ns); + metrics->set(Node_Metric_Kind::backend_execute_duration_ns, + trace.execute_ns); + metrics->set(Node_Metric_Kind::submit_duration_ns, trace.submit_ns); + metrics->set(Node_Metric_Kind::gpu_fence_wait_ns, + trace.gpu_fence_wait_ns); + metrics->set(Node_Metric_Kind::readback_duration_ns, + trace.readback_ns); + if (trace.gpu) { + metrics->set(Node_Metric_Kind::gpu_render_duration_ns, + trace.gpu->render_ns); + metrics->set(Node_Metric_Kind::gpu_transition_duration_ns, + trace.gpu->transition_ns); + metrics->set(Node_Metric_Kind::gpu_copy_duration_ns, + trace.gpu->copy_ns); + metrics->set(Node_Metric_Kind::gpu_total_duration_ns, + trace.gpu->total_ns); + } + metrics->set(Node_Metric_Kind::backend_resource_version, + trace.artifact_resource_version); + metrics->set(Node_Metric_Kind::backend_frame_index, + trace.artifact_frame_index); + metrics->set(Node_Metric_Kind::backend_artifact_status, + trace.artifact_status); + metrics->set(Node_Metric_Kind::backend_validation_code, + trace.validation_code); + metrics->set(Node_Metric_Kind::backend_validation_command, + trace.validation_command_index); + metrics->set(Node_Metric_Kind::backend_artifact_json_bytes, + trace.artifact_json.size()); } - metrics->set(Node_Metric_Kind::backend_resource_version, - trace.artifact_resource_version); - metrics->set(Node_Metric_Kind::backend_frame_index, - trace.artifact_frame_index); - metrics->set(Node_Metric_Kind::backend_artifact_status, - trace.artifact_status); - metrics->set(Node_Metric_Kind::backend_validation_code, - trace.validation_code); - metrics->set(Node_Metric_Kind::backend_validation_command, - trace.validation_command_index); - metrics->set(Node_Metric_Kind::backend_artifact_json_bytes, - trace.artifact_json.size()); + if (!trace.artifact_json.empty()) + diagnostics.attach("datoviz.drp2.artifact.json", + std::move(trace.artifact_json)); } struct Scene_Model { @@ -301,22 +311,100 @@ struct Basic_Point_Scene final const std::uint64_t frame_sequence = context.frame.render_sequence; Node_Execution_Metrics* const metrics = context.metrics; + const Node_Diagnostic_Sink diagnostics = context.diagnostics; + const bool observe = metrics != nullptr; auto source = std::make_shared(); const auto operation = source->operation(); - const auto queued_at = std::chrono::steady_clock::now(); + using Pending_Frame = detail::Datoviz_Visual_Backend::Pending_Frame; + static_assert(std::is_nothrow_move_constructible_v); + using Completion_Result = detail::Gpu_Completion_Service::Result; + static_assert(std::is_nothrow_move_assignable_v); + struct Async_Frame_State { + std::optional pending; + Completion_Result completion; + }; + auto async_frame = std::make_shared(); + auto render_completion = std::make_shared< + detail::Render_Domain::Prepared_Task>( + render_domain.prepare( + [this, async_frame, metrics, diagnostics, source]() mutable { + if (!async_frame->pending) { + static_cast(source->fail( + std::make_exception_ptr(std::logic_error( + "GPU completion has no pending frame")))); + return; + } + auto pending = std::move(*async_frame->pending); + async_frame->pending.reset(); + pending.trace.gpu_fence_wait_ns = + async_frame->completion.wait_duration_ns; + if (async_frame->completion.error) { + try { + backend->discard(std::move(pending)); + static_cast(source->fail( + std::move(async_frame->completion.error))); + } catch (...) { + static_cast(source->fail( + std::current_exception())); + } + return; + } + try { + auto completed = backend->collect( + std::move(pending)); + auto frame = std::move(completed.frame); + if (metrics && frame) { + metrics->set( + Node_Metric_Kind::pixel_count, + static_cast( + frame->extent.width) * + frame->extent.height); + } + publish_trace(metrics, diagnostics, + std::move(completed.trace)); + { + std::lock_guard lock(frame_mutex); + latest = std::move(frame); + } + static_cast(source->complete()); + } catch (...) { + static_cast(source->fail( + std::current_exception())); + } + })); + auto completion = std::make_shared< + detail::Gpu_Completion_Service::Reservation>( + gpu_completion.prepare( + [this, async_frame, render_completion]( + Completion_Result result) noexcept { + async_frame->completion = std::move(result); + render_domain.post(std::move(*render_completion)); + }, + observe)); + const auto queued_at = observe + ? std::chrono::steady_clock::now() + : std::chrono::steady_clock::time_point{}; try { render_domain.post( [this, scene_state, scene_revision, frame_sequence, prepared, - metrics, source, queued_at] { + source, async_frame, completion, observe, + queued_at] { try { - const auto queue_wait = - std::chrono::duration_cast( - std::chrono::steady_clock::now() - queued_at) - .count(); + std::uint64_t queue_wait_ns{}; + if (observe) { + const auto queue_wait = + std::chrono::duration_cast< + std::chrono::nanoseconds>( + std::chrono::steady_clock::now() - + queued_at).count(); + queue_wait_ns = queue_wait > 0 + ? static_cast(queue_wait) + : 0; + } auto pending = backend->submit( scene_state, scene_revision, *prepared, - frame_sequence, metrics != nullptr); + frame_sequence, observe); if (!pending) { { std::lock_guard lock(frame_mutex); @@ -326,70 +414,10 @@ struct Basic_Point_Scene final return; } pending->trace.render_domain_queue_wait_ns = - queue_wait > 0 - ? static_cast(queue_wait) - : 0; - gpu_completion.watch( - pending->device, pending->fence, - [this, pending = *pending, metrics, source]( - detail::Gpu_Completion_Service::Result result) - mutable { - pending.trace.gpu_fence_wait_ns = - result.wait_duration_ns; - try { - render_domain.post( - [this, pending = std::move(pending), - metrics, source, - wait_error = std::move(result.error)]() - mutable { - if (wait_error) { - try { - backend->discard( - std::move(pending)); - static_cast( - source->fail(std::move( - wait_error))); - } catch (...) { - static_cast( - source->fail( - std::current_exception())); - } - return; - } - try { - auto completed = - backend->collect( - std::move(pending)); - auto frame = - std::move(completed.frame); - if (metrics && frame) { - metrics->set( - Node_Metric_Kind:: - pixel_count, - static_cast< - std::uint64_t>( - frame->extent.width) * - frame->extent.height); - } - publish_trace( - metrics, completed.trace); - { - std::lock_guard lock( - frame_mutex); - latest = std::move(frame); - } - static_cast( - source->complete()); - } catch (...) { - static_cast(source->fail( - std::current_exception())); - } - }); - } catch (...) { - static_cast(source->fail( - std::current_exception())); - } - }); + queue_wait_ns; + auto& owned = async_frame->pending.emplace( + std::move(*pending)); + completion->watch(owned.device, owned.fence); } catch (...) { static_cast( source->fail(std::current_exception())); diff --git a/render_3D/render_3D/detail/Datoviz_Frame_Trace.h b/render_3D/render_3D/detail/Datoviz_Frame_Trace.h index b0d4100..c89361a 100644 --- a/render_3D/render_3D/detail/Datoviz_Frame_Trace.h +++ b/render_3D/render_3D/detail/Datoviz_Frame_Trace.h @@ -8,11 +8,14 @@ namespace renderive::render_3d::detail { struct Datoviz_Gpu_Timing { std::uint64_t render_ns{}; + std::uint64_t transition_ns{}; std::uint64_t copy_ns{}; + std::uint64_t total_ns{}; }; struct Datoviz_Frame_Trace { std::uint64_t render_sequence{}; + bool observed{}; std::uint64_t render_domain_queue_wait_ns{}; std::uint64_t apply_ns{}; std::uint64_t emit_ns{}; diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp index a7ab670..758ebc0 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp @@ -238,7 +238,6 @@ public: dvz_buffer_usage(readback_, VK_BUFFER_USAGE_TRANSFER_DST_BIT); if (dvz_buffer_create(readback_) != 0) throw std::runtime_error("failed to create Datoviz readback buffer"); - initialize_timestamps(device, queue); } catch (...) { destroy(); throw; @@ -247,9 +246,15 @@ public: ~Frame_Target() { destroy(); } - void begin() { + void begin(bool observe) { if (in_flight_) throw std::logic_error("Datoviz frame target is still in flight"); + observing_ = observe; + if (observing_ && !timestamps_initialized_) { + initialize_timestamps( + dvz_gpu_ctx_device(gpu_context_), + dvz_gpu_ctx_queue(gpu_context_, DVZ_QUEUE_MAIN)); + } dvz_cmd_reset(commands_); if (dvz_cmd_begin_result(commands_) != 0) throw std::runtime_error("failed to begin Datoviz command buffer"); @@ -278,7 +283,7 @@ public: dvz_barrier_image_mip(image_barrier, 0, 1); dvz_barrier_image_layers(image_barrier, 0, 1); dvz_cmd_barriers(commands_, &barriers); - if (timestamps_supported_) { + if (observing_ && timestamps_supported_) { const VkCommandBuffer command_buffer = dvz_commands_handle(commands_); vkCmdResetQueryPool(command_buffer, query_pool_, 0, 4); @@ -315,7 +320,7 @@ public: if (!recording_) throw std::logic_error("Datoviz frame target is not recording"); const VkCommandBuffer command_buffer = dvz_commands_handle(commands_); - if (timestamps_supported_) { + if (observing_ && timestamps_supported_) { vkCmdWriteTimestamp( command_buffer, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, @@ -338,7 +343,7 @@ public: dvz_barrier_image_mip(image_barrier, 0, 1); dvz_barrier_image_layers(image_barrier, 0, 1); dvz_cmd_barriers(commands_, &image_barriers); - if (timestamps_supported_) { + if (observing_ && timestamps_supported_) { vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, query_pool_, 2); } @@ -360,7 +365,7 @@ public: dvz_barrier_buffer_access(buffer_barrier, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_ACCESS_2_HOST_READ_BIT); dvz_cmd_barriers(commands_, &buffer_barriers); - if (timestamps_supported_) { + if (observing_ && timestamps_supported_) { vkCmdWriteTimestamp(command_buffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, query_pool_, 3); @@ -390,9 +395,11 @@ public: result.gpu_timing = collect_gpu_timing(); } catch (...) { in_flight_ = false; + observing_ = false; throw; } in_flight_ = false; + observing_ = false; return result; } @@ -400,6 +407,7 @@ public: if (!in_flight_) throw std::logic_error("Datoviz frame target has no pending frame"); in_flight_ = false; + observing_ = false; } [[nodiscard]] VkDevice device() const { @@ -418,10 +426,12 @@ public: if (recording_ && commands_ != nullptr) dvz_cmd_reset(commands_); recording_ = false; + observing_ = false; } private: void initialize_timestamps(DvzDevice* device, DvzQueue* queue) noexcept { + timestamps_initialized_ = true; if (device == nullptr || queue == nullptr || vkGetPhysicalDeviceQueueFamilyProperties == nullptr || vkGetPhysicalDeviceProperties == nullptr || @@ -467,7 +477,8 @@ private: [[nodiscard]] std::optional collect_gpu_timing() const noexcept { - if (!timestamps_supported_ || query_pool_ == VK_NULL_HANDLE) + if (!observing_ || !timestamps_supported_ || + query_pool_ == VK_NULL_HANDLE) return std::nullopt; const VkDevice device = dvz_device_handle(dvz_gpu_ctx_device(gpu_context_)); @@ -497,7 +508,9 @@ private: }; return Datoviz_Gpu_Timing{ elapsed(timestamps[0], timestamps[1]), - elapsed(timestamps[2], timestamps[3])}; + elapsed(timestamps[1], timestamps[2]), + elapsed(timestamps[2], timestamps[3]), + elapsed(timestamps[0], timestamps[3])}; } void destroy() noexcept { @@ -556,6 +569,8 @@ private: std::uint32_t timestamp_valid_bits_{}; bool recording_{}; bool in_flight_{}; + bool observing_{}; + bool timestamps_initialized_{}; bool timestamps_supported_{}; }; @@ -1023,15 +1038,17 @@ std::optional Datoviz_Visual_Backend::submit( const Scene_State& scene, std::uint64_t scene_revision, const Prepared_Point& point, std::uint64_t frame_sequence, - bool capture_trace) { + bool observe) { require_domain(); if (scene.viewport.empty()) return std::nullopt; Datoviz_Frame_Trace trace; trace.render_sequence = frame_sequence; - auto phase_started = trace_now_ns(); + trace.observed = observe; + std::uint64_t phase_started = observe ? trace_now_ns() : 0; apply(scene, scene_revision, point); - trace.apply_ns = trace_now_ns() - phase_started; + if (observe) + trace.apply_ns = trace_now_ns() - phase_started; if (target_ == nullptr || target_extent_ != scene.viewport) { target_.reset(); @@ -1040,36 +1057,42 @@ Datoviz_Visual_Backend::submit( ++target_generation_); } - target_->begin(); + target_->begin(observe); DvzSceneFrameArtifact* artifact{}; try { - phase_started = trace_now_ns(); + if (observe) + phase_started = trace_now_ns(); artifact = emit(scene); - trace.emit_ns = trace_now_ns() - phase_started; + if (observe) + trace.emit_ns = trace_now_ns() - phase_started; } catch (...) { target_->abort(); throw; } - trace.artifact_status = static_cast( - dvz_scene_frame_artifact_status(artifact)); - trace.artifact_resource_version = - dvz_scene_frame_artifact_resource_version(artifact); - trace.artifact_frame_index = - dvz_scene_frame_artifact_frame_index(artifact); + if (observe) { + trace.artifact_status = static_cast( + dvz_scene_frame_artifact_status(artifact)); + trace.artifact_resource_version = + dvz_scene_frame_artifact_resource_version(artifact); + trace.artifact_frame_index = + dvz_scene_frame_artifact_frame_index(artifact); + } const DvzDrp2CommandStream* stream = dvz_scene_frame_artifact_stream(artifact); const DvzStreamFrame target_frame = target_->stream_frame(); - phase_started = trace_now_ns(); + if (observe) + phase_started = trace_now_ns(); const bool attached = stream != nullptr && dvz_drp2_runtime_attach_frame_target( runtime_, color_target_id, &target_frame); const DvzDrp2ValidationResult result = attached ? dvz_drp2_runtime_execute(runtime_, stream) : DvzDrp2ValidationResult{}; - trace.execute_ns = trace_now_ns() - phase_started; + if (observe) + trace.execute_ns = trace_now_ns() - phase_started; trace.validation_ok = attached && result.ok; trace.validation_code = static_cast(result.code); trace.validation_command_index = result.command_index; - if (capture_trace || !trace.validation_ok) { + if (observe || !trace.validation_ok) { if (char* json = dvz_scene_frame_artifact_json( artifact, "renderive_frame")) { trace.artifact_json = json; @@ -1095,9 +1118,11 @@ Datoviz_Visual_Backend::submit( throw std::runtime_error(std::move(message)); } - phase_started = trace_now_ns(); + if (observe) + phase_started = trace_now_ns(); target_->submit(); - trace.submit_ns = trace_now_ns() - phase_started; + if (observe) + trace.submit_ns = trace_now_ns() - phase_started; return Pending_Frame{target_->device(), target_->fence(), scene.viewport, frame_sequence, target_->generation(), std::move(trace)}; @@ -1109,9 +1134,12 @@ Datoviz_Visual_Backend::Completed_Frame Datoviz_Visual_Backend::collect( if (target_ == nullptr || target_->generation() != pending.target_generation) throw std::logic_error("Datoviz pending frame target no longer exists"); - const std::uint64_t readback_started = trace_now_ns(); + const std::uint64_t readback_started = pending.trace.observed + ? trace_now_ns() + : 0; auto collection = target_->collect(); - pending.trace.readback_ns = trace_now_ns() - readback_started; + if (pending.trace.observed) + pending.trace.readback_ns = trace_now_ns() - readback_started; pending.trace.gpu = std::move(collection.gpu_timing); auto output = std::make_shared(); output->extent = pending.extent; diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.h b/render_3D/render_3D/detail/Datoviz_Visual_Backend.h index ffed201..d87c090 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.h +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.h @@ -45,7 +45,7 @@ public: [[nodiscard]] std::optional submit( const Scene_State& scene, std::uint64_t scene_revision, const Prepared_Point& point, std::uint64_t frame_sequence, - bool capture_trace); + bool observe); [[nodiscard]] Completed_Frame collect(Pending_Frame pending); void discard(Pending_Frame pending); diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp index ddb5249..37f73eb 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.cpp +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.cpp @@ -14,19 +14,61 @@ Gpu_Completion_Service::~Gpu_Completion_Service() { shutdown(); } -void Gpu_Completion_Service::watch( - VkDevice device, VkFence fence, Completion completion) { - if (device == VK_NULL_HANDLE || fence == VK_NULL_HANDLE) - throw std::invalid_argument("GPU completion fence is invalid"); +Gpu_Completion_Service::Reservation::Reservation( + std::shared_ptr pending) noexcept + : pending_(std::move(pending)) {} + +Gpu_Completion_Service::Reservation::~Reservation() { + cancel(); +} + +Gpu_Completion_Service::Reservation::Reservation( + Reservation&& other) noexcept + : pending_(std::exchange(other.pending_, {})) {} + +void Gpu_Completion_Service::Reservation::watch( + VkDevice device, VkFence fence) noexcept { + if (!pending_ || device == VK_NULL_HANDLE || fence == VK_NULL_HANDLE) + std::terminate(); + auto pending = std::exchange(pending_, {}); + { + std::lock_guard lock(pending->mutex); + if (pending->status != Pending_Fence::Status::reserved) + std::terminate(); + pending->device = device; + pending->fence = fence; + pending->status = Pending_Fence::Status::watched; + } + pending->ready.notify_one(); +} + +void Gpu_Completion_Service::Reservation::cancel() noexcept { + if (!pending_) + return; + auto pending = std::exchange(pending_, {}); + { + std::lock_guard lock(pending->mutex); + if (pending->status == Pending_Fence::Status::reserved) + pending->status = Pending_Fence::Status::canceled; + } + pending->ready.notify_one(); +} + +Gpu_Completion_Service::Reservation Gpu_Completion_Service::prepare( + Completion completion, bool observe) { if (!completion) throw std::invalid_argument("GPU completion callback is empty"); + auto pending = std::make_shared(); + pending->completion = std::move(completion); + pending->observe = observe; { std::lock_guard lock(mutex_); if (stopping_) throw std::runtime_error("GPU completion service is stopping"); - pending_.push({device, fence, std::move(completion)}); + pending_.push_back(pending); } ready_.notify_one(); + return Reservation(std::move(pending)); } void Gpu_Completion_Service::shutdown() noexcept { @@ -35,6 +77,19 @@ void Gpu_Completion_Service::shutdown() noexcept { if (stopping_ && !thread_.joinable()) return; stopping_ = true; + const auto cancel_reserved = [](const auto& pending) { + if (!pending) + return; + { + std::lock_guard pending_lock(pending->mutex); + if (pending->status == Pending_Fence::Status::reserved) + pending->status = Pending_Fence::Status::canceled; + } + pending->ready.notify_one(); + }; + cancel_reserved(active_); + for (const auto& pending : pending_) + cancel_reserved(pending); } ready_.notify_one(); if (thread_.joinable()) @@ -43,7 +98,7 @@ void Gpu_Completion_Service::shutdown() noexcept { void Gpu_Completion_Service::run() noexcept { for (;;) { - Pending_Fence pending; + std::shared_ptr pending; { std::unique_lock lock(mutex_); ready_.wait(lock, [this] { @@ -52,19 +107,45 @@ void Gpu_Completion_Service::run() noexcept { if (stopping_ && pending_.empty()) return; pending = std::move(pending_.front()); - pending_.pop(); + pending_.pop_front(); + active_ = pending; } - const auto wait_started = std::chrono::steady_clock::now(); + VkDevice device{VK_NULL_HANDLE}; + VkFence fence{VK_NULL_HANDLE}; + Completion completion; + bool observe{}; + { + std::unique_lock lock(pending->mutex); + pending->ready.wait(lock, [&pending] { + return pending->status != Pending_Fence::Status::reserved; + }); + if (pending->status == Pending_Fence::Status::canceled) { + lock.unlock(); + std::lock_guard service_lock(mutex_); + active_.reset(); + continue; + } + device = pending->device; + fence = pending->fence; + completion = std::move(pending->completion); + observe = pending->observe; + } + + const auto wait_started = observe + ? std::chrono::steady_clock::now() + : std::chrono::steady_clock::time_point{}; const VkResult result = vkWaitForFences( - pending.device, 1, &pending.fence, VK_TRUE, UINT64_MAX); - const auto wait_duration = - std::chrono::duration_cast( - std::chrono::steady_clock::now() - wait_started).count(); + device, 1, &fence, VK_TRUE, UINT64_MAX); Result completion_result; - completion_result.wait_duration_ns = wait_duration > 0 - ? static_cast(wait_duration) - : 0; + if (observe) { + const auto wait_duration = + std::chrono::duration_cast( + std::chrono::steady_clock::now() - wait_started).count(); + completion_result.wait_duration_ns = wait_duration > 0 + ? static_cast(wait_duration) + : 0; + } if (result != VK_SUCCESS) { try { throw std::runtime_error( @@ -75,9 +156,13 @@ void Gpu_Completion_Service::run() noexcept { } } try { - pending.completion(std::move(completion_result)); + completion(std::move(completion_result)); } catch (...) { } + { + std::lock_guard lock(mutex_); + active_.reset(); + } } } diff --git a/render_3D/render_3D/detail/Gpu_Completion_Service.h b/render_3D/render_3D/detail/Gpu_Completion_Service.h index 457ab94..c38dc8e 100644 --- a/render_3D/render_3D/detail/Gpu_Completion_Service.h +++ b/render_3D/render_3D/detail/Gpu_Completion_Service.h @@ -4,15 +4,18 @@ #include #include +#include #include #include #include -#include +#include #include namespace renderive::render_3d::detail { class Gpu_Completion_Service final { + struct Pending_Fence; + public: struct Result { std::exception_ptr error; @@ -21,27 +24,59 @@ public: using Completion = std::function; + class Reservation final { + public: + Reservation() = default; + ~Reservation(); + + Reservation(const Reservation&) = delete; + Reservation& operator=(const Reservation&) = delete; + Reservation(Reservation&& other) noexcept; + Reservation& operator=(Reservation&&) = delete; + + void watch(VkDevice device, VkFence fence) noexcept; + + private: + explicit Reservation(std::shared_ptr pending) noexcept; + void cancel() noexcept; + + std::shared_ptr pending_; + + friend class Gpu_Completion_Service; + }; + Gpu_Completion_Service(); ~Gpu_Completion_Service(); Gpu_Completion_Service(const Gpu_Completion_Service&) = delete; Gpu_Completion_Service& operator=(const Gpu_Completion_Service&) = delete; - void watch(VkDevice device, VkFence fence, Completion completion); + [[nodiscard]] Reservation prepare(Completion completion, bool observe); void shutdown() noexcept; private: struct Pending_Fence { + enum class Status { + reserved, + watched, + canceled + }; + + std::mutex mutex; + std::condition_variable ready; VkDevice device{VK_NULL_HANDLE}; VkFence fence{VK_NULL_HANDLE}; Completion completion; + Status status{Status::reserved}; + bool observe{}; }; void run() noexcept; std::mutex mutex_; std::condition_variable ready_; - std::queue pending_; + std::deque> pending_; + std::shared_ptr active_; std::thread thread_; bool stopping_{}; }; diff --git a/render_3D/render_3D/detail/Render_Domain.h b/render_3D/render_3D/detail/Render_Domain.h index e7560f1..dcdee51 100644 --- a/render_3D/render_3D/detail/Render_Domain.h +++ b/render_3D/render_3D/detail/Render_Domain.h @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -14,7 +13,34 @@ namespace renderive::render_3d::detail { class Render_Domain final { + struct Task { + explicit Task(std::function value) + : function(std::move(value)) {} + + std::function function; + std::unique_ptr next; + }; + public: + class Prepared_Task final { + public: + Prepared_Task() = default; + ~Prepared_Task() = default; + + Prepared_Task(const Prepared_Task&) = delete; + Prepared_Task& operator=(const Prepared_Task&) = delete; + Prepared_Task(Prepared_Task&&) noexcept = default; + Prepared_Task& operator=(Prepared_Task&&) noexcept = default; + + private: + explicit Prepared_Task(std::unique_ptr task) noexcept + : task_(std::move(task)) {} + + std::unique_ptr task_; + + friend class Render_Domain; + }; + Render_Domain() : thread_([this] { run(); }) {} ~Render_Domain() { { @@ -29,14 +55,32 @@ public: Render_Domain(const Render_Domain&) = delete; Render_Domain& operator=(const Render_Domain&) = delete; - void post(std::function function) { + [[nodiscard]] Prepared_Task prepare(std::function function) { if (!function) throw std::invalid_argument("render domain task is empty"); + return Prepared_Task( + std::make_unique(std::move(function))); + } + + void post(std::function function) { + auto task = prepare(std::move(function)); { std::lock_guard lock(mutex_); if (stopping_) throw std::runtime_error("Point_Scene render domain is stopping"); - tasks_.push(std::move(function)); + enqueue_locked(std::move(task.task_)); + } + condition_.notify_one(); + } + + void post(Prepared_Task task) noexcept { + if (!task.task_) + std::terminate(); + { + std::lock_guard lock(mutex_); + if (stopping_) + std::terminate(); + enqueue_locked(std::move(task.task_)); } condition_.notify_one(); } @@ -57,22 +101,34 @@ public: private: void run() { for (;;) { - std::function task; + std::unique_ptr task; { std::unique_lock lock(mutex_); - condition_.wait(lock, [&] { return stopping_ || !tasks_.empty(); }); - if (stopping_ && tasks_.empty()) + condition_.wait(lock, [&] { return stopping_ || first_; }); + if (stopping_ && !first_) return; - task = std::move(tasks_.front()); - tasks_.pop(); + task = std::move(first_); + first_ = std::move(task->next); + if (!first_) + last_ = nullptr; } - task(); + task->function(); } } + void enqueue_locked(std::unique_ptr task) noexcept { + Task* const inserted = task.get(); + if (last_) + last_->next = std::move(task); + else + first_ = std::move(task); + last_ = inserted; + } + std::mutex mutex_; std::condition_variable condition_; - std::queue> tasks_; + std::unique_ptr first_; + Task* last_{}; bool stopping_{}; std::thread thread_; }; diff --git a/render_3D/tests/Gpu_Completion_Service_Tests.cpp b/render_3D/tests/Gpu_Completion_Service_Tests.cpp new file mode 100644 index 0000000..8d7dc20 --- /dev/null +++ b/render_3D/tests/Gpu_Completion_Service_Tests.cpp @@ -0,0 +1,63 @@ +#include "render_3D/detail/Gpu_Completion_Service.h" +#include "render_3D/detail/Render_Domain.h" + +#include + +#include +#include +#include + +namespace renderive::render_3d::detail { +namespace { + +static_assert(noexcept( + std::declval().watch( + VK_NULL_HANDLE, VK_NULL_HANDLE))); +static_assert(noexcept( + std::declval().post( + std::declval()))); + +TEST(GpuCompletionService, AbandonedReservationCancelsBeforeFenceWait) { + Gpu_Completion_Service service; + std::atomic completion_count{}; + { + auto reservation = service.prepare( + [&](Gpu_Completion_Service::Result) { + completion_count.fetch_add(1, std::memory_order_relaxed); + }, + false); + } + service.shutdown(); + EXPECT_EQ(completion_count.load(std::memory_order_relaxed), 0); +} + +TEST(GpuCompletionService, PrepareRejectsAStoppedServiceBeforeSubmission) { + Gpu_Completion_Service service; + service.shutdown(); + EXPECT_THROW({ + auto reservation = service.prepare( + [](Gpu_Completion_Service::Result) {}, false); + static_cast(reservation); + }, std::runtime_error); +} + +TEST(GpuCompletionService, ShutdownCancelsAnUnarmedReservation) { + Gpu_Completion_Service service; + auto reservation = service.prepare( + [](Gpu_Completion_Service::Result) {}, false); + service.shutdown(); +} + +TEST(RenderDomain, PreparedTaskRunsAfterNoThrowHandoff) { + Render_Domain domain; + std::atomic executed{}; + auto task = domain.prepare([&] { + executed.store(true, std::memory_order_release); + }); + domain.post(std::move(task)); + domain.invoke([] {}); + EXPECT_TRUE(executed.load(std::memory_order_acquire)); +} + +} // namespace +} // namespace renderive::render_3d::detail diff --git a/render_3D/tests/Point_Render_Integration_Tests.cpp b/render_3D/tests/Point_Render_Integration_Tests.cpp index a854f03..bc72a68 100644 --- a/render_3D/tests/Point_Render_Integration_Tests.cpp +++ b/render_3D/tests/Point_Render_Integration_Tests.cpp @@ -139,8 +139,36 @@ TEST(PointRenderIntegration, Node_Metric_Kind::gpu_fence_wait_ns)); EXPECT_TRUE(execution.metrics.contains( Node_Metric_Kind::readback_duration_ns)); - EXPECT_GT(execution.metrics.get( - Node_Metric_Kind::backend_artifact_json_bytes), 0U); + const auto artifact_bytes = execution.metrics.get( + Node_Metric_Kind::backend_artifact_json_bytes); + EXPECT_GT(artifact_bytes, 0U); + const auto artifact = std::find_if( + execution.attachments.begin(), execution.attachments.end(), + [](const Node_Diagnostic_Attachment& attachment) { + return attachment.type == "datoviz.drp2.artifact.json"; + }); + ASSERT_NE(artifact, execution.attachments.end()); + EXPECT_EQ(artifact->content.size(), artifact_bytes); + EXPECT_FALSE(artifact->content.empty()); + + const bool gpu_timing = execution.metrics.contains( + Node_Metric_Kind::gpu_render_duration_ns); + EXPECT_EQ(gpu_timing, execution.metrics.contains( + Node_Metric_Kind::gpu_transition_duration_ns)); + EXPECT_EQ(gpu_timing, execution.metrics.contains( + Node_Metric_Kind::gpu_copy_duration_ns)); + EXPECT_EQ(gpu_timing, execution.metrics.contains( + Node_Metric_Kind::gpu_total_duration_ns)); + if (gpu_timing) { + const auto total = execution.metrics.get( + Node_Metric_Kind::gpu_total_duration_ns); + EXPECT_GE(total, execution.metrics.get( + Node_Metric_Kind::gpu_render_duration_ns)); + EXPECT_GE(total, execution.metrics.get( + Node_Metric_Kind::gpu_transition_duration_ns)); + EXPECT_GE(total, execution.metrics.get( + Node_Metric_Kind::gpu_copy_duration_ns)); + } } catch (const std::exception& error) { GTEST_SKIP() << "Vulkan/Datoviz unavailable: " << error.what(); } diff --git a/web_server/app/Gallery_Capture_Json.h b/web_server/app/Gallery_Capture_Json.h index c0e4161..c284e58 100644 --- a/web_server/app/Gallery_Capture_Json.h +++ b/web_server/app/Gallery_Capture_Json.h @@ -59,7 +59,8 @@ inline Json metrics_json(const Node_Execution_Metrics& metrics) { "primitive_count", "queue_wait_ns", "apply_duration_ns", "plan_emit_duration_ns", "backend_execute_duration_ns", "submit_duration_ns", "gpu_fence_wait_ns", - "gpu_render_duration_ns", "gpu_copy_duration_ns", + "gpu_render_duration_ns", "gpu_transition_duration_ns", + "gpu_copy_duration_ns", "gpu_total_duration_ns", "readback_duration_ns", "backend_resource_version", "backend_frame_index", "backend_artifact_status", "backend_validation_code", "backend_validation_command", @@ -147,6 +148,11 @@ inline Json plan_json(const Scene_Base& scene, const Render_Plan& plan) { inline Json execution_json(const Node_Execution& execution, const Frame_Snapshot& frame) { + Json attachments = Json::array(); + for (const auto& attachment : execution.attachments) { + attachments.push_back({{"type", attachment.type}, + {"content", attachment.content}}); + } return { {"node_id", execution.node_id}, {"ready_time_ns", execution.ready_time_ns}, @@ -172,7 +178,8 @@ inline Json execution_json(const Node_Execution& execution, {"external_duration_ns", execution.external_duration_ns()}, {"worker_id", execution.worker_id}, {"status", execution_status(execution.status)}, - {"metrics", metrics_json(execution.metrics)} + {"metrics", metrics_json(execution.metrics)}, + {"attachments", std::move(attachments)} }; } @@ -240,6 +247,10 @@ inline Json node_statistics_json(const Node_Statistics& statistics) { {"p99_ns", statistics.p99_ns}, {"minimum_ns", statistics.minimum_ns}, {"maximum_ns", statistics.maximum_ns}, + {"average_cpu_ns", statistics.average_cpu_ns}, + {"average_external_ns", statistics.average_external_ns}, + {"p95_cpu_ns", statistics.p95_cpu_ns}, + {"p95_external_ns", statistics.p95_external_ns}, {"critical_path_frequency", statistics.critical_path_frequency}, {"average_scheduler_wait_ns", statistics.average_scheduler_wait_ns} }; diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index 4f3483b..0c84ec6 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -551,6 +551,7 @@ TEST(RenderiveWebGallery, PerformanceCaptureReturnsPlanFrameAnalysisAndStatistic EXPECT_TRUE(execution.contains("end_time_ns")); EXPECT_TRUE(execution.contains("worker_id")); EXPECT_TRUE(execution.contains("status")); + EXPECT_TRUE(execution.contains("attachments")); const auto& analysis = frame.at("analysis"); EXPECT_TRUE(analysis.contains("critical_path")); EXPECT_TRUE(analysis.contains("parallel_overlap_ns")); @@ -570,6 +571,10 @@ TEST(RenderiveWebGallery, PerformanceCaptureReturnsPlanFrameAnalysisAndStatistic EXPECT_TRUE(statistic.contains("p50_ns")); EXPECT_TRUE(statistic.contains("p95_ns")); EXPECT_TRUE(statistic.contains("p99_ns")); + EXPECT_TRUE(statistic.contains("average_cpu_ns")); + EXPECT_TRUE(statistic.contains("average_external_ns")); + EXPECT_TRUE(statistic.contains("p95_cpu_ns")); + EXPECT_TRUE(statistic.contains("p95_external_ns")); EXPECT_TRUE(statistic.contains("critical_path_frequency")); EXPECT_TRUE(statistic.contains("average_scheduler_wait_ns")); } diff --git a/webapp_gallery/src/capture/node_detail.tsx b/webapp_gallery/src/capture/node_detail.tsx index 19ab0a1..4820f44 100644 --- a/webapp_gallery/src/capture/node_detail.tsx +++ b/webapp_gallery/src/capture/node_detail.tsx @@ -1,9 +1,14 @@ import {Stack,Typography} from "@mui/material"; -import type {Gallery_Capture_Session,Gallery_Captured_Frame,Gallery_Render_Plan} from "../protocol/gallery_types"; +import type {Gallery_Capture_Session,Gallery_Captured_Frame,Gallery_Render_Plan,Json_Value} from "../protocol/gallery_types"; import {Metric_Grid} from "../common/metric_grid"; import {Json_Viewer} from "../common/json_viewer"; import {format_nanoseconds} from "../protocol/format"; +function attachment_value(type:string,content:string):Json_Value { + if(!type.endsWith(".json"))return content; + try{return JSON.parse(content) as Json_Value;}catch{return content;} +} + export function Node_Detail({frame,plan,session,selected_node_id}:{frame:Gallery_Captured_Frame;plan:Gallery_Render_Plan;session:Gallery_Capture_Session;selected_node_id:number|null}) { const node=plan.nodes.find(item=>item.node_id===selected_node_id); const execution=frame.node_executions.find(item=>item.node_id===selected_node_id); @@ -22,6 +27,10 @@ export function Node_Detail({frame,plan,session,selected_node_id}:{frame:Gallery ["Critical",`${((analysis?.critical_path_contribution??0)*100).toFixed(2)}%`], ["Moving avg",format_nanoseconds(history?.moving_average_ns)], ["P95",format_nanoseconds(history?.p95_ns)], - ["P99",format_nanoseconds(history?.p99_ns)] - ]}/>{execution.metrics&&}; + ["P99",format_nanoseconds(history?.p99_ns)], + ["CPU avg",format_nanoseconds(history?.average_cpu_ns)], + ["CPU P95",format_nanoseconds(history?.p95_cpu_ns)], + ["External avg",format_nanoseconds(history?.average_external_ns)], + ["External P95",format_nanoseconds(history?.p95_external_ns)] + ]}/>{execution.metrics&&} {execution.attachments.map((attachment,index)=>{attachment.type})}; } diff --git a/webapp_gallery/src/protocol/gallery_types.ts b/webapp_gallery/src/protocol/gallery_types.ts index e3ee0e0..3f272fc 100644 --- a/webapp_gallery/src/protocol/gallery_types.ts +++ b/webapp_gallery/src/protocol/gallery_types.ts @@ -31,10 +31,11 @@ export interface Gallery_Render_Node {node_id: number; id?: string | number; nam export interface Gallery_Render_Edge {from: number | string; to: number | string;} export interface Gallery_Renderable_Cache {owner_id: number; name: string; prepare_cache: string; paint_cache: string;} export interface Gallery_Render_Plan {version: number; nodes: Gallery_Render_Node[]; edges: Gallery_Render_Edge[]; renderables: Gallery_Renderable_Cache[];} -export interface Gallery_Node_Execution {node_id: number; worker_id: number; start_offset_ns: number; cpu_end_offset_ns: number; external_start_offset_ns: number; external_end_offset_ns: number; end_offset_ns: number; duration_ns: number; cpu_duration_ns: number; external_duration_ns: number; status: "pending"|"ready"|"running"|"waiting_external"|"complete"|"failed"; metrics?: Record;} +export interface Gallery_Node_Diagnostic_Attachment {type: string; content: string;} +export interface Gallery_Node_Execution {node_id: number; worker_id: number; start_offset_ns: number; cpu_end_offset_ns: number; external_start_offset_ns: number; external_end_offset_ns: number; end_offset_ns: number; duration_ns: number; cpu_duration_ns: number; external_duration_ns: number; status: "pending"|"ready"|"running"|"waiting_external"|"complete"|"failed"; metrics?: Record; attachments: Gallery_Node_Diagnostic_Attachment[];} export interface Gallery_Node_Analysis {node_id: number; duration_ns: number; cpu_duration_ns: number; external_duration_ns: number; scheduler_wait_ns: number; critical_path_contribution: number; on_critical_path: boolean;} export interface Gallery_Captured_Frame {frame_id: number; render_plan_version: number; render_duration_ns: number; node_executions: Gallery_Node_Execution[]; analysis?: {total_work_duration_ns?: number; total_external_duration_ns?: number; nodes: Gallery_Node_Analysis[]};} -export interface Gallery_Node_Statistics {node_id: number; moving_average_ns: number; p95_ns: number; p99_ns: number; critical_path_frequency: number;} +export interface Gallery_Node_Statistics {node_id: number; moving_average_ns: number; p95_ns: number; p99_ns: number; average_cpu_ns: number; average_external_ns: number; p95_cpu_ns: number; p95_external_ns: number; critical_path_frequency: number;} export interface Gallery_Plan_Statistics {render_plan_version: number; frame_count: number; render_average_ns: number; render_p95_ns: number; average_parallelism: number; peak_parallelism: number; scheduler_wait_average_ns: number; nodes: Gallery_Node_Statistics[];} export interface Gallery_Capture_Session {session_id: number; active: boolean; requested_count: number; captured_count: number; frames: Gallery_Captured_Frame[]; node_statistics: Gallery_Node_Statistics[]; plan_statistics: Gallery_Plan_Statistics[]; summary?: Record;} export interface Gallery_Performance_Capture {controller: {enabled?: boolean; session_id?: number}; sessions: Gallery_Capture_Session[]; plans: Gallery_Render_Plan[];} diff --git a/webapp_gallery/tests/dag/dag_model.test.ts b/webapp_gallery/tests/dag/dag_model.test.ts index 7152bd8..2163809 100644 --- a/webapp_gallery/tests/dag/dag_model.test.ts +++ b/webapp_gallery/tests/dag/dag_model.test.ts @@ -1,2 +1,2 @@ import {describe,expect,it} from "vitest";import {build_dag_model} from "../../src/dag/dag_model"; -describe("DAG view model",()=>{it("uses node_id for current plan and capture overlays",()=>{const plan={version:2,nodes:[{node_id:7,name:"paint",owner:"plot",kind:"paint"}],edges:[],renderables:[]};const frame={frame_id:1,render_plan_version:2,render_duration_ns:10,node_executions:[{node_id:7,worker_id:3,start_offset_ns:0,cpu_end_offset_ns:10,external_start_offset_ns:0,external_end_offset_ns:0,end_offset_ns:10,duration_ns:10,cpu_duration_ns:10,external_duration_ns:0,status:"complete" as const}],analysis:{total_work_duration_ns:10,total_external_duration_ns:0,nodes:[{node_id:7,duration_ns:10,cpu_duration_ns:10,external_duration_ns:0,scheduler_wait_ns:1,critical_path_contribution:1,on_critical_path:true}]}};const model=build_dag_model(plan,frame,[],7);expect(model.nodes[0].id).toBe("7");expect(model.nodes[0].data).toMatchObject({duration_ns:10,worker_id:3,critical:true,selected:true});});}); +describe("DAG view model",()=>{it("uses node_id for current plan and capture overlays",()=>{const plan={version:2,nodes:[{node_id:7,name:"paint",owner:"plot",kind:"paint"}],edges:[],renderables:[]};const frame={frame_id:1,render_plan_version:2,render_duration_ns:10,node_executions:[{node_id:7,worker_id:3,start_offset_ns:0,cpu_end_offset_ns:10,external_start_offset_ns:0,external_end_offset_ns:0,end_offset_ns:10,duration_ns:10,cpu_duration_ns:10,external_duration_ns:0,status:"complete" as const,attachments:[]}],analysis:{total_work_duration_ns:10,total_external_duration_ns:0,nodes:[{node_id:7,duration_ns:10,cpu_duration_ns:10,external_duration_ns:0,scheduler_wait_ns:1,critical_path_contribution:1,on_critical_path:true}]}};const model=build_dag_model(plan,frame,[],7);expect(model.nodes[0].id).toBe("7");expect(model.nodes[0].data).toMatchObject({duration_ns:10,worker_id:3,critical:true,selected:true});});});