Files
Renderive/tests/Frame_Scheduler_Feedback.cpp
T

340 lines
15 KiB
C++

#include "../Renderive/plottable/Performance_Shower_p.h"
#include <QDir>
#include <QFile>
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <limits>
#include <optional>
#include <thread>
#include <vector>
namespace {
constexpr std::uint64_t Nanoseconds_Per_Millisecond = 1000000;
std::uint64_t to_nanoseconds(double milliseconds) {
return static_cast<std::uint64_t>(std::llround(milliseconds * static_cast<double>(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<std::uint64_t>::max();
Scenario_Config config;
renderive::Frame_Feedback_Controller controller;
std::optional<renderive::Frame_Lifecycle_Record> rendering_frame;
std::optional<renderive::Frame_Lifecycle_Record> ready_frame;
std::optional<renderive::Frame_Lifecycle_Record> consuming_frame;
std::vector<renderive::Frame_Lifecycle_Record> 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<QString> read_log_lines(const QString& path) {
QFile file(path);
std::vector<QString> 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<QString>& 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<QString>& 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<QString> wait_log_lines(const QString& path, std::size_t minimum_line_count) {
for (int i = 0; i < 50; ++i) {
std::vector<QString> 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<QString> 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,slot_generation,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, "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<std::uint64_t> 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<Scenario_Config, 4> 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();
}