Files
Renderive/Kernel/tests/renderive/render_graph/Render_DAG_Test.cpp
T
2026-08-16 00:07:05 +08:00

353 lines
15 KiB
C++

#include <gtest/gtest.h>
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <memory>
#include <thread>
#include <vector>
#include "renderive/capture/Capture.hpp"
#include "renderive/render_graph/Frame_Analysis.hpp"
#include "renderive/render_graph/Render_Plan.hpp"
#include "renderive/scene/base/Abstract_Frame.hpp"
namespace {
template <class Node>
concept Exposes_Execution_Function = requires(Node node) {
node.function;
};
static_assert(!Exposes_Execution_Function<Render_Node>);
template <class Frame>
concept Exposes_Execution_Slot = requires(Frame& frame) {
frame.execution_slot(0);
};
static_assert(!Exposes_Execution_Slot<Abstract_Frame>);
Render_Graph make_parallel_graph() {
Render_Graph_Builder builder;
const auto a = builder.emplace(100, 10, "A Prepare", Render_Node_Kind::prepare);
const auto b = builder.emplace(101, 11, "B Prepare", Render_Node_Kind::prepare);
const auto c = builder.emplace(102, 12, "C Paint", Render_Node_Kind::paint);
builder.precede(a, c);
builder.precede(b, c);
return std::move(builder).finish();
}
}
TEST(render_dag_test, one_graph_assigns_dense_execution_slots_and_versions_only_topology) {
Render_Plan_History history;
const auto first = history.publish(make_parallel_graph());
ASSERT_TRUE(first);
ASSERT_EQ(first->graph.nodes.size(), 3u);
EXPECT_EQ(first->graph.nodes[0].execution_index, 0u);
EXPECT_EQ(first->graph.nodes[1].execution_index, 1u);
EXPECT_EQ(first->graph.nodes[2].execution_index, 2u);
const auto same = history.publish(make_parallel_graph());
EXPECT_EQ(same->version, first->version);
auto changed_graph = make_parallel_graph();
changed_graph.edges.push_back({100, 101});
const auto changed = history.publish(std::move(changed_graph));
EXPECT_GT(changed->version, first->version);
const auto restored = history.publish(make_parallel_graph());
EXPECT_GT(restored->version, changed->version);
EXPECT_EQ(history.current(), restored);
EXPECT_EQ(history.find(first->version), first);
EXPECT_EQ(history.find(changed->version), changed);
EXPECT_EQ(history.find(restored->version), restored);
}
TEST(render_dag_test, analysis_derives_wait_critical_path_and_parallel_overlap_from_frame_slots) {
Render_Plan_History history;
const auto plan = history.publish(make_parallel_graph());
Frame_Snapshot snapshot;
snapshot.frame_id = 1;
snapshot.render_plan_version = plan->version;
snapshot.render_start_ns = 100;
snapshot.render_end_ns = 200;
snapshot.node_executions.resize(plan->graph.nodes.size());
auto complete = [&snapshot, &plan](std::size_t slot, std::uint64_t ready,
std::uint64_t start, std::uint64_t end,
std::uint32_t worker) {
auto* execution = &snapshot.node_executions.at(slot);
execution->node_id = plan->graph.nodes.at(slot).node_id;
execution->ready_time_ns = ready;
execution->start_time_ns = start;
execution->cpu_end_time_ns = end;
execution->end_time_ns = end;
execution->worker_id = worker;
execution->status = Node_Execution_Status::complete;
};
complete(0, 100, 110, 150, 0);
complete(1, 100, 115, 145, 1);
complete(2, 150, 160, 190, 1);
const auto analysis = analyze_frame(*plan, snapshot);
ASSERT_EQ(analysis.nodes.size(), 3u);
EXPECT_EQ(analysis.nodes[2].dependency_ready_offset_ns, 50u);
EXPECT_EQ(analysis.nodes[2].scheduler_wait_ns, 10u);
EXPECT_EQ(analysis.total_render_duration_ns, 100u);
EXPECT_EQ(analysis.total_work_duration_ns, 100u);
EXPECT_EQ(analysis.scheduler_peak_parallelism, 2u);
EXPECT_DOUBLE_EQ(analysis.average_cpu_concurrency, 1.0);
EXPECT_EQ(analysis.scheduler_parallel_overlap_ns, 30u);
EXPECT_EQ(analysis.critical_path, (std::vector<Render_Node_Id>{100, 102}));
EXPECT_EQ(analysis.critical_path_duration_ns, 70u);
EXPECT_DOUBLE_EQ(analysis.nodes[0].work_contribution, 0.4);
EXPECT_DOUBLE_EQ(analysis.nodes[0].critical_path_contribution, 40.0 / 70.0);
}
TEST(render_dag_test, capture_controller_and_repository_capture_exact_requested_frames) {
Capture_Controller controller;
Capture_Repository repository;
const auto session_id = controller.capture_frames(2);
repository.begin_session(session_id, 2);
const auto first = controller.begin_frame();
const auto second = controller.begin_frame();
const auto third = controller.begin_frame();
EXPECT_TRUE(first.capture);
EXPECT_TRUE(second.capture);
EXPECT_FALSE(third.capture);
EXPECT_EQ(first.session_id, session_id);
EXPECT_EQ(second.session_id, session_id);
controller.finish_frame(first, true);
controller.finish_frame(second, true);
EXPECT_FALSE(controller.state().enabled());
Render_Plan_History history;
const auto plan = history.publish(make_parallel_graph());
for (std::uint64_t frame_id : {1u, 2u}) {
auto snapshot = std::make_shared<Frame_Snapshot>();
snapshot->frame_id = frame_id;
snapshot->render_plan_version = plan->version;
snapshot->render_start_ns = frame_id * 100;
snapshot->render_end_ns = frame_id * 100 + 50;
snapshot->node_executions.resize(plan->graph.nodes.size());
for (std::size_t slot = 0; slot < plan->graph.nodes.size(); ++slot) {
auto* execution = &snapshot->node_executions.at(slot);
execution->node_id = plan->graph.nodes.at(slot).node_id;
execution->ready_time_ns = frame_id * 100;
execution->start_time_ns = frame_id * 100 + slot * 10;
execution->cpu_end_time_ns = execution->start_time_ns + 5;
execution->end_time_ns = execution->start_time_ns + 5;
execution->worker_id = static_cast<std::uint32_t>(slot);
execution->status = Node_Execution_Status::complete;
}
repository.publish(session_id, snapshot, plan, analyze_frame(*plan, *snapshot));
}
const auto session = repository.session(session_id);
ASSERT_TRUE(session);
EXPECT_EQ(session->requested_count, 2u);
EXPECT_EQ(session->captured_count(), 2u);
EXPECT_FALSE(session->active());
EXPECT_EQ(session->frames[0].snapshot->frame_id, 1u);
EXPECT_EQ(session->frames[1].snapshot->frame_id, 2u);
}
TEST(render_dag_test,
external_wait_contributes_to_wall_path_but_not_cpu_worker_utilization) {
Render_Graph_Builder builder;
builder.emplace(201, 20, "GPU Render", Render_Node_Kind::render);
Render_Plan_History history;
const auto plan = history.publish(std::move(builder).finish());
Frame_Snapshot snapshot;
snapshot.frame_id = 7;
snapshot.render_plan_version = plan->version;
snapshot.render_start_ns = 1'000;
snapshot.render_end_ns = 1'200;
snapshot.node_executions.resize(1);
auto& execution = snapshot.node_executions.front();
execution.node_id = 201;
execution.ready_time_ns = 1'000;
execution.start_time_ns = 1'000;
execution.cpu_end_time_ns = 1'020;
execution.external_start_time_ns = 1'020;
execution.external_end_time_ns = 1'200;
execution.end_time_ns = 1'200;
execution.worker_id = 3;
execution.status = Node_Execution_Status::complete;
const auto analysis = analyze_frame(*plan, snapshot);
ASSERT_EQ(analysis.nodes.size(), 1U);
EXPECT_EQ(analysis.nodes.front().duration_ns, 200U);
EXPECT_EQ(analysis.nodes.front().cpu_duration_ns, 20U);
EXPECT_EQ(analysis.nodes.front().external_duration_ns, 180U);
EXPECT_EQ(analysis.total_work_duration_ns, 20U);
EXPECT_EQ(analysis.total_external_duration_ns, 180U);
EXPECT_EQ(analysis.critical_path_duration_ns, 200U);
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<const Frame_Analysis>(&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) {
Capture_Controller controller;
const auto session_id = controller.capture_next_frame();
const auto failed = controller.begin_frame();
ASSERT_TRUE(failed.capture);
controller.finish_frame(failed, false);
EXPECT_TRUE(controller.state().enabled());
EXPECT_EQ(controller.state().remaining_frame_count, 1u);
const auto retry = controller.begin_frame();
ASSERT_TRUE(retry.capture);
EXPECT_EQ(retry.session_id, session_id);
controller.finish_frame(retry, true);
EXPECT_FALSE(controller.state().enabled());
EXPECT_FALSE(controller.begin_frame().capture);
}
TEST(render_dag_test, concurrent_workers_reserve_exactly_one_capture_slot_each) {
Capture_Controller controller;
constexpr std::size_t requested = 64;
controller.capture_frames(requested);
std::atomic<std::size_t> captured{};
std::vector<std::thread> workers;
workers.reserve(256);
for (std::size_t index = 0; index < 256; ++index) {
workers.emplace_back([&] {
const auto ticket = controller.begin_frame();
if (!ticket.capture)
return;
captured.fetch_add(1, std::memory_order_relaxed);
controller.finish_frame(ticket, true);
});
}
for (auto& worker : workers)
worker.join();
EXPECT_EQ(captured.load(std::memory_order_relaxed), requested);
EXPECT_FALSE(controller.state().enabled());
}
TEST(render_dag_test, render_plan_history_has_bounded_retention) {
Render_Plan_History history;
std::vector<Render_Plan_Version> versions;
for (std::uint64_t index = 0; index < 70; ++index) {
Render_Graph_Builder builder;
builder.emplace(1000 + index, 2000 + index, "Node", Render_Node_Kind::prepare);
versions.push_back(history.publish(std::move(builder).finish())->version);
}
EXPECT_FALSE(history.find(versions.front()));
EXPECT_TRUE(history.find(versions.back()));
ASSERT_TRUE(history.current());
EXPECT_EQ(history.current()->version, versions.back());
}
TEST(render_dag_test, capture_repository_retains_only_recent_completed_sessions) {
Capture_Repository repository;
Render_Graph_Builder builder;
builder.emplace(3000, 4000, "Node", Render_Node_Kind::prepare);
Render_Plan_History history;
const auto plan = history.publish(std::move(builder).finish());
for (Capture_Session_Id session_id = 1; session_id <= 20; ++session_id) {
repository.begin_session(session_id, 1);
auto snapshot = std::make_shared<Frame_Snapshot>();
snapshot->frame_id = session_id;
snapshot->render_plan_version = plan->version;
snapshot->render_start_ns = session_id * 100;
snapshot->render_end_ns = snapshot->render_start_ns + 10;
snapshot->node_executions.resize(1);
auto& execution = snapshot->node_executions.front();
execution.node_id = plan->graph.nodes.front().node_id;
execution.ready_time_ns = snapshot->render_start_ns;
execution.start_time_ns = snapshot->render_start_ns;
execution.cpu_end_time_ns = snapshot->render_end_ns;
execution.end_time_ns = snapshot->render_end_ns;
execution.status = Node_Execution_Status::complete;
repository.publish(session_id, snapshot, plan, analyze_frame(*plan, *snapshot));
}
const auto sessions = repository.sessions();
ASSERT_EQ(sessions.size(), 16U);
EXPECT_EQ(sessions.front(), 5U);
EXPECT_EQ(sessions.back(), 20U);
EXPECT_FALSE(repository.session(1));
EXPECT_TRUE(repository.session(20));
}
TEST(render_dag_test, render_domain_phases_are_counted_as_cpu_work_not_gpu_wall_time) {
Render_Graph_Builder builder;
builder.emplace(5001, 50, "3D Render", Render_Node_Kind::render);
Render_Plan_History history;
const auto plan = history.publish(std::move(builder).finish());
Frame_Snapshot snapshot;
snapshot.frame_id = 11;
snapshot.render_plan_version = plan->version;
snapshot.render_start_ns = 10'000;
snapshot.render_end_ns = 10'500;
snapshot.node_executions.resize(1);
auto& execution = snapshot.node_executions.front();
execution.node_id = 5001;
execution.ready_time_ns = 10'000;
execution.start_time_ns = 10'000;
execution.cpu_end_time_ns = 10'020;
execution.external_start_time_ns = 10'020;
execution.external_end_time_ns = 10'500;
execution.end_time_ns = 10'500;
execution.worker_id = 2;
execution.status = Node_Execution_Status::complete;
execution.metrics.set(Node_Metric_Kind::queue_wait_ns, 30);
execution.metrics.set(Node_Metric_Kind::apply_duration_ns, 10);
execution.metrics.set(Node_Metric_Kind::plan_emit_duration_ns, 20);
execution.metrics.set(Node_Metric_Kind::backend_execute_duration_ns, 30);
execution.metrics.set(Node_Metric_Kind::submit_duration_ns, 40);
execution.metrics.set(Node_Metric_Kind::readback_duration_ns, 50);
execution.metrics.set(Node_Metric_Kind::gpu_fence_wait_ns, 300);
execution.metrics.set(Node_Metric_Kind::gpu_total_duration_ns, 250);
const auto analysis = analyze_frame(*plan, snapshot);
ASSERT_EQ(analysis.nodes.size(), 1U);
EXPECT_EQ(analysis.scheduler_work_duration_ns, 20U);
EXPECT_EQ(analysis.render_domain_work_duration_ns, 150U);
EXPECT_EQ(analysis.total_work_duration_ns, 170U);
EXPECT_EQ(analysis.total_external_duration_ns, 480U);
EXPECT_EQ(analysis.total_render_domain_queue_wait_ns, 30U);
EXPECT_EQ(analysis.total_gpu_completion_wait_ns, 300U);
EXPECT_EQ(analysis.total_gpu_execution_duration_ns, 250U);
EXPECT_DOUBLE_EQ(analysis.scheduler_average_parallelism, 0.04);
EXPECT_DOUBLE_EQ(analysis.render_domain_utilization, 0.3);
EXPECT_DOUBLE_EQ(analysis.average_cpu_concurrency, 0.34);
const auto statistics = analyze_node_statistics(
std::span<const Frame_Analysis>(&analysis, 1));
ASSERT_EQ(statistics.size(), 1U);
EXPECT_DOUBLE_EQ(statistics.front().average_render_domain_cpu_ns, 150.0);
EXPECT_DOUBLE_EQ(statistics.front().average_total_cpu_work_ns, 170.0);
EXPECT_DOUBLE_EQ(statistics.front().average_gpu_execution_ns, 250.0);
}
TEST(render_dag_test, graph_cycle_is_validated_once_when_builder_finishes) {
Render_Graph_Builder builder;
const auto a = builder.emplace(6001, 60, "A", Render_Node_Kind::prepare);
const auto b = builder.emplace(6002, 60, "B", Render_Node_Kind::prepare);
const auto c = builder.emplace(6003, 60, "C", Render_Node_Kind::prepare);
builder.precede(a, b);
builder.precede(b, c);
builder.precede(c, a);
EXPECT_THROW(static_cast<void>(std::move(builder).finish()), std::invalid_argument);
}
TEST(render_dag_test, plan_history_rejects_invalid_manually_constructed_graphs) {
Render_Graph graph;
graph.nodes.push_back({7001, 70, "A", Render_Node_Kind::prepare, 0});
graph.edges.push_back({7001, 7999});
Render_Plan_History history;
EXPECT_THROW(static_cast<void>(history.publish(std::move(graph))),
std::invalid_argument);
}