#include "../Renderive/plottable/Performance_Shower_p.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr std::uint64_t Nanoseconds_Per_Millisecond = 1000000; std::uint64_t to_nanoseconds(double milliseconds) { return static_cast(std::llround(milliseconds * static_cast(Nanoseconds_Per_Millisecond))); } struct Scenario_Config { const char* name{}; std::uint64_t id{}; double input_interval_ms{}; double render_duration_ms{}; double paint_duration_ms{}; double max_render_fps{}; renderive::Bottleneck_State expected_state = renderive::Bottleneck_State::Unknown; double minimum_production_fps{}; double maximum_production_fps{}; double minimum_display_fps{}; double maximum_display_fps{}; }; struct Scenario_Result { renderive::Frame_Feedback_Snapshot state; std::uint64_t returned_frame_count{}; }; class Scenario_Simulation { public: explicit Scenario_Simulation(Scenario_Config config) : config(config) { controller.set_max_render_fps(config.max_render_fps); } Scenario_Result run(double duration_ms) { end_time = to_nanoseconds(duration_ms); next_input_time = to_nanoseconds(config.input_interval_ms); while (now < end_time) { start_consumer(); start_render(); std::uint64_t event_time = next_event_time(); if (event_time > end_time) event_time = end_time; now = event_time; if (now == next_input_time) receive_input(); if (now == render_complete_time) complete_render(); if (now == consume_complete_time) complete_consume(); } if (ready_frame) { ready_frame->superseded_time = now; controller.on_frame_superseded(*ready_frame, consuming_frame.has_value(), false); return_frame(*ready_frame); ready_frame.reset(); } if (consuming_frame) { consuming_frame->paint_end_time = now; consuming_frame->consumer_release_time = now; controller.on_frame_consumed(*consuming_frame, false); return_frame(*consuming_frame); consuming_frame.reset(); } return {controller.state(), returned_frames.size()}; } private: bool input_pending() const { return latest_input_version > published_input_version; } bool can_start_render() const { if (rendering_frame || !input_pending()) return false; return true; } void start_render() { if (!can_start_render() || !controller.rate_limit_ready(now)) return; renderive::Frame_Lifecycle_Record frame; frame.frame_id = config.id * 1000000 + ++next_frame_id; frame.input_version = latest_input_version; frame.core_data_time = latest_input_time; frame.render_request_time = latest_input_time; frame.render_queue_enter_time = now; frame.prepare_begin_time = now; frame.prepare_end_time = now; frame.pixel_width = 1920; frame.pixel_height = 1080; controller.on_render_started(frame, now); rendering_frame = frame; render_complete_time = now + to_nanoseconds(config.render_duration_ms); } void complete_render() { renderive::Frame_Lifecycle_Record frame = *rendering_frame; frame.draw_begin_time = frame.render_begin_time; frame.draw_end_time = now; frame.render_end_time = now; frame.publish_time = now; bool had_ready_frame = ready_frame.has_value(); controller.on_render_completed(frame, had_ready_frame, consuming_frame.has_value(), latest_input_version > frame.input_version); published_input_version = frame.input_version; if (ready_frame) { ready_frame->superseded_time = now; controller.on_frame_superseded(*ready_frame, consuming_frame.has_value(), input_pending()); return_frame(*ready_frame); } ready_frame = frame; rendering_frame.reset(); render_complete_time = Infinite_Time; } void start_consumer() { if (consuming_frame || !ready_frame) return; consuming_frame = *ready_frame; ready_frame.reset(); consuming_frame->update_request_id = consuming_frame->frame_id; consuming_frame->update_request_time = consuming_frame->publish_time; consuming_frame->paint_begin_time = now; consume_complete_time = now + to_nanoseconds(config.paint_duration_ms); } void complete_consume() { consuming_frame->paint_end_time = now; consuming_frame->consumer_release_time = now; controller.on_frame_consumed(*consuming_frame, ready_frame.has_value()); return_frame(*consuming_frame); consuming_frame.reset(); consume_complete_time = Infinite_Time; } void receive_input() { ++latest_input_version; latest_input_time = now; controller.on_input_changed(now, latest_input_version); next_input_time += to_nanoseconds(config.input_interval_ms); } std::uint64_t next_event_time() const { std::uint64_t event_time = std::min({next_input_time, render_complete_time, consume_complete_time, end_time}); if (can_start_render() && !controller.rate_limit_ready(now)) event_time = std::min(event_time, controller.next_render_time_ns()); return event_time; } void return_frame(renderive::Frame_Lifecycle_Record& frame) { renderive::write_frame_performance_log(frame); returned_frames.push_back(frame); } static constexpr std::uint64_t Infinite_Time = std::numeric_limits::max(); Scenario_Config config; renderive::Frame_Feedback_Controller controller; std::optional rendering_frame; std::optional ready_frame; std::optional consuming_frame; std::vector returned_frames; std::uint64_t now{}; std::uint64_t end_time{}; std::uint64_t next_input_time{}; std::uint64_t render_complete_time = Infinite_Time; std::uint64_t consume_complete_time = Infinite_Time; std::uint64_t latest_input_time{}; std::uint64_t latest_input_version{}; std::uint64_t published_input_version{}; std::uint64_t next_frame_id{}; }; bool in_range(double value, double minimum, double maximum) { return value >= minimum && value <= maximum; } std::vector read_log_lines(const QString& path) { QFile file(path); std::vector lines; if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return lines; while (!file.atEnd()) { QString line = QString::fromUtf8(file.readLine()).trimmed(); if (!line.isEmpty()) lines.push_back(line); } return lines; } bool has_line_containing(const std::vector& lines, const QString& text) { return std::any_of(lines.begin(), lines.end(), [&text](const QString& line) { return line.contains(text); }); } bool has_all_bottleneck_states(const std::vector& lines) { return has_line_containing(lines, "Input_Limited") && has_line_containing(lines, "Producer_Limited") && has_line_containing(lines, "Consumer_Limited") && has_line_containing(lines, "Manual_Limited"); } std::vector wait_log_lines(const QString& path, std::size_t minimum_line_count) { for (int i = 0; i < 50; ++i) { std::vector lines = read_log_lines(path); if (lines.size() >= minimum_line_count && has_all_bottleneck_states(lines)) return lines; std::this_thread::sleep_for(std::chrono::milliseconds(20)); } return read_log_lines(path); } void expect_header_field(const QString& header, const QString& field) { EXPECT_TRUE(header.split(',').contains(field)) << field.toStdString(); } void validate_scenario(const Scenario_Config& config, const Scenario_Result& result) { const renderive::Frame_Feedback_Snapshot& state = result.state; SCOPED_TRACE(config.name); EXPECT_EQ(state.bottleneck_state, config.expected_state); EXPECT_TRUE(in_range(state.production_rate_fps, config.minimum_production_fps, config.maximum_production_fps)) << state.production_rate_fps; EXPECT_TRUE(in_range(state.display_rate_fps, config.minimum_display_fps, config.maximum_display_fps)) << state.display_rate_fps; EXPECT_GT(result.returned_frame_count, 0); if (config.expected_state == renderive::Bottleneck_State::Consumer_Limited) { EXPECT_LE(std::abs(state.production_rate_fps - state.display_rate_fps), 6.0); EXPECT_GE(state.adaptive_min_render_interval_ms, config.paint_duration_ms * 0.8); } if (config.expected_state == renderive::Bottleneck_State::Manual_Limited) { double expected_interval = 1000.0 / config.max_render_fps; EXPECT_NEAR(state.effective_min_render_interval_ms, expected_interval, 0.2); } } void validate_performance_log(const QString& log_path) { std::vector lines = wait_log_lines(log_path, 8); ASSERT_GT(lines.size(), 1); const QString& header = lines.front(); EXPECT_TRUE(header.startsWith("frame_id,input_version,outcome,bottleneck")); EXPECT_FALSE(header.contains("policy")); expect_header_field(header, "production_fps"); expect_header_field(header, "display_fps"); expect_header_field(header, "adaptive_interval_ms"); expect_header_field(header, "effective_interval_ms"); expect_header_field(header, "superseded_ratio"); expect_header_field(header, "display_efficiency"); expect_header_field(header, "reused_front_frames"); expect_header_field(header, "renderable_stats"); expect_header_field(header, "renderable_cache_stats"); expect_header_field(header, "cache_rebuild_count"); expect_header_field(header, "cache_rebuild_time_ns"); expect_header_field(header, "cache_compose_time_ns"); int header_column_count = header.split(',').size(); EXPECT_EQ(lines[1].split(',').size(), header_column_count); EXPECT_TRUE(has_all_bottleneck_states(lines)); } } TEST(Frame_Scheduler_Feedback, KeepsPerformanceShowerInputRingBounded) { renderive::Performance_Shower_Input_Data input; for (std::uint64_t frame_id = 1; frame_id <= 72; ++frame_id) { renderive::Frame_Lifecycle_Record frame; frame.frame_id = frame_id; input.push(frame); } EXPECT_EQ(input.count, renderive::Performance_Shower_Input_Data::Capacity); EXPECT_EQ(input.capacity_dropped, 8); std::vector frame_ids; input.read_all([&frame_ids](const renderive::Frame_Lifecycle_Record& frame) { frame_ids.push_back(frame.frame_id); }); ASSERT_FALSE(frame_ids.empty()); EXPECT_EQ(frame_ids.front(), 9); EXPECT_EQ(frame_ids.back(), 72); input.request_reset(); EXPECT_EQ(input.count, 0); EXPECT_TRUE(input.reset_pending); } TEST(Frame_Scheduler_Feedback, AggregatesRenderableCacheStats) { renderive::Frame_Lifecycle_Record frame; frame.render_begin_time = 1000; frame.render_end_time = 5000; frame.record_renderable_render("root", {}, 0, renderive::Renderable_Cache_Mode::Direct, 1000); frame.record_renderable_self_draw("root", {}, 0, renderive::Renderable_Cache_Mode::Direct, 300); frame.record_renderable_render("cache_b", "cache_a", 1, renderive::Renderable_Cache_Mode::Local_Pixel, 2000); frame.record_renderable_cache_hit("cache_a", {}, 0); frame.record_renderable_cache_miss("cache_b", "cache_a", 1); frame.record_renderable_cache_rebuild("cache_b", "cache_a", 1, 1000, 4096, 1024, 3, 3); frame.record_renderable_cache_rebuild("cache_a", {}, 0, 2000, 2048, 512, 4, 4); frame.record_renderable_cache_compose("cache_a", {}, 0, 300, 2048, 512, 4, 4); frame.record_renderable_cache_compose("cache_b", "cache_a", 1, 700, 4096, 1024, 3, 3); EXPECT_EQ(frame.renderable_cache_hit_count, 1); EXPECT_EQ(frame.renderable_cache_miss_count, 1); EXPECT_EQ(frame.renderable_cache_rebuild_count, 2); EXPECT_EQ(frame.renderable_cache_rebuild_time_ns, 3000); EXPECT_EQ(frame.renderable_cache_compose_time_ns, 1000); ASSERT_EQ(frame.renderable_stats.size(), 2); EXPECT_FALSE(frame.renderable_stats[0].cache_enabled); EXPECT_TRUE(frame.renderable_stats[1].cache_enabled); EXPECT_EQ(frame.renderable_stats[1].tree_depth, 1); renderive::Frame_Performance_Aggregate aggregate; aggregate.update(frame); EXPECT_EQ(aggregate.render_time_ns, 4000); ASSERT_EQ(aggregate.renderable_stats.size(), 2); EXPECT_EQ(aggregate.renderable_stats[1].parent_object_name, "cache_a"); EXPECT_EQ(aggregate.renderable_stats[1].tree_depth, 1); EXPECT_TRUE(aggregate.renderable_stats[1].cache_enabled); EXPECT_GT(aggregate.renderable_stats[0].total_render_ms.times, 0); EXPECT_GT(aggregate.renderable_stats[0].self_draw_ms.times, 0); EXPECT_GT(aggregate.renderable_stats[1].total_render_ratio.times, 0); EXPECT_EQ(aggregate.cache_hit_count, 1); EXPECT_EQ(aggregate.cache_miss_count, 1); EXPECT_EQ(aggregate.cache_rebuild_count, 2); EXPECT_EQ(aggregate.cache_rebuild_time_ns, 3000); EXPECT_EQ(aggregate.cache_compose_time_ns, 1000); EXPECT_EQ(aggregate.renderable_cache_stats.size(), 2); EXPECT_EQ(aggregate.renderable_cache_stats[1].parent_object_name, "cache_a"); EXPECT_EQ(aggregate.renderable_cache_stats[1].tree_depth, 1); EXPECT_GT(aggregate.renderable_cache_stats[0].rebuild_ms.times, 0); EXPECT_GT(aggregate.renderable_cache_stats[0].compose_ms.times, 0); EXPECT_GT(aggregate.cache_rebuild_ms.times, 0); EXPECT_GT(aggregate.cache_compose_ms.times, 0); EXPECT_GT(aggregate.cache_rebuild_ratio.times, 0); EXPECT_GT(aggregate.cache_compose_ratio.times, 0); EXPECT_EQ(aggregate.renderable_cache_stats[0].subtree_compose_time_ns, 1000); EXPECT_EQ(aggregate.renderable_cache_stats[1].subtree_compose_time_ns, 700); EXPECT_GT(aggregate.renderable_cache_stats[0].subtree_compose_ms.times, 0); } TEST(Frame_Scheduler_Feedback, ClassifiesFourBottlenecksAndWritesUsableLog) { QString log_path = qEnvironmentVariable("RENDERIVE_PERFORMANCE_LOG_PATH"); if (log_path.isEmpty()) { log_path = QDir::current().filePath("renderive_feedback_test_log.csv"); qputenv("RENDERIVE_PERFORMANCE_LOG_PATH", log_path.toUtf8()); } qputenv("RENDERIVE_PERFORMANCE_LOG", "1"); if (!log_path.isEmpty()) QFile::remove(log_path); const std::array scenarios{{ {"Input_Limited", 1, 50.0, 2.0, 2.0, 0.0, renderive::Bottleneck_State::Input_Limited, 18.0, 22.0, 18.0, 22.0}, {"Producer_Limited", 2, 1.0, 20.0, 2.0, 0.0, renderive::Bottleneck_State::Producer_Limited, 45.0, 55.0, 45.0, 55.0}, {"Consumer_Limited", 3, 1.0, 2.0, 25.0, 0.0, renderive::Bottleneck_State::Consumer_Limited, 34.0, 46.0, 36.0, 44.0}, {"Manual_Limited", 4, 1.0, 2.0, 2.0, 30.0, renderive::Bottleneck_State::Manual_Limited, 28.0, 32.0, 28.0, 32.0} }}; for (const Scenario_Config& scenario : scenarios) { Scenario_Simulation simulation(scenario); Scenario_Result result = simulation.run(8000.0); validate_scenario(scenario, result); } validate_performance_log(log_path); } int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }