340 lines
15 KiB
C++
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();
|
|
}
|