This commit is contained in:
2026-08-15 23:24:23 +08:00
parent 023250c0fc
commit 7a94264c13
32 changed files with 1450 additions and 290 deletions
@@ -93,9 +93,9 @@ TEST(render_dag_test, analysis_derives_wait_critical_path_and_parallel_overlap_f
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.peak_parallelism, 2u);
EXPECT_DOUBLE_EQ(analysis.average_parallelism, 1.0);
EXPECT_EQ(analysis.parallel_overlap_ns, 30u);
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);
@@ -278,3 +278,75 @@ TEST(render_dag_test, capture_repository_retains_only_recent_completed_sessions)
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);
}
@@ -193,3 +193,132 @@ TEST(render_graph_runtime_test,
EXPECT_TRUE(source.complete());
execution.join();
}
TEST(external_operation_test, cancellation_is_terminal_and_delivered_once) {
External_Operation_Source source;
const auto operation = source.operation();
EXPECT_TRUE(operation.cancel());
EXPECT_FALSE(source.complete());
EXPECT_EQ(operation.status(), External_Operation_Status::cancelled);
int completion_count{};
std::exception_ptr completion_error;
operation.on_complete([&](std::exception_ptr error) {
completion_error = std::move(error);
++completion_count;
});
EXPECT_EQ(completion_count, 1);
ASSERT_TRUE(completion_error);
EXPECT_THROW(std::rethrow_exception(completion_error),
External_Operation_Cancelled);
}
TEST(external_operation_test, deadline_cancels_pending_operation) {
External_Operation_Source source;
const auto operation = source.operation();
std::mutex mutex;
std::condition_variable condition;
bool completed{};
std::exception_ptr completion_error;
operation.on_complete([&](std::exception_ptr error) {
{
std::lock_guard lock(mutex);
completion_error = std::move(error);
completed = true;
}
condition.notify_one();
});
source.set_deadline(External_Operation_Source::Clock::now());
{
std::unique_lock lock(mutex);
ASSERT_TRUE(condition.wait_for(lock, std::chrono::seconds(1),
[&] { return completed; }));
}
ASSERT_TRUE(completion_error);
EXPECT_THROW(std::rethrow_exception(completion_error),
External_Operation_Deadline_Exceeded);
EXPECT_EQ(operation.status(), External_Operation_Status::cancelled);
}
TEST(render_graph_runtime_test,
cancellation_releases_external_wait_and_blocks_successor) {
const auto plan = two_node_plan();
External_Operation_Source source;
std::vector<Node_Execution> execution_storage;
auto slots = execution_slots(*plan, execution_storage);
std::atomic<bool> submit_started{};
std::atomic<bool> publish_executed{};
std::exception_ptr graph_error;
renderive::render_graph::detail::Render_Graph_Runtime runtime(*plan);
const auto execute_node = [&](std::size_t index, Node_Execution_Metrics*) {
if (index == 0) {
submit_started.store(true, std::memory_order_release);
submit_started.notify_all();
return Node_Execution_Result::external(source.operation());
}
publish_executed.store(true, std::memory_order_release);
return Node_Execution_Result::completed();
};
std::thread execution([&] {
try {
runtime.execute(slots, execute_node);
} catch (...) {
graph_error = std::current_exception();
}
});
submit_started.wait(false, std::memory_order_acquire);
runtime.cancel_pending();
execution.join();
ASSERT_TRUE(graph_error);
EXPECT_THROW(std::rethrow_exception(graph_error),
External_Operation_Cancelled);
EXPECT_FALSE(publish_executed.load(std::memory_order_acquire));
EXPECT_EQ(execution_storage[0].status, Node_Execution_Status::cancelled);
EXPECT_EQ(source.operation().status(), External_Operation_Status::cancelled);
}
TEST(external_operation_test, completion_commit_is_exclusive_with_cancellation) {
External_Operation_Source source;
const auto operation = source.operation();
std::atomic<int> published{};
EXPECT_TRUE(operation.cancel());
EXPECT_FALSE(source.complete([&] {
published.fetch_add(1, std::memory_order_relaxed);
}));
EXPECT_EQ(published.load(std::memory_order_relaxed), 0);
}
TEST(external_operation_test, completion_commit_precedes_completion_notification) {
External_Operation_Source source;
const auto operation = source.operation();
std::atomic<int> published{};
int observed{};
operation.on_complete([&](std::exception_ptr error) {
EXPECT_FALSE(error);
observed = published.load(std::memory_order_acquire);
});
EXPECT_TRUE(source.complete([&] {
published.store(1, std::memory_order_release);
EXPECT_EQ(operation.status(), External_Operation_Status::pending);
}));
EXPECT_EQ(observed, 1);
EXPECT_EQ(operation.status(), External_Operation_Status::completed);
}
TEST(external_operation_test, completion_commit_failure_fails_operation) {
External_Operation_Source source;
const auto operation = source.operation();
std::exception_ptr completion_error;
operation.on_complete([&](std::exception_ptr error) {
completion_error = std::move(error);
});
EXPECT_TRUE(source.complete([] { throw std::runtime_error("publish failed"); }));
ASSERT_TRUE(completion_error);
EXPECT_THROW(std::rethrow_exception(completion_error), std::runtime_error);
EXPECT_EQ(operation.status(), External_Operation_Status::failed);
}