Files
Renderive/Core/plottable/Performance_Overlay_p.h
T
2026-08-03 11:53:39 +08:00

1693 lines
90 KiB
C++

#pragma once
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cctype>
#include <cstdlib>
#include <functional>
#include <limits>
#include <memory>
#include <mutex>
#include <string>
#include <string_view>
#include <system_error>
#include <unordered_map>
#include <utility>
#include <vector>
#include <rigtorp/MPMCQueue.h>
#include "../architecture/Frame_Scheduler.h"
#include "../architecture/Render_Data.h"
#include "../performance/Performance_Log_Service.h"
#include "../base/Color.h"
#include "../base/Geometry.h"
#include "../base/Memory.h"
#include "../base/Style.h"
#include "../base/String_Format.h"
#include "../base/Text.h"
#include "../render/Button.h"
#include "../render/Canvas.h"
#include "Performance_Overlay.h"
#include "Psc_Cpp_Core/Statistics/Statistics.h"
namespace renderive {
void write_frame_performance_log(std::shared_ptr<const Frame_Lifecycle_Record> frame);
struct Performance_Overlay_Render_State : Render_State, Performance_Overlay_Options {};
struct Performance_Overlay_Input_Data : Input_Data {};
struct Renderable_Cache_Aggregate_Stat {
std::uint64_t node_id{};
std::uint64_t parent_node_id{};
std::string object_name;
std::string parent_object_name;
int tree_depth{};
std::uint64_t hit_count{};
std::uint64_t miss_count{};
std::uint64_t rebuild_count{};
std::uint64_t rebuild_time_ns{};
std::uint64_t compose_time_ns{};
std::uint64_t subtree_compose_time_ns{};
std::uint64_t last_rebuild_time_ns{};
std::uint64_t last_compose_time_ns{};
std::uint64_t last_image_bytes{};
std::uint64_t last_image_area{};
std::uint64_t last_edit_version{};
std::uint64_t last_ready_version{};
Psc::Value_Statistics rebuild_ms;
Psc::Value_Statistics compose_ms;
Psc::Value_Statistics subtree_compose_ms;
};
struct Renderable_Aggregate_Stat {
std::uint64_t node_id{};
std::uint64_t parent_node_id{};
std::string object_name;
std::string parent_object_name;
int tree_depth{};
bool cache_enabled{};
std::uint64_t render_count{};
std::uint64_t self_draw_count{};
std::uint64_t total_render_time_ns{};
std::uint64_t self_draw_time_ns{};
std::uint64_t last_total_render_time_ns{};
std::uint64_t last_self_draw_time_ns{};
Psc::Value_Statistics total_render_ms;
Psc::Value_Statistics self_draw_ms;
Psc::Value_Statistics total_render_ratio;
Psc::Value_Statistics self_draw_ratio;
};
struct Performance_Aggregate {
Psc::Value_Statistics request_wait_ms;
Psc::Value_Statistics producer_interval_ms;
Psc::Value_Statistics admission_wait_ms;
Psc::Value_Statistics render_slot_idle_wait_ms;
Psc::Value_Statistics executor_queue_wait_ms;
Psc::Value_Statistics prepare_ms;
Psc::Value_Statistics draw_ms;
Psc::Value_Statistics render_ms;
Psc::Value_Statistics ready_wait_ms;
Psc::Value_Statistics update_wait_ms;
Psc::Value_Statistics paint_ms;
Psc::Value_Statistics display_interval_ms;
Psc::Value_Statistics effective_interval_ms;
Psc::Value_Statistics frame_age_ms;
Psc::Value_Statistics frame_total_ms;
Psc::Value_Statistics cache_rebuild_ms;
Psc::Value_Statistics cache_compose_ms;
Psc::Value_Statistics cache_rebuild_ratio;
Psc::Value_Statistics cache_compose_ratio;
Frame_Lifecycle_Record latest;
std::uint64_t observed_frame_count{};
std::uint64_t render_time_ns{};
std::uint64_t consumed_outcome_count{};
std::uint64_t superseded_outcome_count{};
std::uint64_t cancelled_outcome_count{};
std::uint64_t cache_hit_count{};
std::uint64_t cache_miss_count{};
std::uint64_t cache_rebuild_count{};
std::uint64_t cache_rebuild_time_ns{};
std::uint64_t cache_compose_time_ns{};
std::uint64_t input_capacity_dropped{};
bool has_frame{};
std::vector<Renderable_Aggregate_Stat> renderable_stats;
std::vector<Renderable_Cache_Aggregate_Stat> renderable_cache_stats;
void clear() {
*this = {};
}
static Frame_Lifecycle_Record summarize_frame(const Frame_Lifecycle_Record& frame) {
Frame_Lifecycle_Record summary;
summary.frame_id = frame.frame_id;
summary.input_version = frame.input_version;
summary.core_data_time_ns = frame.core_data_time_ns;
summary.render_request_ns = frame.render_request_ns;
summary.render_role_enter_ns = frame.render_role_enter_ns;
summary.render_begin_ns = frame.render_begin_ns;
summary.prepare_begin_ns = frame.prepare_begin_ns;
summary.prepare_end_ns = frame.prepare_end_ns;
summary.draw_begin_ns = frame.draw_begin_ns;
summary.draw_end_ns = frame.draw_end_ns;
summary.render_end_ns = frame.render_end_ns;
summary.transition_12_ns = frame.transition_12_ns;
summary.update_request_id = frame.update_request_id;
summary.update_request_time_ns = frame.update_request_time_ns;
summary.update_posted_ns = frame.update_posted_ns;
summary.transition_23_ns = frame.transition_23_ns;
summary.paint_begin_ns = frame.paint_begin_ns;
summary.paint_end_ns = frame.paint_end_ns;
summary.presented_frame_id = frame.presented_frame_id;
summary.transition_32_ns = frame.transition_32_ns;
summary.render_slot_idle_wait_ns = frame.render_slot_idle_wait_ns;
summary.executor_queue_wait_ns = frame.executor_queue_wait_ns;
summary.ready_queue_wait_ns = frame.ready_queue_wait_ns;
summary.present_queue_wait_ns = frame.present_queue_wait_ns;
summary.paint_return_wait_ns = frame.paint_return_wait_ns;
summary.superseded_time_ns = frame.superseded_time_ns;
summary.outcome = frame.outcome;
summary.refresh = frame.refresh;
summary.worker = frame.worker;
summary.raw_input_count = frame.raw_input_count;
summary.prepared_output_count = frame.prepared_output_count;
summary.renderable_cache_hit_count = frame.renderable_cache_hit_count;
summary.renderable_cache_miss_count = frame.renderable_cache_miss_count;
summary.renderable_cache_rebuild_count = frame.renderable_cache_rebuild_count;
summary.renderable_cache_rebuild_time_ns = frame.renderable_cache_rebuild_time_ns;
summary.renderable_cache_compose_time_ns = frame.renderable_cache_compose_time_ns;
summary.renderable_cache_image_bytes = frame.renderable_cache_image_bytes;
summary.renderable_cache_image_area = frame.renderable_cache_image_area;
summary.pixel_width = frame.pixel_width;
summary.pixel_height = frame.pixel_height;
summary.viewport_version = frame.viewport_version;
return summary;
}
void update(const Frame_Lifecycle_Record& frame) {
latest = summarize_frame(frame);
has_frame = true;
++observed_frame_count;
std::uint64_t frame_render_time_ns = duration_ns(frame.render_begin_ns, frame.render_end_ns);
render_time_ns += frame_render_time_ns;
update_duration(request_wait_ms, frame.render_request_ns, frame.render_begin_ns);
if (frame.refresh.producer_interval_ns)
producer_interval_ms.update(ns_to_ms(frame.refresh.producer_interval_ns));
admission_wait_ms.update(ns_to_ms(frame.refresh.admission_wait_elapsed_ns));
render_slot_idle_wait_ms.update(ns_to_ms(frame.render_slot_idle_wait_ns));
executor_queue_wait_ms.update(ns_to_ms(frame.executor_queue_wait_ns));
update_duration(prepare_ms, frame.prepare_begin_ns, frame.prepare_end_ns);
update_duration(draw_ms, frame.draw_begin_ns, frame.draw_end_ns);
update_duration(render_ms, frame.render_begin_ns, frame.render_end_ns);
update_duration(ready_wait_ms, frame.transition_12_ns, frame.paint_begin_ns ? frame.paint_begin_ns : frame.superseded_time_ns);
update_duration(update_wait_ms, frame.update_request_time_ns, frame.paint_begin_ns);
update_duration(paint_ms, frame.paint_begin_ns, frame.paint_end_ns);
if (frame.refresh.display_frame_interval_ns)
display_interval_ms.update(ns_to_ms(frame.refresh.display_frame_interval_ns));
if (frame.refresh.effective_min_render_interval_ns)
effective_interval_ms.update(ns_to_ms(frame.refresh.effective_min_render_interval_ns));
update_duration(frame_age_ms, frame.core_data_time_ns, frame.paint_end_ns);
update_duration(frame_total_ms, frame.render_request_ns, frame.transition_32_ns ? frame.transition_32_ns : frame.superseded_time_ns);
cache_hit_count += frame.renderable_cache_hit_count;
cache_miss_count += frame.renderable_cache_miss_count;
cache_rebuild_count += frame.renderable_cache_rebuild_count;
cache_rebuild_time_ns += frame.renderable_cache_rebuild_time_ns;
cache_compose_time_ns += frame.renderable_cache_compose_time_ns;
if (frame.renderable_cache_rebuild_time_ns)
cache_rebuild_ms.update(ns_to_ms(frame.renderable_cache_rebuild_time_ns));
if (frame.renderable_cache_compose_time_ns)
cache_compose_ms.update(ns_to_ms(frame.renderable_cache_compose_time_ns));
if (frame_render_time_ns) {
cache_rebuild_ratio.update(static_cast<double>(frame.renderable_cache_rebuild_time_ns) * 100.0 / static_cast<double>(frame_render_time_ns));
cache_compose_ratio.update(static_cast<double>(frame.renderable_cache_compose_time_ns) * 100.0 / static_cast<double>(frame_render_time_ns));
}
for (const auto& stat : frame.renderable_stats) {
Renderable_Aggregate_Stat& aggregate_stat = renderable_stat(stat.node_id, stat.parent_node_id, stat.object_name, stat.parent_object_name, stat.tree_depth);
aggregate_stat.cache_enabled = stat.cache_enabled;
aggregate_stat.render_count += stat.render_count;
aggregate_stat.self_draw_count += stat.self_draw_count;
aggregate_stat.total_render_time_ns += stat.total_render_time_ns;
aggregate_stat.self_draw_time_ns += stat.self_draw_time_ns;
aggregate_stat.last_total_render_time_ns = stat.last_total_render_time_ns ? stat.last_total_render_time_ns : aggregate_stat.last_total_render_time_ns;
aggregate_stat.last_self_draw_time_ns = stat.last_self_draw_time_ns ? stat.last_self_draw_time_ns : aggregate_stat.last_self_draw_time_ns;
if (stat.total_render_time_ns)
aggregate_stat.total_render_ms.update(ns_to_ms(stat.total_render_time_ns));
if (stat.self_draw_time_ns)
aggregate_stat.self_draw_ms.update(ns_to_ms(stat.self_draw_time_ns));
if (frame_render_time_ns) {
aggregate_stat.total_render_ratio.update(static_cast<double>(stat.total_render_time_ns) * 100.0 / static_cast<double>(frame_render_time_ns));
aggregate_stat.self_draw_ratio.update(static_cast<double>(stat.self_draw_time_ns) * 100.0 / static_cast<double>(frame_render_time_ns));
}
}
for (const auto& stat : frame.renderable_cache_stats) {
Renderable_Cache_Aggregate_Stat& aggregate_stat = cache_stat(stat.node_id, stat.parent_node_id, stat.object_name, stat.parent_object_name, stat.tree_depth);
aggregate_stat.hit_count += stat.hit_count;
aggregate_stat.miss_count += stat.miss_count;
aggregate_stat.rebuild_count += stat.rebuild_count;
aggregate_stat.rebuild_time_ns += stat.rebuild_time_ns;
aggregate_stat.compose_time_ns += stat.compose_time_ns;
aggregate_stat.last_rebuild_time_ns = stat.last_rebuild_time_ns ? stat.last_rebuild_time_ns : aggregate_stat.last_rebuild_time_ns;
aggregate_stat.last_compose_time_ns = stat.last_compose_time_ns ? stat.last_compose_time_ns : aggregate_stat.last_compose_time_ns;
aggregate_stat.last_image_bytes = stat.last_image_bytes ? stat.last_image_bytes : aggregate_stat.last_image_bytes;
aggregate_stat.last_image_area = stat.last_image_area ? stat.last_image_area : aggregate_stat.last_image_area;
aggregate_stat.last_edit_version = stat.last_edit_version ? stat.last_edit_version : aggregate_stat.last_edit_version;
aggregate_stat.last_ready_version = stat.last_ready_version ? stat.last_ready_version : aggregate_stat.last_ready_version;
if (stat.rebuild_time_ns)
aggregate_stat.rebuild_ms.update(ns_to_ms(stat.rebuild_time_ns));
if (stat.compose_time_ns)
aggregate_stat.compose_ms.update(ns_to_ms(stat.compose_time_ns));
}
update_cache_subtree_compose(frame.renderable_cache_stats);
switch (frame.outcome) {
case Frame_Outcome::Presented:
++consumed_outcome_count;
break;
case Frame_Outcome::Superseded:
++superseded_outcome_count;
break;
case Frame_Outcome::Render_Acquire_Dropped:
case Frame_Outcome::Publish_Dropped:
case Frame_Outcome::Paint_Acquire_Dropped:
case Frame_Outcome::Worker_Budget_Dropped:
case Frame_Outcome::Cancelled:
++cancelled_outcome_count;
break;
}
}
private:
Renderable_Aggregate_Stat& renderable_stat(std::uint64_t node_id, std::uint64_t parent_node_id, const std::string& object_name, const std::string& parent_object_name, int tree_depth) {
for (auto& stat : renderable_stats) {
if (stat.node_id == node_id && stat.parent_node_id == parent_node_id && stat.tree_depth == tree_depth)
return stat;
}
Renderable_Aggregate_Stat stat;
stat.node_id = node_id;
stat.parent_node_id = parent_node_id;
stat.object_name = object_name;
stat.parent_object_name = parent_object_name;
stat.tree_depth = tree_depth;
renderable_stats.push_back(std::move(stat));
return renderable_stats.back();
}
void update_cache_subtree_compose(const std::vector<Renderable_Cache_Frame_Stats>& frame_stats) {
for (const auto& stat : frame_stats) {
std::uint64_t subtree_compose_time_ns = renderable_cache_subtree_compose_time(frame_stats, stat);
if (!subtree_compose_time_ns)
continue;
Renderable_Cache_Aggregate_Stat& aggregate_stat = cache_stat(stat.node_id, stat.parent_node_id, stat.object_name, stat.parent_object_name, stat.tree_depth);
aggregate_stat.subtree_compose_time_ns += subtree_compose_time_ns;
aggregate_stat.subtree_compose_ms.update(ns_to_ms(subtree_compose_time_ns));
}
}
Renderable_Cache_Aggregate_Stat& cache_stat(std::uint64_t node_id, std::uint64_t parent_node_id, const std::string& object_name, const std::string& parent_object_name, int tree_depth) {
for (auto& stat : renderable_cache_stats) {
if (stat.node_id == node_id && stat.parent_node_id == parent_node_id)
return stat;
}
Renderable_Cache_Aggregate_Stat stat;
stat.node_id = node_id;
stat.parent_node_id = parent_node_id;
stat.object_name = object_name;
stat.parent_object_name = parent_object_name;
stat.tree_depth = tree_depth;
renderable_cache_stats.push_back(std::move(stat));
return renderable_cache_stats.back();
}
static double ns_to_ms(std::uint64_t ns) {
return static_cast<double>(ns) / 1000000.0;
}
static std::uint64_t duration_ns(std::uint64_t begin_ns, std::uint64_t end_ns) {
return end_ns > begin_ns ? end_ns - begin_ns : 0;
}
static void update_duration(Psc::Value_Statistics& statistics, std::uint64_t begin_ns, std::uint64_t end_ns) {
if (end_ns > begin_ns)
statistics.update(static_cast<double>(end_ns - begin_ns) / 1000000.0);
}
};
enum class Performance_Metric_Id : std::uint16_t {
Frame_Id,
Input_Version,
Render_Period,
Paint_Period,
User_Period,
Target_Period,
Render_Slot_Idle_Wait,
Executor_Queue_Wait,
Ready_Queue_Wait,
Present_Queue_Wait,
Paint_Return_Wait,
Worker_Count,
Active_Workers,
Queued_Frame_Jobs,
Active_Frame_Jobs,
Submitted_Topologies,
Started_Topologies,
Completed_Topologies,
Started_Task_Nodes,
Completed_Task_Nodes,
Rejected_Frame_Jobs,
Topology_Queue_Wait_Avg,
Topology_Queue_Wait_Max,
Task_Node_Run_Avg,
Task_Node_Run_Max,
Frame_Business_Task_Count,
Frame_Business_Task_Peak_Parallelism,
Frame_Topology_Queue_Wait,
Frame_Business_Task_Run_Total,
Outcome,
Cache_Hit,
Cache_Miss,
Cache_Ratio,
Cache_Rebuild_Count,
Cache_Rebuild_Time,
Cache_Compose_Time,
Cache_Image_Area,
Cache_Image_Bytes,
Cache_Edit_Version,
Cache_Ready_Version,
Current_Ms,
Smooth_Ms,
Variation_Ms,
P50_Ms,
P95_Ms,
P99_Ms,
Current_Percent,
Smooth_Percent,
Variation_Percent,
P50_Percent,
P95_Percent,
P99_Percent,
Raw_Input_Count,
Prepared_Output_Count,
Observed_Frame_Count,
Consumed_Frame_Count,
Superseded_Frame_Count,
Cancelled_Frame_Count,
Dropped_Input_Count,
Generic_Count,
Generic_Number,
Count
};
inline constexpr std::size_t performance_field_count = static_cast<std::size_t>(Performance_Metric_Id::Count);
struct Performance_Field_Layout_State {
double max_value_width{};
};
struct Performance_Text_Span {
bool value{};
Performance_Metric_Id field_id = Performance_Metric_Id::Generic_Number;
Performance_Text_Role role = Performance_Text_Role::Value;
std::string text;
std::string field_key;
double measured_width{};
double reserved_width{};
double x_offset{};
int column_index{};
};
struct Performance_Line_Layout {
std::vector<Performance_Text_Span> runs;
double width{};
};
using Performance_Frame_Record = std::shared_ptr<const Frame_Lifecycle_Record>;
struct Performance_Worker_State {
static constexpr std::size_t Capacity = 256;
Performance_Worker_State()
: metric_queue(Capacity) {}
rigtorp::MPMCQueue<Performance_Frame_Record> metric_queue;
std::atomic<std::uint64_t> dropped_frame_record_count{0};
std::atomic_bool enabled{false};
std::atomic_bool worker_active{false};
std::atomic_bool work_requested{false};
std::atomic_bool cancelled{false};
std::atomic_bool reset_requested{false};
std::atomic_bool snapshot_rebuild_requested{false};
std::atomic_bool repaint_requested{false};
std::atomic<Button_State> copy_button_state{Button_State::Normal};
std::atomic<Button_State> close_button_state{Button_State::Normal};
std::atomic<double> scroll_offset{0.0};
std::atomic<double> pending_wheel_delta_px{0.0};
std::atomic<int> pending_page_delta{0};
std::atomic_bool drag_active{false};
std::atomic_bool drag_end_requested{false};
std::atomic<double> drag_position_y{0.0};
std::atomic<double> drag_anchor_y{0.0};
std::atomic<int> viewport_width{0};
std::atomic<int> viewport_height{0};
std::atomic<std::uint64_t> viewport_version{0};
std::atomic<std::uint64_t> next_content_version{1};
Performance_Aggregate aggregate;
std::array<Performance_Field_Layout_State, performance_field_count> field_layout_states{};
std::unordered_map<std::string, double> field_max_value_widths;
RectF last_scroll_track_rect;
RectF last_scroll_thumb_rect;
double last_content_viewport_height{};
double last_line_height{1.0};
double maximum_scroll_offset{};
double maximum_content_width{};
mutable std::mutex options_mutex;
Performance_Overlay_Options options;
std::mutex update_callback_mutex;
std::function<void()> update_callback;
std::atomic<std::shared_ptr<const Performance_Display_Snapshot>> published_snapshot;
};
inline double performance_clamp_scroll_offset(double offset, double max_offset) {
if (!std::isfinite(offset))
return 0.0;
if (max_offset <= 0.0)
return 0.0;
return std::clamp(offset, 0.0, max_offset);
}
inline double performance_atomic_exchange(std::atomic<double>& value, double replacement = 0.0) {
double current = value.load(std::memory_order_acquire);
while (!value.compare_exchange_weak(current, replacement, std::memory_order_acq_rel, std::memory_order_acquire)) {}
return current;
}
inline void performance_atomic_add(std::atomic<double>& value, double delta) {
double current = value.load(std::memory_order_acquire);
while (!value.compare_exchange_weak(current, current + delta, std::memory_order_acq_rel, std::memory_order_acquire)) {}
}
void schedule_performance_metrics_worker(const std::shared_ptr<Performance_Worker_State>& state, bool request_repaint = false);
struct Performance_Metric_Cell {
std::string label;
std::string value;
std::string unit;
Performance_Metric_Id metric_id = Performance_Metric_Id::Generic_Number;
Performance_Text_Role value_role = Performance_Text_Role::Value;
};
struct Performance_Metric_Row {
std::vector<Performance_Metric_Cell> cells;
};
struct Performance_Metric_Section {
std::string title;
std::vector<Performance_Metric_Row> rows;
};
struct Performance_Layout_Builder {
Performance_Aggregate aggregate;
std::vector<Performance_Metric_Section> sections;
std::vector<std::string> lines;
std::vector<Performance_Line_Layout> render_lines;
std::vector<Performance_Display_Text_Run> text_runs;
std::array<Performance_Field_Layout_State, performance_field_count> field_layout_states{};
std::unordered_map<std::string, double> field_max_value_widths;
RectF panel_rect;
RectF scroll_bar_rect;
RectF scroll_thumb_rect;
Button copy_button{"Copy"};
Button close_button{"Close"};
double content_width{};
double toolbar_height{};
double button_gap{4.0};
static std::string value(double number, int precision = 2) {
return format_number(number, {.precision = precision});
}
double panel_height(double line_height, Performance_Overlay_Render_State* state, int plot_height) const {
return std::min(content_height(line_height, state), static_cast<double>(std::max(0, plot_height)));
}
double text_top(Performance_Overlay_Render_State* state) const {
return static_cast<double>(state->top_margin) + toolbar_height + 4.0;
}
double content_height(double line_height, Performance_Overlay_Render_State* state) const {
return line_height * static_cast<double>(render_lines.size()) + text_top(state) + state->bottom_margin;
}
double content_viewport_height(Performance_Overlay_Render_State* state, double height) const {
return std::max(0.0, height - text_top(state) - state->bottom_margin);
}
void update_toolbar_button_rects(Performance_Overlay_Render_State* state) {
copy_button.set_rect({});
close_button.set_rect({});
if (panel_rect.empty() || toolbar_height <= 0.0)
return;
double right = panel_rect.width - static_cast<double>(state->right_margin + state->scroll_bar_width + state->scroll_bar_margin * 2);
double left = static_cast<double>(state->left_margin);
double available_width = std::max(0.0, right - left);
double close_width = std::min(close_button.preferred_width(), available_width);
if (close_width <= 0.0)
return;
close_button.set_rect(RectF{right - close_width, static_cast<double>(state->top_margin), close_width, toolbar_height});
available_width = std::max(0.0, close_button.rect().x - button_gap - left);
double copy_width = std::min(copy_button.preferred_width(), available_width);
if (copy_width <= 0.0)
return;
copy_button.set_rect(RectF{close_button.rect().x - button_gap - copy_width, static_cast<double>(state->top_margin), copy_width, toolbar_height});
}
void update_scroll_bar_rects(Performance_Overlay_Render_State* state, double max_scroll, double scroll, double visible_height, double total_height) {
if (max_scroll <= 0.0 || panel_rect.width <= 0.0 || panel_rect.height <= 0.0 || total_height <= 0.0) {
scroll_bar_rect = {};
scroll_thumb_rect = {};
return;
}
double track_height = std::max(0.0, panel_rect.height - static_cast<double>(state->scroll_bar_margin * 2));
scroll_bar_rect = RectF{
panel_rect.x + panel_rect.width - static_cast<double>(state->scroll_bar_width + state->scroll_bar_margin),
static_cast<double>(state->scroll_bar_margin),
static_cast<double>(state->scroll_bar_width),
track_height
};
double thumb_height = std::max(16.0, scroll_bar_rect.height * visible_height / std::max(visible_height, total_height));
thumb_height = std::min(thumb_height, scroll_bar_rect.height);
double travel = std::max(0.0, scroll_bar_rect.height - thumb_height);
double thumb_y = scroll_bar_rect.y + (max_scroll > 0.0 ? travel * scroll / max_scroll : 0.0);
scroll_thumb_rect = RectF{scroll_bar_rect.x, thumb_y, scroll_bar_rect.width, thumb_height};
}
static bool ends_with(std::string_view text, std::string_view suffix) {
return text.size() >= suffix.size() && text.substr(text.size() - suffix.size()) == suffix;
}
static bool contains(std::string_view text, std::string_view needle) {
return text.find(needle) != std::string_view::npos;
}
static Performance_Metric_Id field_id_from_key(std::string_view key) {
if (key == "frame_id")
return Performance_Metric_Id::Frame_Id;
if (key == "input_version")
return Performance_Metric_Id::Input_Version;
if (key == "render_period")
return Performance_Metric_Id::Render_Period;
if (key == "paint_period")
return Performance_Metric_Id::Paint_Period;
if (key == "user_period")
return Performance_Metric_Id::User_Period;
if (key == "target_period")
return Performance_Metric_Id::Target_Period;
if (key == "render_slot_idle_wait")
return Performance_Metric_Id::Render_Slot_Idle_Wait;
if (key == "executor_queue_wait")
return Performance_Metric_Id::Executor_Queue_Wait;
if (key == "ready_queue_wait")
return Performance_Metric_Id::Ready_Queue_Wait;
if (key == "present_queue_wait")
return Performance_Metric_Id::Present_Queue_Wait;
if (key == "paint_return_wait")
return Performance_Metric_Id::Paint_Return_Wait;
if (key == "taskflow_workers")
return Performance_Metric_Id::Worker_Count;
if (key == "taskflow_active")
return Performance_Metric_Id::Active_Workers;
if (key == "taskflow_queued_frames")
return Performance_Metric_Id::Queued_Frame_Jobs;
if (key == "taskflow_active_frames")
return Performance_Metric_Id::Active_Frame_Jobs;
if (key == "taskflow_submitted")
return Performance_Metric_Id::Submitted_Topologies;
if (key == "taskflow_topologies_started")
return Performance_Metric_Id::Started_Topologies;
if (key == "taskflow_topologies_completed")
return Performance_Metric_Id::Completed_Topologies;
if (key == "taskflow_started")
return Performance_Metric_Id::Started_Task_Nodes;
if (key == "taskflow_completed")
return Performance_Metric_Id::Completed_Task_Nodes;
if (key == "taskflow_rejected")
return Performance_Metric_Id::Rejected_Frame_Jobs;
if (key == "taskflow_queue_wait_avg")
return Performance_Metric_Id::Topology_Queue_Wait_Avg;
if (key == "taskflow_queue_wait_max")
return Performance_Metric_Id::Topology_Queue_Wait_Max;
if (key == "taskflow_run_avg")
return Performance_Metric_Id::Task_Node_Run_Avg;
if (key == "taskflow_run_max")
return Performance_Metric_Id::Task_Node_Run_Max;
if (key == "frame_worker_tasks")
return Performance_Metric_Id::Frame_Business_Task_Count;
if (key == "frame_worker_peak")
return Performance_Metric_Id::Frame_Business_Task_Peak_Parallelism;
if (key == "frame_worker_wait_max")
return Performance_Metric_Id::Frame_Topology_Queue_Wait;
if (key == "frame_worker_run_total")
return Performance_Metric_Id::Frame_Business_Task_Run_Total;
if (key == "frame_outcome")
return Performance_Metric_Id::Outcome;
if (key == "cache_hit_count")
return Performance_Metric_Id::Cache_Hit;
if (key == "cache_miss_count")
return Performance_Metric_Id::Cache_Miss;
if (key == "cache_hit_ratio")
return Performance_Metric_Id::Cache_Ratio;
if (key == "cache_rebuild_count")
return Performance_Metric_Id::Cache_Rebuild_Count;
if (key == "cache_rebuild_time")
return Performance_Metric_Id::Cache_Rebuild_Time;
if (key == "cache_compose_time")
return Performance_Metric_Id::Cache_Compose_Time;
if (key == "raw_input_count")
return Performance_Metric_Id::Raw_Input_Count;
if (key == "prepared_output_count")
return Performance_Metric_Id::Prepared_Output_Count;
if (key == "observed_frame_count")
return Performance_Metric_Id::Observed_Frame_Count;
if (key == "consumed_outcome_count")
return Performance_Metric_Id::Consumed_Frame_Count;
if (key == "superseded_outcome_count")
return Performance_Metric_Id::Superseded_Frame_Count;
if (key == "cancelled_outcome_count")
return Performance_Metric_Id::Cancelled_Frame_Count;
if (key == "input_capacity_dropped")
return Performance_Metric_Id::Dropped_Input_Count;
if (ends_with(key, "_cache_hit"))
return Performance_Metric_Id::Cache_Hit;
if (ends_with(key, "_cache_miss"))
return Performance_Metric_Id::Cache_Miss;
if (ends_with(key, "_cache_rebuild"))
return Performance_Metric_Id::Cache_Rebuild_Count;
if (ends_with(key, "_cache_ratio"))
return Performance_Metric_Id::Cache_Ratio;
if (ends_with(key, "_cache_image_area"))
return Performance_Metric_Id::Cache_Image_Area;
if (ends_with(key, "_cache_image_bytes"))
return Performance_Metric_Id::Cache_Image_Bytes;
if (ends_with(key, "_cache_edit_version"))
return Performance_Metric_Id::Cache_Edit_Version;
if (ends_with(key, "_cache_ready_version"))
return Performance_Metric_Id::Cache_Ready_Version;
if (contains(key, "ratio")) {
if (ends_with(key, "_instant"))
return Performance_Metric_Id::Current_Percent;
if (ends_with(key, "_smooth"))
return Performance_Metric_Id::Smooth_Percent;
if (ends_with(key, "_variation"))
return Performance_Metric_Id::Variation_Percent;
if (ends_with(key, "_p50"))
return Performance_Metric_Id::P50_Percent;
if (ends_with(key, "_p95"))
return Performance_Metric_Id::P95_Percent;
if (ends_with(key, "_p99"))
return Performance_Metric_Id::P99_Percent;
return Performance_Metric_Id::Current_Percent;
}
if (ends_with(key, "_instant"))
return Performance_Metric_Id::Current_Ms;
if (ends_with(key, "_smooth") || ends_with(key, "_srtt"))
return Performance_Metric_Id::Smooth_Ms;
if (ends_with(key, "_variation") || ends_with(key, "_jitter"))
return Performance_Metric_Id::Variation_Ms;
if (ends_with(key, "_p50"))
return Performance_Metric_Id::P50_Ms;
if (ends_with(key, "_p95"))
return Performance_Metric_Id::P95_Ms;
if (ends_with(key, "_p99"))
return Performance_Metric_Id::P99_Ms;
if (ends_with(key, "_fps") || ends_with(key, "_count"))
return Performance_Metric_Id::Generic_Count;
return Performance_Metric_Id::Generic_Number;
}
static constexpr std::array<Frame_Outcome, 7> outcome_values() {
return {
Frame_Outcome::Presented,
Frame_Outcome::Superseded,
Frame_Outcome::Render_Acquire_Dropped,
Frame_Outcome::Publish_Dropped,
Frame_Outcome::Paint_Acquire_Dropped,
Frame_Outcome::Worker_Budget_Dropped,
Frame_Outcome::Cancelled
};
}
void seed_static_field_widths(Canvas& canvas) {
for (Frame_Outcome outcome : outcome_values()) {
Performance_Text_Span run;
run.value = true;
run.field_id = Performance_Metric_Id::Outcome;
run.text = outcome_text(outcome);
measure_run(canvas, run);
}
}
std::string number_text(const std::string& key, double number, int precision = 2) {
return value(number, precision);
}
std::string count_text(const std::string& key, std::uint64_t count) {
return std::to_string(count);
}
std::string duration_line(const std::string& key, const std::string& name, const Psc::Value_Statistics& statistics) {
return name + " current:" + number_text(key + "_instant", statistics.instant) + "ms smooth:" + number_text(key + "_smooth", statistics.smooth) + "ms P50:" + number_text(key + "_p50", statistics.p50) + "ms P95:" + number_text(key + "_p95", statistics.p95) + "ms P99:" + number_text(key + "_p99", statistics.p99) + "ms";
}
std::string percent_stat_line(const std::string& key, const std::string& name, const Psc::Value_Statistics& statistics) {
return name + " current:" + number_text(key + "_instant", statistics.instant) + "% smooth:" + number_text(key + "_smooth", statistics.smooth) + "% P50:" + number_text(key + "_p50", statistics.p50) + "% P95:" + number_text(key + "_p95", statistics.p95) + "% P99:" + number_text(key + "_p99", statistics.p99) + "%";
}
std::string interval_line(const std::string& key, const std::string& name, double srtt, double jitter, double fps) {
return name + " interval_smooth:" + number_text(key + "_srtt", srtt) + "ms var:" + number_text(key + "_jitter", jitter) + "ms actual:" + number_text(key + "_fps", fps) + "fps";
}
static double percent(double value) {
return value * 100.0;
}
std::string percent_text(const std::string& key, double ratio) {
return number_text(key, percent(ratio)) + "%";
}
static std::string limit_source_text(Refresh_Limit_Source source) {
switch (source) {
case Refresh_Limit_Source::Unknown:
return "unknown";
case Refresh_Limit_Source::Input:
return "input";
case Refresh_Limit_Source::Render:
return "render";
case Refresh_Limit_Source::Paint:
return "paint";
case Refresh_Limit_Source::User:
return "user";
}
return "unknown";
}
static std::string bottleneck_text(Low_Latency_Bottleneck_State state) {
switch (state) {
case Low_Latency_Bottleneck_State::Unknown:
return "unknown";
case Low_Latency_Bottleneck_State::Input_Limited:
return "input";
case Low_Latency_Bottleneck_State::Producer_Limited:
return "producer";
case Low_Latency_Bottleneck_State::Consumer_Limited:
return "consumer";
case Low_Latency_Bottleneck_State::User_Limited:
return "user";
}
return "unknown";
}
static std::string production_mode_text(Low_Latency_Production_Mode mode) {
switch (mode) {
case Low_Latency_Production_Mode::Latency_Eager:
return "latency_eager";
case Low_Latency_Production_Mode::Consumer_Paced:
return "consumer_paced";
}
return "latency_eager";
}
static std::string limit_reason_text(Low_Latency_Limit_Reason reason) {
switch (reason) {
case Low_Latency_Limit_Reason::User_Fps:
return "user_fps";
case Low_Latency_Limit_Reason::Consumer_Capacity:
return "consumer_capacity";
case Low_Latency_Limit_Reason::Render_Capacity:
return "render_capacity";
case Low_Latency_Limit_Reason::No_Work:
return "no_work";
case Low_Latency_Limit_Reason::Shutdown:
return "shutdown";
}
return "user_fps";
}
static std::string consumer_confidence_text(Consumer_Confidence confidence) {
switch (confidence) {
case Consumer_Confidence::Low:
return "low";
case Consumer_Confidence::Medium:
return "medium";
case Consumer_Confidence::High:
return "high";
}
return "low";
}
static std::string consumer_sample_result_text(Consumer_Sample_Result result) {
switch (result) {
case Consumer_Sample_Result::Accepted:
return "accepted";
case Consumer_Sample_Result::Rejected_No_Backlog:
return "rejected_no_backlog";
case Consumer_Sample_Result::Rejected_Producer_Starved:
return "rejected_producer_starved";
case Consumer_Sample_Result::Rejected_No_New_Frame:
return "rejected_no_new_frame";
case Consumer_Sample_Result::Rejected_Invalid_Timestamp:
return "rejected_invalid_timestamp";
}
return "rejected_invalid_timestamp";
}
static std::string feedback_decision_text(Low_Latency_Feedback_Decision decision) {
switch (decision) {
case Low_Latency_Feedback_Decision::Hold_User_Period:
return "hold_user_period";
case Low_Latency_Feedback_Decision::Enter_Consumer_Paced:
return "enter_consumer_paced";
case Low_Latency_Feedback_Decision::Exit_Consumer_Paced:
return "exit_consumer_paced";
case Low_Latency_Feedback_Decision::Reject_Consumer_Sample:
return "reject_consumer_sample";
case Low_Latency_Feedback_Decision::Producer_Starved:
return "producer_starved";
}
return "hold_user_period";
}
static std::string dirty_reason_text(std::uint32_t mask) {
std::string result;
auto append = [&result](const char* text) {
if (!result.empty())
result += "|";
result += text;
};
if (mask & dirty_reason_bit(Dirty_Reason::Core_Data))
append("core");
if (mask & dirty_reason_bit(Dirty_Reason::Config))
append("config");
if (mask & dirty_reason_bit(Dirty_Reason::Viewport))
append("viewport");
return result.empty() ? "none" : result;
}
static std::string outcome_text(Frame_Outcome outcome) {
switch (outcome) {
case Frame_Outcome::Presented:
return "presented";
case Frame_Outcome::Superseded:
return "superseded";
case Frame_Outcome::Render_Acquire_Dropped:
return "render acquire dropped";
case Frame_Outcome::Publish_Dropped:
return "publish dropped";
case Frame_Outcome::Paint_Acquire_Dropped:
return "paint acquire dropped";
case Frame_Outcome::Worker_Budget_Dropped:
return "worker budget dropped";
case Frame_Outcome::Cancelled:
return "cancelled";
}
return "unknown";
}
std::string period_text(const std::string& key, std::uint64_t ns) {
return number_text(key, static_cast<double>(ns) / 1000000.0) + "ms";
}
static double cache_total(const Renderable_Cache_Aggregate_Stat& stat) {
return static_cast<double>(stat.hit_count + stat.miss_count);
}
std::string cache_ratio_text(const std::string& key, std::uint64_t hit, std::uint64_t miss) {
std::uint64_t total = hit + miss;
if (!total)
return number_text(key, 0.0) + "%";
return number_text(key, static_cast<double>(hit) * 100.0 / static_cast<double>(total)) + "%";
}
std::string ns_text(const std::string& key, std::uint64_t ns) {
return number_text(key, static_cast<double>(ns) / 1000000.0);
}
std::string cache_key(const Renderable_Cache_Aggregate_Stat& stat) const {
return stat.parent_object_name + "/" + stat.object_name;
}
std::string renderable_key(const Renderable_Aggregate_Stat& stat) const {
return stat.parent_object_name + "/" + stat.object_name + "/" + std::to_string(stat.tree_depth);
}
static std::string indent_str(int depth) {
return std::string(static_cast<std::size_t>(std::max(0, depth)) * 2, ' ');
}
static std::string cache_mode_text(bool cache_enabled) {
return cache_enabled ? "cached" : "direct";
}
static std::uint64_t frame_render_time_ns(const Frame_Lifecycle_Record& frame) {
return frame.render_end_ns > frame.render_begin_ns ? frame.render_end_ns - frame.render_begin_ns : 0;
}
std::string ns_ratio_text(const std::string& key, std::uint64_t numerator, std::uint64_t denominator) {
if (!denominator)
return number_text(key, 0.0) + "%";
return number_text(key, static_cast<double>(numerator) * 100.0 / static_cast<double>(denominator)) + "%";
}
bool should_show_cache(const Renderable_Cache_Aggregate_Stat& stat) const {
return cache_total(stat) > 0.0 || stat.rebuild_count || stat.rebuild_ms.times || stat.compose_ms.times || stat.subtree_compose_ms.times;
}
bool should_show_renderable(const Renderable_Aggregate_Stat& stat) const {
return stat.render_count || stat.self_draw_count || stat.total_render_ms.times || stat.self_draw_ms.times;
}
std::string cache_duration_text(const std::string& key, const std::string& name, const Psc::Value_Statistics& statistics) {
return name + " current:" + number_text(key + "_instant", statistics.instant) + "ms smooth:" + number_text(key + "_smooth", statistics.smooth) + "ms P50:" + number_text(key + "_p50", statistics.p50) + "ms P95:" + number_text(key + "_p95", statistics.p95) + "ms P99:" + number_text(key + "_p99", statistics.p99) + "ms";
}
void append_cache_node_lines(const Renderable_Cache_Aggregate_Stat& stat) {
std::string key = cache_key(stat);
std::string indent = indent_str(stat.tree_depth);
std::string name = stat.object_name.empty() ? "unnamed" : stat.object_name;
lines.push_back("cache tree " + indent + name + " hit:" + count_text(key + "_cache_hit", stat.hit_count) + " stale:" + count_text(key + "_cache_miss", stat.miss_count) + " miss:" + count_text(key + "_cache_rebuild", stat.rebuild_count) + " ratio:" + cache_ratio_text(key + "_cache_ratio", stat.hit_count, stat.miss_count));
lines.push_back("cache image " + indent + name + " area:" + count_text(key + "_cache_image_area", stat.last_image_area) + " bytes:" + count_text(key + "_cache_image_bytes", stat.last_image_bytes) + " version:" + count_text(key + "_cache_edit_version", stat.last_edit_version) + "/" + count_text(key + "_cache_ready_version", stat.last_ready_version));
lines.push_back("cache rebuild " + indent + name + " " + cache_duration_text(key + "_rebuild", "rebuild", stat.rebuild_ms));
lines.push_back("cache compose " + indent + name + " " + cache_duration_text(key + "_compose", "self compose", stat.compose_ms));
lines.push_back("cache subtree " + indent + name + " " + cache_duration_text(key + "_subtree_compose", "subtree compose", stat.subtree_compose_ms));
}
void append_renderable_node_lines(const Renderable_Aggregate_Stat& stat) {
std::string key = renderable_key(stat);
std::string indent = indent_str(stat.tree_depth);
std::string name = (stat.object_name.empty() ? "unnamed" : stat.object_name);
std::uint64_t latest_render_time = frame_render_time_ns(aggregate.latest);
std::string name_indent = indent + std::string(name.size(), ' ');
lines.push_back(indent + " " + name + " cache:" + cache_mode_text(stat.cache_enabled) + " render:" + count_text(key + "_render_count", stat.render_count) + " draws:" + count_text(key + "_self_draw_count", stat.self_draw_count) + " cur%:" + ns_ratio_text(key + "_current_total_ratio", stat.last_total_render_time_ns, latest_render_time) + " total%:" + ns_ratio_text(key + "_aggregate_total_ratio", stat.total_render_time_ns, aggregate.render_time_ns));
lines.push_back(name_indent + " total " + duration_line(key + "_total_render", "time", stat.total_render_ms));
lines.push_back(name_indent + " total% " + percent_stat_line(key + "_total_ratio", "ratio", stat.total_render_ratio));
lines.push_back(name_indent + " self " + duration_line(key + "_self_draw", "time", stat.self_draw_ms));
lines.push_back(name_indent + " self% " + percent_stat_line(key + "_self_ratio", "ratio", stat.self_draw_ratio));
}
void append_renderable_tree_lines(std::uint64_t parent_node_id, int tree_depth, std::vector<const Renderable_Aggregate_Stat*>& emitted) {
std::vector<const Renderable_Aggregate_Stat*> children;
for (const auto& stat : aggregate.renderable_stats) {
if (stat.parent_node_id == parent_node_id && stat.tree_depth == tree_depth && should_show_renderable(stat))
children.push_back(&stat);
}
std::stable_sort(children.begin(), children.end(), [](const auto* a, const auto* b) {
return a->object_name < b->object_name;
});
for (const auto* stat : children) {
if (std::find(emitted.begin(), emitted.end(), stat) != emitted.end())
continue;
emitted.push_back(stat);
append_renderable_node_lines(*stat);
append_renderable_tree_lines(stat->node_id, stat->tree_depth + 1, emitted);
}
}
void append_renderable_tree_lines() {
std::vector<const Renderable_Aggregate_Stat*> emitted;
append_renderable_tree_lines(0, 0, emitted);
for (const auto& stat : aggregate.renderable_stats) {
if (!should_show_renderable(stat) || std::find(emitted.begin(), emitted.end(), &stat) != emitted.end())
continue;
emitted.push_back(&stat);
append_renderable_node_lines(stat);
}
}
void append_cache_tree_lines(std::uint64_t parent_node_id, int tree_depth, std::vector<const Renderable_Cache_Aggregate_Stat*>& emitted) {
std::vector<const Renderable_Cache_Aggregate_Stat*> children;
for (const auto& stat : aggregate.renderable_cache_stats) {
if (stat.parent_node_id == parent_node_id && stat.tree_depth == tree_depth && should_show_cache(stat))
children.push_back(&stat);
}
std::stable_sort(children.begin(), children.end(), [](const auto* a, const auto* b) {
return a->object_name < b->object_name;
});
for (const auto* stat : children) {
if (std::find(emitted.begin(), emitted.end(), stat) != emitted.end())
continue;
emitted.push_back(stat);
append_cache_node_lines(*stat);
append_cache_tree_lines(stat->node_id, stat->tree_depth + 1, emitted);
}
}
void append_cache_tree_lines() {
std::vector<const Renderable_Cache_Aggregate_Stat*> emitted;
append_cache_tree_lines(0, 0, emitted);
for (const auto& stat : aggregate.renderable_cache_stats) {
if (!should_show_cache(stat) || std::find(emitted.begin(), emitted.end(), &stat) != emitted.end())
continue;
emitted.push_back(&stat);
append_cache_node_lines(stat);
}
}
void rebuild_lines() {
if (!aggregate.has_frame) {
lines = {"waiting for frame stats"};
return;
}
const Frame_Lifecycle_Record& frame = aggregate.latest;
const Refresh_Control_Snapshot& refresh = frame.refresh;
const Frame_Worker_Stats& worker = frame.worker;
const Render_Executor_Snapshot& executor = worker.executor_at_finish;
std::uint64_t aggregate_cache_total = aggregate.cache_hit_count + aggregate.cache_miss_count;
std::uint64_t latest_render_time = frame_render_time_ns(frame);
double cache_hit_ratio = aggregate_cache_total ? static_cast<double>(aggregate.cache_hit_count) * 100.0 / static_cast<double>(aggregate_cache_total) : 0.0;
lines.clear();
lines.push_back("[common] frame:" + count_text("frame_id", frame.frame_id) + " input_ver:" + count_text("input_version", frame.input_version) + " outcome:" + outcome_text(frame.outcome) + " dirty:" + dirty_reason_text(frame.dirty_reason_mask));
lines.push_back("[low_latency] mode:" + production_mode_text(refresh.production_mode) + " limit_reason:" + limit_reason_text(refresh.limit_reason) + " consumer_confidence:" + consumer_confidence_text(refresh.consumer_confidence) + " effective:" + period_text("effective_interval", refresh.effective_min_render_interval_ns) + " user:" + period_text("user_period", refresh.user_period_ns));
lines.push_back("[low_latency] consumer_samples accepted:" + count_text("consumer_accepted", refresh.consumer_sample_accepted_count) + " rejected:" + count_text("consumer_rejected", refresh.consumer_sample_total_count - refresh.consumer_sample_accepted_count) + " ratio:" + number_text("consumer_ratio", refresh.consumer_sample_accepted_ratio * 100.0) + "% last:" + consumer_sample_result_text(refresh.last_consumer_sample_result));
lines.push_back("[low_latency] timing producer_interval:" + period_text("producer_interval", refresh.producer_interval_ns) + " render_service:" + period_text("render_period", refresh.render_period_ns) + " request_to_post:" + period_text("request_to_update_post", refresh.request_to_update_post_srtt_ns) + " post_to_paint:" + period_text("update_to_paint", refresh.update_post_to_paint_srtt_ns) + " paint_service:" + period_text("paint_period", refresh.paint_service_ns) + " raw_display:" + period_text("raw_display", refresh.raw_display_interval_srtt_ns) + " accepted_consumer:" + period_text("accepted_consumer", refresh.accepted_consumer_interval_srtt_ns));
lines.push_back("[low_latency] consumer_window size:" + count_text("consumer_window_size", refresh.consumer_window_size) + " accepted:" + count_text("consumer_window_accepted", refresh.consumer_window_accepted) + " rejected:" + count_text("consumer_window_rejected", refresh.consumer_window_size - refresh.consumer_window_accepted) + " ratio:" + number_text("consumer_window_ratio", refresh.consumer_window_accepted_ratio * 100.0) + "%");
lines.push_back("[low_latency] pacing admission_wait:" + ns_text("admission_wait", refresh.admission_wait_elapsed_ns) + "ms render_slot_idle:" + ns_text("render_slot_idle_wait", frame.render_slot_idle_wait_ns) + "ms");
lines.push_back("[pipeline] queue executor_wait:" + ns_text("executor_queue_wait", frame.executor_queue_wait_ns) + "ms present_wait:" + ns_text("present_wait", frame.present_queue_wait_ns) + "ms paint_return:" + ns_text("paint_return_wait", frame.paint_return_wait_ns) + "ms");
lines.push_back("[low_latency] update posted:" + count_text("update_posted_count", refresh.update_posted_count) + " duplicate_suppressed:" + count_text("duplicate_present_request_suppressed_count", refresh.duplicate_present_request_suppressed_count) + " ack:" + count_text("update_ack_count", refresh.update_ack_count) + " repost:" + count_text("update_repost_count", refresh.update_repost_count) + " paint_without_posted:" + count_text("paint_without_posted", refresh.paint_without_posted_update_count));
lines.push_back("[logger] dropped:" + count_text("logger_dropped", performance_log_service().dropped_record_count()));
lines.push_back("[low_latency] buffers rendered:" + count_text("presented_feedback", refresh.presented_frame_count) + " middle_published:" + count_text("middle_published", refresh.middle_published_count) + " middle_superseded:" + count_text("middle_superseded", refresh.middle_superseded_count) + " painting_advanced:" + count_text("painting_advanced", refresh.painting_advanced_count) + " painting_repeated:" + count_text("repeated_feedback", refresh.repeated_frame_count) + " underrun:" + count_text("underrun_feedback", refresh.underrun_count));
lines.push_back("[low_latency] decision:" + feedback_decision_text(refresh.last_decision) + " reason:" + refresh.decision_reason + " at:" + count_text("decision_time", refresh.decision_timestamp_ns) + " producer_starvation:" + count_text("producer_starvation", refresh.producer_starvation_count) + " starvation_active:" + (refresh.producer_starvation_active ? std::string("1") : std::string("0")) + " enter:" + count_text("mode_enter", refresh.mode_enter_count) + " exit:" + count_text("mode_exit", refresh.mode_exit_count) + " efficiency:" + number_text("display_efficiency", refresh.display_efficiency * 100.0) + "%");
lines.push_back("[executor/global] workers:" + count_text("taskflow_workers", static_cast<std::uint64_t>(executor.worker_count)) + " active_workers:" + count_text("taskflow_active", static_cast<std::uint64_t>(executor.active_workers)) + " queued_frame_jobs:" + count_text("taskflow_queued_frames", static_cast<std::uint64_t>(executor.queued_frame_jobs)) + " active_frame_jobs:" + count_text("taskflow_active_frames", static_cast<std::uint64_t>(executor.active_frame_jobs)));
lines.push_back("[executor/global] topologies submitted:" + count_text("taskflow_submitted", executor.submitted_topologies) + " started:" + count_text("taskflow_topologies_started", executor.started_topologies) + " completed:" + count_text("taskflow_topologies_completed", executor.completed_topologies) + " rejected_frame_jobs:" + count_text("taskflow_rejected", executor.rejected_frame_jobs));
lines.push_back("[executor/global] task_nodes started:" + count_text("taskflow_started", executor.started_task_nodes) + " completed:" + count_text("taskflow_completed", executor.completed_task_nodes));
lines.push_back("[executor/global] topology_queue_wait avg:" + ns_text("taskflow_queue_wait_avg", executor.average_topology_queue_wait_ns) + "ms max:" + ns_text("taskflow_queue_wait_max", executor.max_topology_queue_wait_ns) + "ms task_node_run avg:" + ns_text("taskflow_run_avg", executor.average_task_node_duration_ns) + "ms max:" + ns_text("taskflow_run_max", executor.max_task_node_duration_ns) + "ms");
lines.push_back("[executor/frame] business_tasks:" + count_text("frame_worker_tasks", worker.business_task_count) + " peak_parallel:" + count_text("frame_worker_peak", worker.business_task_peak_parallelism) + " topology_queue_wait:" + ns_text("frame_worker_wait_max", worker.topology_queue_wait_max_ns) + "ms business_run_total:" + ns_text("frame_worker_run_total", worker.business_task_run_total_ns) + "ms");
lines.push_back(duration_line("request_wait", "lifecycle render_request_to_start", aggregate.request_wait_ms));
lines.push_back(duration_line("producer_interval", "timing producer_interval", aggregate.producer_interval_ms));
lines.push_back(duration_line("admission_wait", "timing admission_wait", aggregate.admission_wait_ms));
lines.push_back(duration_line("render_slot_idle_wait", "timing render_slot_idle_wait", aggregate.render_slot_idle_wait_ms));
lines.push_back(duration_line("executor_queue_wait", "timing executor_queue_wait", aggregate.executor_queue_wait_ms));
lines.push_back(duration_line("prepare", "lifecycle prepare", aggregate.prepare_ms));
lines.push_back(duration_line("draw", "lifecycle draw", aggregate.draw_ms));
lines.push_back(duration_line("render", "lifecycle render_total", aggregate.render_ms));
lines.push_back(duration_line("ready_wait", "lifecycle ready_to_paint", aggregate.ready_wait_ms));
lines.push_back(duration_line("update_wait", "timing update_to_paint", aggregate.update_wait_ms));
lines.push_back(duration_line("paint", "timing paint_service", aggregate.paint_ms));
lines.push_back(duration_line("display_interval", "timing display_interval", aggregate.display_interval_ms));
lines.push_back(duration_line("effective_interval", "timing effective_interval", aggregate.effective_interval_ms));
lines.push_back(duration_line("frame_age", "lifecycle input_to_display_age", aggregate.frame_age_ms));
lines.push_back(duration_line("frame_total", "lifecycle e2e_total", aggregate.frame_total_ms));
lines.push_back("cache hit:" + count_text("cache_hit_count", aggregate.cache_hit_count) + " miss:" + count_text("cache_miss_count", aggregate.cache_miss_count) + " ratio:" + number_text("cache_hit_ratio", cache_hit_ratio));
lines.push_back("cache rebuild count:" + count_text("cache_rebuild_count", aggregate.cache_rebuild_count) + " time:" + ns_text("cache_rebuild_time", aggregate.cache_rebuild_time_ns) + "ms compose:" + ns_text("cache_compose_time", aggregate.cache_compose_time_ns) + "ms");
lines.push_back(duration_line("cache_rebuild_slide", "cache total rebuild", aggregate.cache_rebuild_ms));
lines.push_back(duration_line("cache_compose_slide", "cache total compose", aggregate.cache_compose_ms));
lines.push_back(percent_stat_line("cache_rebuild_ratio_slide", "cache rebuild ratio", aggregate.cache_rebuild_ratio));
lines.push_back(percent_stat_line("cache_compose_ratio_slide", "cache compose ratio", aggregate.cache_compose_ratio));
lines.push_back("cache cur rebuild:" + ns_ratio_text("cache_rebuild_current_ratio", frame.renderable_cache_rebuild_time_ns, latest_render_time) + " compose:" + ns_ratio_text("cache_compose_current_ratio", frame.renderable_cache_compose_time_ns, latest_render_time) + " total rebuild:" + ns_ratio_text("cache_rebuild_aggregate_ratio", aggregate.cache_rebuild_time_ns, aggregate.render_time_ns) + " compose:" + ns_ratio_text("cache_compose_aggregate_ratio", aggregate.cache_compose_time_ns, aggregate.render_time_ns));
append_renderable_tree_lines();
append_cache_tree_lines();
lines.push_back("work raw:" + count_text("raw_input_count", frame.raw_input_count) + " prepared:" + count_text("prepared_output_count", frame.prepared_output_count));
lines.push_back("observed:" + count_text("observed_frame_count", aggregate.observed_frame_count) + " consumed:" + count_text("consumed_outcome_count", aggregate.consumed_outcome_count) + " superseded:" + count_text("superseded_outcome_count", aggregate.superseded_outcome_count) + " cancelled:" + count_text("cancelled_outcome_count", aggregate.cancelled_outcome_count) + " dropped:" + count_text("input_capacity_dropped", aggregate.input_capacity_dropped));
}
std::vector<std::string> stripped_lines() const {
return lines;
}
std::vector<std::string> display_lines() const {
std::vector<std::string> result;
result.reserve(render_lines.size());
for (const auto& line : render_lines)
result.push_back(render_line_text(line));
return result;
}
static bool warning_line(std::string_view line) {
return (contains(line, "underrun:") && !contains(line, "underrun:0"))
|| (contains(line, "dropped:") && !contains(line, "dropped:0"))
|| (contains(line, "starvation_active:1"))
|| (contains(line, "confidence:low") && contains(line, "consumer_paced"));
}
static bool whitespace(char value) {
return std::isspace(static_cast<unsigned char>(value)) != 0;
}
static std::size_t metric_unit_offset(std::string_view value) {
std::size_t index = 0;
if (index < value.size() && (value[index] == '+' || value[index] == '-'))
++index;
bool has_digit = false;
while (index < value.size()) {
char current = value[index];
if (std::isdigit(static_cast<unsigned char>(current))) {
has_digit = true;
++index;
continue;
}
if (current == '.' || current == ',') {
++index;
continue;
}
break;
}
if (!has_digit || index == value.size())
return std::string_view::npos;
char unit = value[index];
if (unit == '%' || std::isalpha(static_cast<unsigned char>(unit)))
return index;
return std::string_view::npos;
}
static std::size_t first_metric_item(std::string_view text, std::size_t begin) {
std::size_t cursor = begin;
while (cursor < text.size()) {
while (cursor < text.size() && whitespace(text[cursor]))
++cursor;
std::size_t token_end = cursor;
while (token_end < text.size() && !whitespace(text[token_end]))
++token_end;
if (text.substr(cursor, token_end - cursor).find(':') != std::string_view::npos)
return cursor;
cursor = token_end;
}
return std::string_view::npos;
}
std::vector<Performance_Text_Span> plain_line_runs(const std::string& text, Canvas& canvas) {
std::vector<Performance_Text_Span> runs;
std::unordered_map<std::string, std::size_t> field_occurrences;
double offset = 0.0;
bool warning = warning_line(text);
double space_width = canvas.measure_text("0 0").width - canvas.measure_text("00").width;
if (space_width <= 0.0)
space_width = std::max(1.0, canvas.measure_text("0").width * 0.5);
auto trim = [](std::string_view value) {
while (!value.empty() && whitespace(value.front()))
value.remove_prefix(1);
while (!value.empty() && whitespace(value.back()))
value.remove_suffix(1);
return std::string(value);
};
auto append = [&](std::string value, Performance_Text_Role role, bool is_value = false, std::string field_key = {}) {
if (value.empty())
return;
Performance_Text_Span run;
run.text = std::move(value);
run.field_key = std::move(field_key);
run.role = role;
run.value = is_value;
run.column_index = 0;
run.x_offset = offset;
measure_run(canvas, run);
if (std::all_of(run.text.begin(), run.text.end(), whitespace)) {
run.measured_width = space_width * static_cast<double>(run.text.size());
run.reserved_width = run.measured_width;
}
offset += run_width(run);
runs.push_back(std::move(run));
};
auto make_field_key = [&](std::string_view section, std::string_view context, std::string_view label) {
std::string key;
key.reserve(section.size() + context.size() + label.size() + 3);
key.append(section);
key.push_back('|');
key.append(context);
key.push_back('|');
key.append(label);
std::size_t occurrence = field_occurrences[key]++;
key.push_back('|');
key += std::to_string(occurrence);
return key;
};
auto append_value = [&](std::string_view value, const std::string& field_key) {
Performance_Text_Role value_role = warning ? Performance_Text_Role::Warning : Performance_Text_Role::Value;
std::size_t unit_offset = metric_unit_offset(value);
if (unit_offset == std::string_view::npos) {
append(std::string(value), value_role, true, field_key);
return;
}
append(std::string(value.substr(0, unit_offset)), value_role, true, field_key);
append(std::string(value.substr(unit_offset)), warning ? Performance_Text_Role::Warning : Performance_Text_Role::Unit);
};
std::size_t cursor = 0;
std::string section;
std::size_t section_end = text.find(']');
if (!text.empty() && text.front() == '[' && section_end != std::string::npos) {
section = text.substr(0, section_end + 1);
append(section, Performance_Text_Role::Section);
cursor = section_end + 1;
}
std::size_t item_begin = first_metric_item(text, cursor);
if (item_begin == std::string_view::npos) {
append(text.substr(cursor), warning ? Performance_Text_Role::Warning : Performance_Text_Role::Value);
return runs;
}
std::string context = trim(std::string_view(text).substr(cursor, item_begin - cursor));
if (item_begin > cursor)
append(text.substr(cursor, item_begin - cursor), Performance_Text_Role::Label);
cursor = item_begin;
while (cursor < text.size()) {
std::size_t whitespace_end = cursor;
while (whitespace_end < text.size() && whitespace(text[whitespace_end]))
++whitespace_end;
if (whitespace_end > cursor)
append(text.substr(cursor, whitespace_end - cursor), Performance_Text_Role::Muted);
cursor = whitespace_end;
if (cursor >= text.size())
break;
std::size_t token_end = cursor;
while (token_end < text.size() && !whitespace(text[token_end]))
++token_end;
std::size_t colon = text.find(':', cursor);
if (colon == std::string::npos || colon >= token_end) {
std::string token = text.substr(cursor, token_end - cursor);
append(token, warning ? Performance_Text_Role::Warning : Performance_Text_Role::Label);
std::size_t next = token_end;
while (next < text.size() && whitespace(text[next]))
++next;
std::size_t next_end = next;
while (next_end < text.size() && !whitespace(text[next_end]))
++next_end;
std::size_t next_colon = text.find(':', next);
if (next < text.size() && next_colon != std::string::npos && next_colon < next_end)
context = token;
cursor = token_end;
continue;
}
std::string_view label = std::string_view(text).substr(cursor, colon - cursor);
std::string field_key = make_field_key(section, context, label);
append(text.substr(cursor, colon - cursor + 1), Performance_Text_Role::Label);
append_value(std::string_view(text).substr(colon + 1, token_end - colon - 1), field_key);
cursor = token_end;
}
return runs;
}
void build_render_lines(Canvas& canvas) {
render_lines.clear();
text_runs.clear();
for (const auto& text : lines) {
Performance_Line_Layout line;
std::vector<Performance_Text_Span> runs = plain_line_runs(text, canvas);
for (auto& run : runs) {
line.width = std::max(line.width, run.x_offset + run_width(run));
line.runs.push_back(std::move(run));
}
if (!line.runs.empty())
render_lines.push_back(std::move(line));
}
if (render_lines.empty()) {
Performance_Line_Layout line;
Performance_Text_Span run;
run.text = "waiting for frame stats";
measure_run(canvas, run);
append_run(line, run);
render_lines.push_back(std::move(line));
}
}
void update_content_width(Performance_Overlay_Render_State* state, double& held_width, int viewport_width) {
double max_width = 0.0;
for (const auto& line : render_lines)
max_width = std::max(max_width, line.width);
max_width = std::max(max_width, copy_button.preferred_width() + button_gap + close_button.preferred_width());
double measured_width = max_width
+ static_cast<double>(state->left_margin + state->right_margin + state->scroll_bar_width + state->scroll_bar_margin * 2);
held_width = std::max(held_width, measured_width);
content_width = std::min(held_width, static_cast<double>(std::max(0, viewport_width)));
if (content_width <= 0.0)
content_width = std::min(measured_width, static_cast<double>(std::max(0, viewport_width)));
}
static Color role_color(const Performance_Overlay_Render_State& state, Performance_Text_Role role) {
switch (role) {
case Performance_Text_Role::Section:
return state.section_color;
case Performance_Text_Role::Label:
return state.label_color;
case Performance_Text_Role::Value:
return state.value_color;
case Performance_Text_Role::Unit:
return state.unit_color;
case Performance_Text_Role::Warning:
return state.warning_color;
case Performance_Text_Role::Muted:
return state.muted_color;
}
return state.value_color;
}
void draw_render_line(Canvas& canvas, const Performance_Line_Layout& line, double x, double y, double line_height, const Performance_Overlay_Render_State& state) {
for (const auto& run : line.runs) {
if (run.text.empty())
continue;
canvas.set_pen(Pen{role_color(state, run.role)});
canvas.draw_text(PointF{x + run.x_offset, y}, run.text);
text_runs.push_back(Performance_Display_Text_Run{
run.text,
run.role,
RectF{x + run.x_offset, y, run.measured_width, line_height},
run.column_index
});
}
}
static double run_width(const Performance_Text_Span& run) {
return run.value ? run.reserved_width : run.measured_width;
}
static void append_run(Performance_Line_Layout& line, const Performance_Text_Span& run) {
line.runs.push_back(run);
line.width += run_width(run);
}
void append_wrapped_runs(std::vector<Performance_Text_Span> runs, Canvas& canvas, double available_width) {
Performance_Line_Layout current;
for (auto& original_run : runs) {
Performance_Text_Span run = std::move(original_run);
while (!run.text.empty()) {
double remaining = available_width - current.width;
double width = run_width(run);
if (current.runs.empty() || width <= remaining) {
if (run.value || width <= available_width) {
append_run(current, run);
break;
}
Performance_Text_Span fragment = split_text_run(canvas, run, available_width);
if (fragment.text.empty()) {
append_run(current, run);
break;
}
append_run(current, fragment);
push_non_empty_line(current);
continue;
}
push_non_empty_line(current);
}
}
push_non_empty_line(current);
}
void push_non_empty_line(Performance_Line_Layout& line) {
if (line.runs.empty())
return;
render_lines.push_back(std::move(line));
line = {};
}
Performance_Text_Span split_text_run(Canvas& canvas, Performance_Text_Span& run, double available_width) {
Performance_Text_Span fragment;
if (run.value || run.text.empty())
return fragment;
std::size_t split = split_utf8_text_to_width(canvas, run.text, available_width);
if (!split || split >= run.text.size())
return fragment;
fragment.text = run.text.substr(0, split);
measure_run(canvas, fragment);
std::size_t rest = split;
while (rest < run.text.size() && run.text[rest] == ' ')
++rest;
run.text = run.text.substr(rest);
measure_run(canvas, run);
return fragment;
}
static std::size_t next_utf8_boundary(std::string_view text, std::size_t pos) {
if (pos >= text.size())
return text.size();
unsigned char c = static_cast<unsigned char>(text[pos]);
std::size_t advance = 1;
if ((c & 0x80u) == 0)
advance = 1;
else if ((c & 0xE0u) == 0xC0u)
advance = 2;
else if ((c & 0xF0u) == 0xE0u)
advance = 3;
else if ((c & 0xF8u) == 0xF0u)
advance = 4;
return std::min(text.size(), pos + advance);
}
static std::size_t split_utf8_text_to_width(Canvas& canvas, const std::string& text, double max_width) {
std::size_t best = 0;
std::size_t best_space = 0;
for (std::size_t pos = 0; pos < text.size();) {
std::size_t next = next_utf8_boundary(text, pos);
std::string candidate = text.substr(0, next);
if (canvas.measure_text(candidate).width > max_width)
break;
best = next;
if (text[pos] == ' ')
best_space = next;
pos = next;
}
if (best_space > 0)
return best_space;
if (best > 0)
return best;
return next_utf8_boundary(text, 0);
}
std::vector<Performance_Text_Span> legacy_plain_runs(const std::string& line, Canvas& canvas) {
std::vector<Performance_Text_Span> runs;
Performance_Text_Span run;
run.text = line;
measure_run(canvas, run);
runs.push_back(std::move(run));
return runs;
}
void measure_run(Canvas& canvas, Performance_Text_Span& run) {
run.measured_width = run.text.empty() ? 0.0 : canvas.measure_text(run.text).width;
if (!run.value) {
run.reserved_width = run.measured_width;
return;
}
if (!run.field_key.empty()) {
double& max_width = field_max_value_widths[run.field_key];
max_width = std::max(max_width, run.measured_width);
run.reserved_width = max_width;
return;
}
auto index = static_cast<std::size_t>(run.field_id);
if (index >= field_layout_states.size())
index = static_cast<std::size_t>(Performance_Metric_Id::Generic_Number);
field_layout_states[index].max_value_width = std::max(field_layout_states[index].max_value_width, run.measured_width);
run.reserved_width = field_layout_states[index].max_value_width;
}
static std::string render_line_text(const Performance_Line_Layout& line) {
std::string text;
for (const auto& run : line.runs)
text += run.text;
return text;
}
static std::string plain_text(const std::string& line) {
return line;
}
};
struct Performance_Overlay_Private {
std::atomic_bool enabled_value{false};
std::shared_ptr<Performance_Worker_State> metrics_worker;
mutable std::mutex identity_mutex;
Performance_Log_Plot_Identity log_identity;
mutable std::mutex clipboard_callback_mutex;
std::function<void(std::string)> clipboard_callback;
Button_Interaction copy_button_interaction;
Button_Interaction close_button_interaction;
Performance_Overlay_Private()
: metrics_worker(std::make_shared<Performance_Worker_State>()) {}
~Performance_Overlay_Private() {
if (!metrics_worker)
return;
metrics_worker->cancelled.store(true, std::memory_order_release);
Performance_Frame_Record frame;
while (metrics_worker->metric_queue.try_pop(frame)) {}
}
void enqueue_frame_metric(std::shared_ptr<const Frame_Lifecycle_Record> frame) {
if (!frame)
return;
if (!metrics_worker->metric_queue.try_push(std::move(frame))) {
metrics_worker->dropped_frame_record_count.fetch_add(1, std::memory_order_relaxed);
return;
}
schedule_performance_metrics_worker(metrics_worker);
}
void set_identity(Performance_Log_Plot_Identity identity) {
std::lock_guard<std::mutex> lock(identity_mutex);
log_identity = std::move(identity);
}
Performance_Log_Plot_Identity identity() const {
std::lock_guard<std::mutex> lock(identity_mutex);
return log_identity;
}
void set_clipboard_callback(std::function<void(std::string)> callback) {
std::lock_guard<std::mutex> lock(clipboard_callback_mutex);
clipboard_callback = std::move(callback);
}
void copy_to_clipboard(std::string text) const {
std::function<void(std::string)> callback;
{
std::lock_guard<std::mutex> lock(clipboard_callback_mutex);
callback = clipboard_callback;
}
if (callback)
callback(std::move(text));
}
void publish_button_states(bool changed) {
if (!changed)
return;
metrics_worker->copy_button_state.store(copy_button_interaction.state(), std::memory_order_release);
metrics_worker->close_button_state.store(close_button_interaction.state(), std::memory_order_release);
metrics_worker->snapshot_rebuild_requested.store(true, std::memory_order_release);
if (enabled_value.load(std::memory_order_acquire))
schedule_performance_metrics_worker(metrics_worker, true);
}
void reset_button_states() {
copy_button_interaction.reset();
close_button_interaction.reset();
metrics_worker->copy_button_state.store(Button_State::Normal, std::memory_order_release);
metrics_worker->close_button_state.store(Button_State::Normal, std::memory_order_release);
}
static const char* feedback_outcome_event(Frame_Outcome outcome) {
switch (outcome) {
case Frame_Outcome::Presented:
return "PaintingAdvanced";
case Frame_Outcome::Superseded:
return "MiddleSuperseded";
case Frame_Outcome::Render_Acquire_Dropped:
return "RenderAcquireDropped";
case Frame_Outcome::Publish_Dropped:
return "PublishDropped";
case Frame_Outcome::Paint_Acquire_Dropped:
return "PaintAcquireDropped";
case Frame_Outcome::Worker_Budget_Dropped:
return "WorkerBudgetDropped";
case Frame_Outcome::Cancelled:
return "Cancelled";
}
return "UnknownOutcome";
}
void enqueue_frame_log(std::shared_ptr<const Frame_Lifecycle_Record> frame) {
if (!frame)
return;
Performance_Log_Plot_Identity plot = identity();
performance_log_service().enqueue_frame(plot, frame);
}
void enqueue_feedback_log(std::shared_ptr<const Frame_Feedback_Event_Record> event) {
if (!event)
return;
performance_log_service().enqueue_feedback_event(identity(), *event);
}
static void run_metrics_worker(const std::shared_ptr<Performance_Worker_State>& state) {
if (!state)
return;
while (!state->cancelled.load(std::memory_order_acquire)) {
state->work_requested.store(false, std::memory_order_release);
bool changed = false;
if (state->reset_requested.exchange(false, std::memory_order_acq_rel)) {
state->aggregate.clear();
state->scroll_offset.store(0.0, std::memory_order_release);
changed = true;
}
if (state->snapshot_rebuild_requested.exchange(false, std::memory_order_acq_rel))
changed = true;
if (consume_scroll_commands(*state))
changed = true;
std::uint64_t dropped = state->dropped_frame_record_count.exchange(0, std::memory_order_acq_rel);
if (dropped) {
state->aggregate.input_capacity_dropped += dropped;
changed = true;
}
Performance_Frame_Record frame;
while (state->metric_queue.try_pop(frame)) {
if (frame)
state->aggregate.update(*frame);
changed = true;
}
if (changed && state->enabled.load(std::memory_order_acquire)) {
state->published_snapshot.store(build_display_snapshot(*state), std::memory_order_release);
if (state->repaint_requested.exchange(false, std::memory_order_acq_rel))
invoke_update_callback(*state);
}
if (state->work_requested.load(std::memory_order_acquire))
continue;
state->worker_active.store(false, std::memory_order_release);
if (!state->work_requested.load(std::memory_order_acquire) || state->worker_active.exchange(true, std::memory_order_acq_rel))
return;
}
state->worker_active.store(false, std::memory_order_release);
}
static std::shared_ptr<const Performance_Display_Snapshot> build_display_snapshot(Performance_Worker_State& worker_state) {
Performance_Layout_Builder layout;
layout.aggregate = worker_state.aggregate;
layout.field_layout_states = worker_state.field_layout_states;
layout.field_max_value_widths = worker_state.field_max_value_widths;
layout.rebuild_lines();
auto snapshot = std::make_shared<Performance_Display_Snapshot>();
snapshot->content_version = worker_state.next_content_version.fetch_add(1, std::memory_order_acq_rel);
Size viewport = current_viewport(worker_state);
snapshot->viewport_size = viewport;
snapshot->viewport_version = current_viewport_version(worker_state);
snapshot->viewport_rect = RectF{0.0, 0.0, static_cast<double>(viewport.width), static_cast<double>(viewport.height)};
if (viewport.empty() || layout.lines.empty()) {
snapshot->lines = layout.stripped_lines();
return snapshot;
}
Performance_Overlay_Render_State state;
{
std::lock_guard<std::mutex> lock(worker_state.options_mutex);
static_cast<Performance_Overlay_Options&>(state) = worker_state.options;
}
Image measure_image(1, 1);
measure_image.fill(Color::transparent());
Text_Metrics metrics;
{
Canvas measure_canvas(measure_image);
measure_canvas.set_font(state.font);
metrics = measure_canvas.measure_text("0123456789 ABC \xE4\xB8\xAD\xE6\x96\x87");
if (metrics.line_height() <= 0.0)
metrics = measure_canvas.measure_text("M");
Button_Style close_style = layout.close_button.style();
close_style.text = state.warning_color;
close_style.hover_text = state.warning_color;
close_style.pressed_text = state.warning_color;
layout.close_button.set_style(std::move(close_style));
layout.copy_button.set_state(worker_state.copy_button_state.load(std::memory_order_acquire));
layout.close_button.set_state(worker_state.close_button_state.load(std::memory_order_acquire));
layout.copy_button.measure(measure_canvas);
layout.close_button.measure(measure_canvas);
layout.toolbar_height = std::max(layout.copy_button.preferred_height(), layout.close_button.preferred_height());
layout.seed_static_field_widths(measure_canvas);
layout.build_render_lines(measure_canvas);
}
worker_state.field_layout_states = layout.field_layout_states;
worker_state.field_max_value_widths = layout.field_max_value_widths;
snapshot->lines = layout.display_lines();
layout.update_content_width(&state, worker_state.maximum_content_width, viewport.width);
double line_height = std::max(1.0, metrics.line_height());
double panel_width = std::min(layout.content_width, static_cast<double>(std::max(0, viewport.width)));
double height = layout.panel_height(line_height, &state, viewport.height);
if (panel_width <= 0.0 || height <= 0.0)
return snapshot;
double content_height = layout.content_height(line_height, &state);
double text_content_height = line_height * static_cast<double>(layout.render_lines.size());
double content_viewport_height = layout.content_viewport_height(&state, height);
double max_scroll = std::max(0.0, text_content_height - content_viewport_height);
double scroll = performance_clamp_scroll_offset(worker_state.scroll_offset.load(std::memory_order_acquire), max_scroll);
worker_state.scroll_offset.store(scroll, std::memory_order_release);
layout.panel_rect = RectF{0.0, 0.0, panel_width, height};
layout.update_toolbar_button_rects(&state);
Image image(static_cast<int>(std::ceil(panel_width)), static_cast<int>(std::ceil(height)));
image.fill(Color::transparent());
Canvas canvas(image);
canvas.save();
canvas.set_font(state.font);
canvas.set_brush(Brush{state.background, Brush_Style::Solid});
canvas.fill_rect(RectF{0.0, 0.0, panel_width, height});
canvas.set_pen(Pen{state.foreground});
RectF text_rect{
static_cast<double>(state.left_margin),
layout.text_top(&state),
std::max(0.0, panel_width - state.left_margin - state.right_margin - state.scroll_bar_width - state.scroll_bar_margin * 2),
std::max(0.0, height - layout.text_top(&state) - state.bottom_margin)
};
canvas.set_clip_rect(text_rect);
int first_line = std::max(0, static_cast<int>(std::floor(scroll / line_height)));
double y = layout.text_top(&state) - (scroll - static_cast<double>(first_line) * line_height);
layout.text_runs.clear();
for (int i = first_line; i < static_cast<int>(layout.render_lines.size()) && y < text_rect.bottom(); ++i, y += line_height) {
if (y + line_height < text_rect.y)
continue;
layout.draw_render_line(canvas, layout.render_lines[static_cast<std::size_t>(i)], text_rect.x, y, line_height, state);
}
canvas.reset_clip();
layout.copy_button.draw(canvas);
layout.close_button.draw(canvas);
layout.update_scroll_bar_rects(&state, max_scroll, scroll, content_viewport_height, text_content_height);
if (!layout.scroll_bar_rect.empty()) {
canvas.set_brush(Brush{state.scroll_track, Brush_Style::Solid});
canvas.fill_rect(layout.scroll_bar_rect);
canvas.set_brush(Brush{state.scroll_thumb, Brush_Style::Solid});
canvas.fill_rect(layout.scroll_thumb_rect);
}
canvas.restore();
snapshot->destination_rect = layout.panel_rect;
snapshot->scroll_track_rect = layout.scroll_bar_rect;
snapshot->scroll_thumb_rect = layout.scroll_thumb_rect;
snapshot->copy_button_rect = layout.copy_button.rect();
snapshot->close_button_rect = layout.close_button.rect();
snapshot->content_height = content_height;
snapshot->scroll_offset = scroll;
snapshot->max_scroll_offset = max_scroll;
snapshot->line_height = line_height;
snapshot->image = std::move(image);
snapshot->text_runs = layout.text_runs;
worker_state.last_scroll_track_rect = layout.scroll_bar_rect;
worker_state.last_scroll_thumb_rect = layout.scroll_thumb_rect;
worker_state.last_content_viewport_height = content_viewport_height;
worker_state.last_line_height = line_height;
worker_state.maximum_scroll_offset = max_scroll;
return snapshot;
}
private:
static Size current_viewport(const Performance_Worker_State& worker_state) {
int width = worker_state.viewport_width.load(std::memory_order_acquire);
int height = worker_state.viewport_height.load(std::memory_order_acquire);
if (width > 0 && height > 0)
return {width, height};
if (!worker_state.aggregate.has_frame)
return {};
const Frame_Lifecycle_Record& frame = worker_state.aggregate.latest;
return {
static_cast<int>(std::min<std::uint64_t>(frame.pixel_width, static_cast<std::uint64_t>(std::numeric_limits<int>::max()))),
static_cast<int>(std::min<std::uint64_t>(frame.pixel_height, static_cast<std::uint64_t>(std::numeric_limits<int>::max())))
};
}
static std::uint64_t current_viewport_version(const Performance_Worker_State& worker_state) {
std::uint64_t version = worker_state.viewport_version.load(std::memory_order_acquire);
if (version || !worker_state.aggregate.has_frame)
return version;
return worker_state.aggregate.latest.viewport_version;
}
static void invoke_update_callback(Performance_Worker_State& worker_state) {
std::function<void()> callback;
{
std::lock_guard<std::mutex> lock(worker_state.update_callback_mutex);
callback = worker_state.update_callback;
}
if (callback)
callback();
}
static bool consume_scroll_commands(Performance_Worker_State& worker_state) {
double offset = worker_state.scroll_offset.load(std::memory_order_acquire);
double previous = offset;
double line_height = std::max(1.0, worker_state.last_line_height);
double wheel_delta = performance_atomic_exchange(worker_state.pending_wheel_delta_px);
if (wheel_delta != 0.0)
offset += wheel_delta;
int page_delta = worker_state.pending_page_delta.exchange(0, std::memory_order_acq_rel);
if (page_delta)
offset += static_cast<double>(page_delta) * std::max(line_height, worker_state.last_content_viewport_height * 0.8);
bool dragging = worker_state.drag_active.load(std::memory_order_acquire);
bool drag_end = worker_state.drag_end_requested.exchange(false, std::memory_order_acq_rel);
if (dragging && worker_state.maximum_scroll_offset > 0.0) {
const RectF& track = worker_state.last_scroll_track_rect;
const RectF& thumb = worker_state.last_scroll_thumb_rect;
double travel = track.height - thumb.height;
if (track.height > 0.0 && thumb.height > 0.0 && travel > 0.0) {
double thumb_top = worker_state.drag_position_y.load(std::memory_order_acquire)
- worker_state.drag_anchor_y.load(std::memory_order_acquire);
double ratio = (thumb_top - track.y) / travel;
offset = std::clamp(ratio, 0.0, 1.0) * worker_state.maximum_scroll_offset;
}
}
if (drag_end)
worker_state.drag_active.store(false, std::memory_order_release);
offset = performance_clamp_scroll_offset(offset, worker_state.maximum_scroll_offset);
worker_state.scroll_offset.store(offset, std::memory_order_release);
return wheel_delta != 0.0 || page_delta != 0 || drag_end || std::abs(offset - previous) > 0.01;
}
};
} // namespace renderive