8#include <imgui/imgui.h>
9#include <imgui/imgui_internal.h>
15#include <dotnetpp/dotnetpp.h>
28constexpr float row_height = 20.0f;
29constexpr float lane_header_width = 120.0f;
31constexpr float frame_bar_height = 92.0f;
33constexpr float memory_hist_row_height = 72.0f;
34constexpr float memory_hist_top_pad = 10.0f;
35constexpr float megabyte_divisor = 1024.0f * 1024.0f;
36constexpr ImU32 cpu_heap_hist_color = IM_COL32(200, 140, 70, 220);
37constexpr ImU32 gpu_mem_hist_color = IM_COL32(70, 130, 210, 220);
38constexpr ImU32 process_rss_hist_color = IM_COL32(140, 200, 120, 220);
40enum class memory_histogram_metric : uint8_t
47auto memory_mb_from_snapshot(
const frame_snapshot* snap, memory_histogram_metric metric) ->
float
55 case memory_histogram_metric::managed_heap_mb:
56 return static_cast<float>(snap->cpu_heap_used_bytes) / megabyte_divisor;
57 case memory_histogram_metric::gpu_memory_mb:
58 return static_cast<float>(snap->gpu_memory_used_bytes) / megabyte_divisor;
59 case memory_histogram_metric::process_rss_mb:
60 return static_cast<float>(snap->process_resident_bytes) / megabyte_divisor;
65auto memory_hist_inner_height() ->
float
67 return memory_hist_row_height - memory_hist_top_pad;
71constexpr float histogram_plot_top_pad = 14.0f;
73constexpr float histogram_inner_height = frame_bar_height - histogram_plot_top_pad;
74constexpr float ruler_height = 22.0f;
75constexpr float min_visible_width_px = 1.0f;
77constexpr float target_60fps_ms = 16.667f;
79constexpr double min_view_duration_ns = 100'000.0;
80constexpr double max_view_duration_ns = 2'000'000'000.0;
83constexpr uint16_t max_timeline_lane_depth = 48;
84constexpr float thread_lane_spacing = 4.0f;
85constexpr float timeline_wheel_scroll_px = 40.0f;
87auto lane_height_for_depth(uint16_t max_depth) ->
float
89 const uint16_t
d = std::min(max_depth, max_timeline_lane_depth);
90 return (
static_cast<float>(d) + 1.0f) * row_height + 2.0f;
96auto max_nesting_depth_for_thread_in_view(
const std::vector<const frame_snapshot*>& frames,
97 uint16_t thread_index,
99 double view_end_ns) -> uint16_t
107 std::vector<span> spans;
110 for(
const frame_snapshot*
frame : frames)
116 for(
const auto& ts :
frame->threads)
118 if(ts.thread_index != thread_index)
122 for(
const auto& ev : ts.events)
124 if(ev.end_ns <= ev.start_ns)
128 const double es =
static_cast<double>(ev.start_ns);
129 const double ee =
static_cast<double>(ev.end_ns);
130 if(ee < view_start_ns || es > view_end_ns)
134 spans.push_back({ev.start_ns, ev.end_ns});
144 std::sort(spans.begin(), spans.end(), [](
const span&
a,
const span&
b) ->
bool
153 std::vector<int64_t> active_ends;
154 active_ends.reserve(spans.size());
157 for(
const span& sp : spans)
159 while(!active_ends.empty() && active_ends.back() <= sp.s)
161 active_ends.pop_back();
163 const auto d =
static_cast<uint16_t
>(active_ends.size());
168 active_ends.push_back(sp.e);
174auto color_from_hash(uint32_t hash) -> ImU32
176 float h =
static_cast<float>(hash % 360) / 360.0f;
177 float s = 0.5f +
static_cast<float>((hash >> 12) % 30) / 100.0f;
178 float v = 0.6f +
static_cast<float>((hash >> 20) % 30) / 100.0f;
181 ImGui::ColorConvertHSVtoRGB(h, s,
v, r, g,
b);
183 static_cast<int>(r * 255),
184 static_cast<int>(g * 255),
185 static_cast<int>(
b * 255),
189auto dim_color(ImU32 col) -> ImU32
191 int r =
static_cast<int>((col >> IM_COL32_R_SHIFT) & 0xFF) * 2 / 5;
192 int g =
static_cast<int>((col >> IM_COL32_G_SHIFT) & 0xFF) * 2 / 5;
193 int b =
static_cast<int>((col >> IM_COL32_B_SHIFT) & 0xFF) * 2 / 5;
194 return IM_COL32(r, g,
b, 140);
197auto format_time(
float ms) -> std::string
201 return fmt::format(
"{:.0f} ns", ms * 1'000'000.0f);
205 return fmt::format(
"{:.1f} us", ms * 1'000.0f);
207 return fmt::format(
"{:.2f} ms", ms);
210using lane_context = profiler_timeline_panel::lane_context;
212struct event_lane_geom
221auto compute_event_geom(
const lane_context& lc,
const profile_event& ev) -> event_lane_geom
224 const double ev_start =
static_cast<double>(ev.start_ns);
225 const double ev_end =
static_cast<double>(ev.end_ns);
227 if(ev_end < lc.view_start_ns || ev_start > lc.view_end_ns)
232 g.x0 = lc.canvas_pos.x +
static_cast<float>((ev_start - lc.view_start_ns) / lc.ns_per_pixel);
233 g.x1 = lc.canvas_pos.x +
static_cast<float>((ev_end - lc.view_start_ns) / lc.ns_per_pixel);
234 g.x0 = std::max(
g.x0, lc.canvas_pos.x);
235 g.x1 = std::min(
g.x1, lc.canvas_pos.x + lc.lane_content_width);
237 if((
g.x1 -
g.x0) < min_visible_width_px)
242 g.y0 = lc.canvas_pos.y +
static_cast<float>(ev.depth) * row_height;
243 g.y1 =
g.y0 + row_height - 1.0f;
248auto hist_index_of(performance_profiler* profiler,
const frame_snapshot* snap) -> uint32_t
250 if(!profiler || !snap)
254 const uint32_t
n = profiler->get_frame_count();
255 for(uint32_t i = 0;
i <
n; ++
i)
257 if(profiler->get_frame_snapshot(i) == snap)
265constexpr ImU32 wait_color_selected = IM_COL32(50, 50, 55, 200);
266constexpr ImU32 wait_color_dimmed = IM_COL32(30, 30, 35, 140);
268auto compute_cpu_ratio(
const profile_event& ev) ->
float
270 int64_t wall = ev.end_ns - ev.start_ns;
271 int64_t
cpu = ev.cpu_end_ns - ev.cpu_start_ns;
272 if(wall <= 0 || cpu <= 0)
276 return std::clamp(
static_cast<float>(cpu) /
static_cast<float>(wall), 0.0f, 1.0f);
279auto histogram_bar_color(
float ms) -> ImU32
283 return IM_COL32(200, 50, 50, 200);
287 return IM_COL32(200, 160, 50, 200);
289 return IM_COL32(60, 150, 60, 200);
293void draw_histogram_column(ImDrawList* draw_list,
303 if(ms <= 0.0f || scale_max <= 0.0f)
307 const float h_frac = std::clamp(ms / scale_max, 0.01f, 1.0f);
308 const float bar_h = histogram_inner_height * h_frac;
309 const float y_top = bottom_y - bar_h;
310 const ImU32 cpu_col = histogram_bar_color(ms);
313 constexpr ImU32 hist_wait_fill = IM_COL32(72, 72, 88, 255);
316 draw_list->AddRectFilled(ImVec2(
x0, y_top), ImVec2(
x1, bottom_y), hist_wait_fill);
317 const float cpu_h = bar_h * std::clamp(cpu_ratio, 0.0f, 1.0f);
318 if(cpu_h >= min_visible_width_px)
320 draw_list->AddRectFilled(ImVec2(
x0, bottom_y - cpu_h), ImVec2(
x1, bottom_y), cpu_col);
325 draw_list->AddRectFilled(ImVec2(
x0, y_top), ImVec2(
x1, bottom_y), cpu_col);
327 if(draw_outline && (
x1 -
x0) >= 2.0f)
329 draw_list->AddRect(ImVec2(
x0 + 0.5f, y_top + 0.5f), ImVec2(
x1 - 0.5f, bottom_y - 0.5f),
330 IM_COL32(140, 140, 160, 140), 0.0f, 0, 1.0f);
334void render_histogram_bars(ImDrawList* draw_list,
335 performance_profiler* profiler,
345 draw_list->PushClipRect(canvas_pos,
346 ImVec2(canvas_pos.x + bar_width, canvas_pos.y + frame_bar_height),
true);
350 constexpr float min_column_px = 1.0f;
351 const bool use_pixel_buckets = entry_w < min_column_px;
353 constexpr bool draw_cpu_split =
true;
354 const bool draw_outline = entry_w >= 4.0f;
356 if(use_pixel_buckets && entry_w > 0.0f)
358 const int32_t col_count = std::max(1,
static_cast<int32_t
>(std::floor(bar_width)));
359 for(int32_t col = 0; col < col_count; ++col)
361 const float i0f = hist_start +
static_cast<float>(col) / entry_w;
362 const float i1f = hist_start +
static_cast<float>(col + 1) / entry_w;
363 int32_t i0 = std::max(first_frame,
static_cast<int32_t
>(std::floor(i0f)));
364 int32_t i1 = std::min(last_frame,
static_cast<int32_t
>(std::ceil(i1f)) - 1);
369 float peak_ms = 0.0f;
370 float peak_cpu = 1.0f;
371 for(int32_t i = i0;
i <= i1; ++
i)
373 const auto* snap = profiler->get_frame_snapshot(
static_cast<uint32_t
>(i));
374 if(snap ==
nullptr || snap->frame_wall_ms <= 0.0f)
378 if(snap->frame_wall_ms > peak_ms)
380 peak_ms = snap->frame_wall_ms;
381 peak_cpu = snap->frame_cpu_ratio;
384 const float x0 = canvas_pos.x +
static_cast<float>(col);
385 const float x1 = canvas_pos.x +
static_cast<float>(col + 1);
386 draw_histogram_column(draw_list,
x0,
x1, bottom_y, peak_ms, peak_cpu, scale_max, draw_cpu_split,
false);
391 for(int32_t i = first_frame;
i <= last_frame; ++
i)
393 const auto* snap = profiler->get_frame_snapshot(
static_cast<uint32_t
>(i));
394 if(snap ==
nullptr || snap->frame_wall_ms <= 0.0f)
398 const float x0 = canvas_pos.x + (
static_cast<float>(
i) - hist_start) * entry_w;
399 const float x1 = canvas_pos.x + (
static_cast<float>(
i + 1) - hist_start) * entry_w;
400 draw_histogram_column(draw_list,
405 snap->frame_cpu_ratio,
412 draw_list->PopClipRect();
415void render_histogram_guides(ImDrawList* draw_list,
421 constexpr float target_30fps_ms = 33.333f;
423 float line_60_y = bottom_y - histogram_inner_height * (target_60fps_ms / scale_max);
424 draw_list->AddLine(ImVec2(canvas_pos.x, line_60_y),
425 ImVec2(canvas_pos.x + bar_width, line_60_y),
426 IM_COL32(0, 200, 0, 100));
427 draw_list->AddText(ImVec2(canvas_pos.x + 2.0f, line_60_y - 13.0f),
428 IM_COL32(0, 200, 0, 180),
"16ms (60 FPS)");
430 if(target_30fps_ms < scale_max)
432 float line_30_y = bottom_y - histogram_inner_height * (target_30fps_ms / scale_max);
433 draw_list->AddLine(ImVec2(canvas_pos.x, line_30_y),
434 ImVec2(canvas_pos.x + bar_width, line_30_y),
435 IM_COL32(200, 100, 0, 100));
436 draw_list->AddText(ImVec2(canvas_pos.x + 2.0f, line_30_y - 13.0f),
437 IM_COL32(200, 100, 0, 180),
"33ms (30 FPS)");
442void render_memory_histogram_guides(ImDrawList* draw_list,
447 if(scale_max_mb <= 0.001f)
451 const float inner_h = memory_hist_inner_height();
452 const float bottom_y = row_top_left.y + memory_hist_row_height;
453 constexpr ImU32 line_col = IM_COL32(130, 130, 155, 95);
454 constexpr ImU32 text_col = IM_COL32(200, 200, 220, 175);
456 auto bytes_for_mb_frac = [](
float mb,
float frac) -> uint64_t
458 const double b =
static_cast<double>(mb) *
static_cast<double>(megabyte_divisor) *
static_cast<double>(frac);
463 return static_cast<uint64_t
>(
b);
468 constexpr float frac = 0.5f;
469 const float y = bottom_y - inner_h * frac;
470 draw_list->AddLine(ImVec2(row_top_left.x,
y), ImVec2(row_top_left.x + bar_width,
y), line_col);
471 const auto pretty =
format_bytes(bytes_for_mb_frac(scale_max_mb, frac), 0);
472 draw_list->AddText(ImVec2(row_top_left.x + 2.0f,
y - 13.0f), text_col, pretty.c_str());
477 const float y = bottom_y - inner_h;
478 draw_list->AddLine(ImVec2(row_top_left.x,
y), ImVec2(row_top_left.x + bar_width,
y), line_col);
479 const auto pretty =
format_bytes(bytes_for_mb_frac(scale_max_mb, 1.0f), 0);
480 const std::string label = fmt::format(
"max {}", pretty);
481 const ImVec2 ts = ImGui::CalcTextSize(label.c_str());
482 draw_list->AddText(ImVec2(row_top_left.x + bar_width - ts.x - 4.0f,
y - 13.0f), text_col,
487void render_memory_mb_row(ImDrawList* draw_list,
488 performance_profiler* profiler,
496 memory_histogram_metric metric,
499 const float inner_h = memory_hist_inner_height();
500 const float bottom_y = row_top_left.y + memory_hist_row_height;
501 draw_list->PushClipRect(row_top_left,
502 ImVec2(row_top_left.x + bar_width, row_top_left.y + memory_hist_row_height),
505 constexpr float min_column_px = 1.0f;
506 const bool use_pixel_buckets = entry_w < min_column_px && entry_w > 0.0f;
507 const bool draw_outline = entry_w >= 4.0f;
509 auto draw_mem_col = [&](
float x0,
float x1,
float mb)
512 (scale_max_mb > 0.001f) ? std::clamp(mb / scale_max_mb, 0.02f, 1.0f) : 0.02f;
513 const float bar_h = inner_h * h_frac;
514 const float y_top = bottom_y - bar_h;
515 draw_list->AddRectFilled(ImVec2(
x0, y_top), ImVec2(
x1, bottom_y), fill_col);
516 if(draw_outline && (
x1 -
x0) >= 2.0f)
518 draw_list->AddRect(ImVec2(
x0 + 0.5f, y_top + 0.5f), ImVec2(
x1 - 0.5f, bottom_y - 0.5f),
519 IM_COL32(100, 100, 120, 100), 0.0f, 0, 1.0f);
523 if(use_pixel_buckets)
525 const int32_t col_count = std::max(1,
static_cast<int32_t
>(std::floor(bar_width)));
526 for(int32_t col = 0; col < col_count; ++col)
528 const float i0f = hist_start +
static_cast<float>(col) / entry_w;
529 const float i1f = hist_start +
static_cast<float>(col + 1) / entry_w;
530 int32_t i0 = std::max(first_frame,
static_cast<int32_t
>(std::floor(i0f)));
531 int32_t i1 = std::min(last_frame,
static_cast<int32_t
>(std::ceil(i1f)) - 1);
536 float peak_mb = 0.0f;
537 for(int32_t i = i0;
i <= i1; ++
i)
539 const auto* snap = profiler->get_frame_snapshot(
static_cast<uint32_t
>(i));
542 peak_mb = std::max(peak_mb, memory_mb_from_snapshot(snap, metric));
545 draw_mem_col(row_top_left.x +
static_cast<float>(col),
546 row_top_left.x +
static_cast<float>(col + 1),
552 for(int32_t i = first_frame;
i <= last_frame; ++
i)
554 const auto* snap = profiler->get_frame_snapshot(
static_cast<uint32_t
>(i));
559 const float x0 = row_top_left.x + (
static_cast<float>(
i) - hist_start) * entry_w;
560 const float x1 = row_top_left.x + (
static_cast<float>(
i + 1) - hist_start) * entry_w;
561 draw_mem_col(
x0,
x1, memory_mb_from_snapshot(snap, metric));
565 draw_list->PopClipRect();
568void render_live_sample_row(ImDrawList* draw_list,
571 const sample_data& samples,
574 bool is_frame_ms_row,
575 const sample_data* busy_samples =
nullptr)
578 if(n <= 0 || scale_max <= 0.0f)
582 const float entry_w = bar_width /
static_cast<float>(
n);
583 const float inner_h = is_frame_ms_row ? histogram_inner_height : memory_hist_inner_height();
584 const float bottom_y = row_top_left.y + (is_frame_ms_row ? frame_bar_height : memory_hist_row_height);
586 draw_list->PushClipRect(row_top_left,
587 ImVec2(row_top_left.x + bar_width,
588 row_top_left.y + (is_frame_ms_row ? frame_bar_height : memory_hist_row_height)),
591 const ImU32 row_bg = is_frame_ms_row ? IM_COL32(30, 30, 30, 255) : IM_COL32(26, 26, 28, 255);
592 draw_list->AddRectFilled(row_top_left, ImVec2(row_top_left.x + bar_width, bottom_y), row_bg);
594 const int offset = samples.get_offset();
595 const float* vals = samples.get_values();
596 const float* busy_vals = (busy_samples !=
nullptr) ? busy_samples->get_values() :
nullptr;
597 const int busy_offset = (busy_samples !=
nullptr) ? busy_samples->get_offset() : 0;
598 const bool draw_busy_split = is_frame_ms_row && busy_vals !=
nullptr;
599 const bool draw_outline = entry_w >= 4.0f;
600 const bool use_pixel_buckets = entry_w < 1.0f;
602 auto busy_ratio_at = [&](
int sample_col,
float wall_ms) ->
float
604 if(!draw_busy_split || wall_ms <= 0.001f)
608 const int bidx = (busy_offset + sample_col) % n;
609 return std::clamp(busy_vals[bidx] / wall_ms, 0.0f, 1.0f);
612 if(use_pixel_buckets)
614 const int col_count = std::max(1,
static_cast<int>(std::floor(bar_width)));
615 const float samples_per_col =
static_cast<float>(
n) /
static_cast<float>(col_count);
616 for(
int col = 0; col < col_count; ++col)
618 const int s0 =
static_cast<int>(std::floor(
static_cast<float>(col) * samples_per_col));
619 int s1 =
static_cast<int>(std::floor(
static_cast<float>(col + 1) * samples_per_col)) - 1;
620 s1 = std::max(s0, std::min(s1, n - 1));
622 float peak_ratio = 1.0f;
623 for(
int s = s0;
s <= s1; ++
s)
625 const int idx = (offset +
s) % n;
626 const float v = vals[idx];
630 peak_ratio = busy_ratio_at(s,
v);
633 const float x0 = row_top_left.x +
static_cast<float>(col);
634 const float x1 = row_top_left.x +
static_cast<float>(col + 1);
637 draw_histogram_column(draw_list,
x0,
x1, bottom_y, peak, peak_ratio, scale_max, draw_busy_split,
642 const float h_frac = std::clamp(peak / scale_max, 0.02f, 1.0f);
643 const float bar_h = inner_h * h_frac;
644 const float y_top = bottom_y - bar_h;
645 draw_list->AddRectFilled(ImVec2(
x0, y_top), ImVec2(
x1, bottom_y), fill_col);
651 for(
int col = 0; col <
n; ++col)
653 const int idx = (offset + col) % n;
654 const float v = vals[idx];
655 const float x0 = row_top_left.x +
static_cast<float>(col) * entry_w;
656 const float x1 = row_top_left.x +
static_cast<float>(col + 1) * entry_w;
659 draw_histogram_column(draw_list,
664 busy_ratio_at(col,
v),
671 const float h_frac = std::clamp(
v / scale_max, 0.02f, 1.0f);
672 const float bar_h = inner_h * h_frac;
673 const float y_top = bottom_y - bar_h;
674 draw_list->AddRectFilled(ImVec2(
x0, y_top), ImVec2(
x1, bottom_y), fill_col);
677 draw_list->AddRect(ImVec2(
x0 + 0.5f, y_top + 0.5f), ImVec2(
x1 - 0.5f, bottom_y - 0.5f),
678 IM_COL32(100, 100, 120, 80), 0.0f, 0, 1.0f);
684 draw_list->PopClipRect();
687void render_histogram_cursor(ImDrawList* draw_list,
691 int32_t selected_idx,
696 float cursor_x = canvas_x + (
static_cast<float>(selected_idx) + 0.5f - hist_start) * entry_w;
697 if(cursor_x < canvas_x - 10.0f || cursor_x > canvas_x + bar_width + 10.0f)
702 draw_list->AddLine(ImVec2(cursor_x, plot_top_y),
703 ImVec2(cursor_x, bottom_y),
704 IM_COL32(255, 255, 255, 220), 1.5f);
706 constexpr float tri_half = 5.0f;
707 constexpr float tri_h = 7.0f;
708 draw_list->AddTriangleFilled(
709 ImVec2(cursor_x - tri_half, plot_top_y),
710 ImVec2(cursor_x + tri_half, plot_top_y),
711 ImVec2(cursor_x, plot_top_y + tri_h),
712 IM_COL32(255, 255, 255, 230));
717void profiler_timeline_panel::timeline_render_event_block(
const lane_context& lc,
718 const profile_event& ev,
719 bool is_reference_frame,
720 const std::string& thread_name,
721 profiler_timeline_panel* panel,
722 uint32_t hist_frame_idx,
726 const event_lane_geom
g = compute_event_geom(lc, ev);
732 const float x0 =
g.x0;
733 const float y0 =
g.y0;
734 const float x1 =
g.x1;
735 const float y1 =
g.y1;
737 const bool is_scope_selected =
738 panel !=
nullptr && panel->is_timeline_scope_selected(hist_frame_idx, thread_idx, event_idx);
740 if(panel !=
nullptr && hist_frame_idx != UINT32_MAX &&
741 ImGui::IsMouseClicked(ImGuiMouseButton_Left) && ImGui::IsWindowHovered(ImGuiHoveredFlags_None))
743 const ImVec2
mouse = ImGui::GetMousePos();
746 panel->set_timeline_scope_selection(hist_frame_idx, thread_idx, event_idx, ev.name());
750 ImDrawList* draw_list = ImGui::GetWindowDrawList();
752 ImU32 cpu_color = color_from_hash(ev.color_hash);
753 if(!is_reference_frame)
755 cpu_color = dim_color(cpu_color);
758 const float cpu_ratio = compute_cpu_ratio(ev);
759 const float rect_w =
x1 -
x0;
760 const float cpu_x1 =
x0 + rect_w * cpu_ratio;
762 draw_list->AddRectFilled(ImVec2(
x0,
y0), ImVec2(cpu_x1,
y1), cpu_color);
764 if(cpu_ratio < 0.99f)
766 const ImU32 wait_col = is_reference_frame ? wait_color_selected : wait_color_dimmed;
767 draw_list->AddRectFilled(ImVec2(cpu_x1,
y0), ImVec2(
x1,
y1), wait_col);
770 draw_list->AddRect(ImVec2(
x0,
y0), ImVec2(
x1,
y1), IM_COL32(0, 0, 0, 80));
772 if(is_scope_selected)
774 draw_list->AddRect(ImVec2(
x0 - 1.0f,
y0 - 1.0f), ImVec2(
x1 + 1.0f,
y1 + 1.0f),
775 IM_COL32(255, 230, 90, 255), 0.0f, 0, 2.5f);
778 const float rect_h =
y1 -
y0;
779 constexpr float min_text_width_px = 8.0f;
780 if(rect_w > min_text_width_px)
782 ImVec2 text_size = ImGui::CalcTextSize(ev.name());
783 float fit_ratio = std::clamp(rect_w / std::max(text_size.x + 4.0f, 1.0f), 0.0f, 1.0f);
784 int base_alpha = is_reference_frame ? 240 : 180;
785 int alpha =
static_cast<int>(
static_cast<float>(base_alpha) * fit_ratio);
786 ImU32 text_col = is_reference_frame
787 ? IM_COL32(255, 255, 255, alpha)
788 : IM_COL32(180, 180, 180, alpha);
790 float tx =
x0 + std::max(0.0f, (rect_w - text_size.x) * 0.5f);
791 float ty =
y0 + (rect_h - text_size.y) * 0.5f;
793 draw_list->PushClipRect(ImVec2(
x0,
y0), ImVec2(
x1,
y1),
true);
794 draw_list->AddText(ImVec2(tx, ty), text_col, ev.name());
795 draw_list->PopClipRect();
798 const ImVec2
mouse = ImGui::GetMousePos();
801 const float wall_ms =
static_cast<float>(ev.end_ns - ev.start_ns) / 1'000'000.0f;
802 const float cpu_ms =
static_cast<float>(ev.cpu_end_ns - ev.cpu_start_ns) / 1'000'000.0f;
803 const float wait_ms = std::max(0.0f, wall_ms -
cpu_ms);
805 ImGui::SetNextWindowViewportToCurrent();
806 ImGui::BeginTooltip();
807 ImGui::Text(
"%s", ev.name());
808 ImGui::Text(
"Wall: %s", format_time(wall_ms).c_str());
809 ImGui::Text(
"Busy: %s (%.0f%%)", format_time(
cpu_ms).c_str(), cpu_ratio * 100.0f);
810 if(wait_ms > 0.0001f)
812 ImGui::Text(
"Idle: %s (%.0f%%)", format_time(wait_ms).c_str(),
813 (1.0f - cpu_ratio) * 100.0f);
815 ImGui::Text(
"Depth: %d", ev.depth);
816 ImGui::Text(
"Thread: %s", thread_name.c_str());
821void profiler_timeline_panel::validate_timeline_scope_selection(uint32_t frame_count)
823 if(!has_timeline_scope_selection_)
827 if(frame_count == 0 || selected_scope_hist_frame_ >= frame_count)
829 has_timeline_scope_selection_ =
false;
830 selected_scope_label_.clear();
835 const frame_snapshot* snap = profiler->get_frame_snapshot(selected_scope_hist_frame_);
838 has_timeline_scope_selection_ =
false;
839 selected_scope_label_.clear();
843 for(
const auto& ts : snap->threads)
845 if(ts.thread_index != selected_scope_thread_index_)
849 if(selected_scope_event_index_ >= ts.events.size())
851 has_timeline_scope_selection_ =
false;
852 selected_scope_label_.clear();
857 has_timeline_scope_selection_ =
false;
858 selected_scope_label_.clear();
871 show_request_ =
false;
873 ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size * 0.5f, ImGuiCond_Once);
881 if(ImGui::Begin(
name, &show_))
895 draw_recording_toolbar();
899 draw_frame_selector_bar();
903 if(has_timeline_scope_selection_ && !selected_scope_label_.empty())
905 ImGui::TextDisabled(
"Selected:");
907 ImGui::TextUnformatted(selected_scope_label_.c_str());
914 draw_aggregate_section();
916 draw_profiler_bottom_sections(ctx);
923void profiler_timeline_panel::draw_recording_toolbar()
930 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f));
937 auto_follow_ =
false;
938 const uint32_t
count = profiler->get_frame_count();
941 selected_frame_ =
static_cast<int32_t
>(
count - 1);
942 last_centered_frame_ = -2;
949 last_centered_frame_ = -2;
954 ImGui::PopStyleColor();
958 ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
963 profiler->clear_history();
964 selected_frame_ = -1;
965 last_centered_frame_ = -2;
966 has_timeline_scope_selection_ =
false;
967 selected_scope_label_.clear();
970 uint32_t frame_count = profiler->get_frame_count();
973 ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
978 int32_t display_idx = auto_follow_ ?
static_cast<int32_t
>(frame_count - 1) : selected_frame_;
981 ImGui::Text(
"Frame %d / %u", display_idx + 1, frame_count);
985 ImGui::Text(
"%u frames", frame_count);
990 ImGui::TextDisabled(
"No frames captured");
994 ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
999 last_centered_frame_ = -2;
1000 view_duration_ns_ = 20'000'000.0;
1006 ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
1010 static constexpr uint32_t history_presets[] = {64, 128, 256, 512, 1024, 2000};
1011 static constexpr const char* history_labels[] = {
"64",
"128",
"256",
"512",
"1024",
"2000"};
1012 const uint32_t current_cap = profiler->get_max_frame_history();
1013 int current_idx = 2;
1014 for(
int i = 0; i < static_cast<int>(IM_ARRAYSIZE(history_presets)); ++
i)
1016 if(history_presets[i] == current_cap)
1022 ImGui::SetNextItemWidth(80.0f);
1023 if(ImGui::Combo(
"History", ¤t_idx, history_labels, IM_ARRAYSIZE(history_labels)))
1025 profiler->set_max_frame_history(history_presets[current_idx]);
1026 if(selected_frame_ >= 0)
1028 const uint32_t
count = profiler->get_frame_count();
1031 selected_frame_ = -1;
1035 selected_frame_ = std::min(selected_frame_,
static_cast<int32_t
>(
count) - 1);
1038 last_centered_frame_ = -2;
1040 ImGui::SetItemTooltipEx(
"Max captured frames. Lower values reduce histogram cost.");
1044 ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
1047 double visible_ms = view_duration_ns_ / 1'000'000.0;
1048 if(visible_ms >= 1.0)
1050 ImGui::Text(
"%.1f ms visible", visible_ms);
1054 ImGui::Text(
"%.0f us visible", visible_ms * 1000.0);
1062auto profiler_timeline_panel::histogram_stack_height() const ->
float
1064 float h = frame_bar_height;
1065 if(show_histogram_managed_heap_)
1067 h += memory_hist_row_height;
1069 if(show_histogram_gpu_memory_)
1071 h += memory_hist_row_height;
1073 if(show_histogram_process_rss_)
1075 h += memory_hist_row_height;
1080void profiler_timeline_panel::draw_profiler_bottom_sections(
rtti::context& ctx)
1082 if(parent_ ==
nullptr)
1086 ImGui::PushID(
"profiler_bottom");
1087 if(ImGui::CollapsingHeader(
ICON_MDI_CHIP "\tRender Passes"))
1094 if(ctx.
has<settings>())
1101void profiler_timeline_panel::draw_live_histogram_stack(
float bar_width)
1103 ImDrawList* dl = ImGui::GetWindowDrawList();
1104 const ImVec2 pos = ImGui::GetCursorScreenPos();
1105 const float max_ms = std::max(frame_time_history_.
get_max(), target_60fps_ms) * 1.1f;
1106 const float max_cpu_mb = std::max(cpu_heap_mb_history_.
get_max(), 1.0f) * 1.1f;
1107 const float max_gpu_mb = std::max(gpu_memory_mb_history_.
get_max(), 1.0f) * 1.1f;
1108 const float max_rss_mb = std::max(process_rss_mb_history_.
get_max(), 1.0f) * 1.1f;
1110 render_live_sample_row(dl, pos, bar_width, frame_time_history_, max_ms, 0,
true, &frame_busy_ms_history_);
1111 const float frame_bottom = pos.y + frame_bar_height;
1112 render_histogram_guides(dl, pos, bar_width, frame_bottom, max_ms);
1113 dl->AddText(ImVec2(pos.x + 4, pos.y + 2),
1114 IM_COL32(180, 180, 200, 200),
1115 "Frame wall (ms) — color: busy, gray: wait");
1117 float row_y = frame_bottom;
1118 if(show_histogram_managed_heap_)
1120 const ImVec2 row(pos.x, row_y);
1121 render_live_sample_row(dl, row, bar_width, cpu_heap_mb_history_, max_cpu_mb, cpu_heap_hist_color,
false);
1122 dl->AddText(ImVec2(row.x + 4, row.y + 2), IM_COL32(180, 180, 200, 200),
"Managed heap (MB)");
1123 render_memory_histogram_guides(dl, row, bar_width, max_cpu_mb);
1124 row_y += memory_hist_row_height;
1126 if(show_histogram_gpu_memory_)
1128 const ImVec2 row(pos.x, row_y);
1129 render_live_sample_row(dl, row, bar_width, gpu_memory_mb_history_, max_gpu_mb, gpu_mem_hist_color,
false);
1130 dl->AddText(ImVec2(row.x + 4, row.y + 2), IM_COL32(180, 180, 200, 200),
"GPU memory (MB)");
1131 render_memory_histogram_guides(dl, row, bar_width, max_gpu_mb);
1132 row_y += memory_hist_row_height;
1134 if(show_histogram_process_rss_)
1136 const ImVec2 row(pos.x, row_y);
1137 render_live_sample_row(dl, row, bar_width, process_rss_mb_history_, max_rss_mb, process_rss_hist_color,
false);
1138 dl->AddText(ImVec2(row.x + 4, row.y + 2), IM_COL32(180, 180, 200, 200),
"Process RSS (MB)");
1139 render_memory_histogram_guides(dl, row, bar_width, max_rss_mb);
1142 ImGui::Dummy(ImVec2(bar_width, histogram_stack_height()));
1145void profiler_timeline_panel::draw_frame_selector_bar()
1149 auto frame_start = profiler->get_frame_start_ns();
1150 auto frame_end = profiler->get_frame_end_ns();
1151 float frame_ms = 0.0f;
1152 if(frame_end > frame_start)
1154 frame_ms =
static_cast<float>(frame_end - frame_start) / 1'000'000.0f;
1158 const uint32_t frame_count = profiler->get_frame_count();
1159 float frame_busy_ms = frame_ms;
1162 const frame_snapshot* latest = profiler->get_frame_snapshot(frame_count - 1);
1163 if(latest !=
nullptr)
1165 frame_busy_ms = latest->frame_busy_ms;
1168 frame_busy_ms_history_.
push_sample(frame_busy_ms);
1170 ImGui::Checkbox(
"Managed heap", &show_histogram_managed_heap_);
1172 ImGui::Checkbox(
"GPU memory", &show_histogram_gpu_memory_);
1174 ImGui::Checkbox(
"Process RSS", &show_histogram_process_rss_);
1178 if(frame_count == 0)
1180 const float cpu_mb =
static_cast<float>(dotnet::gc_get_used_size()) / megabyte_divisor;
1182 float gpu_mb = 0.0f;
1186 gpu_mb =
static_cast<float>(
stats->gpuMemoryUsed) / megabyte_divisor;
1189 const float rss_mb =
1192 auto region = ImGui::GetContentRegionAvail();
1193 draw_live_histogram_stack(region.x);
1197 auto region = ImGui::GetContentRegionAvail();
1198 float bar_width = region.x;
1199 if(bar_width < 10.0f)
1204 draw_frame_histogram(profiler, frame_count, bar_width);
1207void profiler_timeline_panel::draw_frame_histogram(performance_profiler* profiler,
1208 uint32_t frame_count,
1211 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1212 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
1213 const float frame_bottom_y = canvas_pos.y + frame_bar_height;
1215 float fc_f =
static_cast<float>(frame_count);
1216 float eff_range = (hist_range_ <= 0.0f) ? fc_f :
std::min(hist_range_, fc_f);
1217 float eff_start = std::clamp(hist_start_, 0.0f, std::max(0.0f, fc_f - eff_range));
1221 eff_start = std::max(0.0f, fc_f - eff_range);
1222 hist_start_ = eff_start;
1225 float entry_w = bar_width / eff_range;
1227 int32_t first_vis = std::max(0,
static_cast<int32_t
>(std::floor(eff_start)));
1228 int32_t last_vis = std::min(
static_cast<int32_t
>(frame_count) - 1,
1229 static_cast<int32_t
>(std::ceil(eff_start + eff_range)));
1231 draw_list->AddRectFilled(canvas_pos,
1232 ImVec2(canvas_pos.x + bar_width, frame_bottom_y),
1233 IM_COL32(30, 30, 30, 255));
1235 float max_frame_ms = target_60fps_ms;
1236 float max_cpu_mb = 1.0f;
1237 float max_gpu_mb = 1.0f;
1238 float max_rss_mb = 1.0f;
1239 for(int32_t i = first_vis;
i <= last_vis; ++
i)
1241 const auto* snap = profiler->get_frame_snapshot(
static_cast<uint32_t
>(i));
1246 max_frame_ms = std::max(max_frame_ms, snap->frame_wall_ms);
1247 if(show_histogram_managed_heap_)
1250 std::max(max_cpu_mb,
static_cast<float>(snap->cpu_heap_used_bytes) / megabyte_divisor);
1252 if(show_histogram_gpu_memory_)
1255 std::max(max_gpu_mb,
static_cast<float>(snap->gpu_memory_used_bytes) / megabyte_divisor);
1257 if(show_histogram_process_rss_)
1260 std::max(max_rss_mb,
static_cast<float>(snap->process_resident_bytes) / megabyte_divisor);
1263 float scale_max = max_frame_ms * 1.1f;
1264 const float scale_cpu_mb = max_cpu_mb * 1.1f;
1265 const float scale_gpu_mb = max_gpu_mb * 1.1f;
1266 const float scale_rss_mb = max_rss_mb * 1.1f;
1268 int32_t effective_selected = auto_follow_
1269 ?
static_cast<int32_t
>(frame_count) - 1
1271 effective_selected = std::clamp(effective_selected, 0,
static_cast<int32_t
>(frame_count) - 1);
1273 draw_list->AddText(ImVec2(canvas_pos.x + 4, canvas_pos.y + 2),
1274 IM_COL32(180, 180, 200, 200),
1275 "Frame wall (ms) — color: busy, gray: wait");
1277 render_histogram_bars(draw_list, profiler, first_vis, last_vis,
1278 canvas_pos, frame_bottom_y, bar_width, eff_start, entry_w, scale_max);
1279 render_histogram_guides(draw_list, canvas_pos, bar_width, frame_bottom_y, scale_max);
1281 float mem_row_y = frame_bottom_y;
1282 if(show_histogram_managed_heap_)
1284 const ImVec2 cpu_row_top(canvas_pos.x, mem_row_y);
1285 draw_list->AddRectFilled(cpu_row_top,
1286 ImVec2(canvas_pos.x + bar_width, mem_row_y + memory_hist_row_height),
1287 IM_COL32(26, 26, 28, 255));
1288 draw_list->AddText(ImVec2(cpu_row_top.x + 4, cpu_row_top.y + 2),
1289 IM_COL32(180, 180, 200, 200),
"Managed heap (MB)");
1290 render_memory_mb_row(draw_list, profiler, first_vis, last_vis, cpu_row_top, bar_width, eff_start,
1291 entry_w, scale_cpu_mb, memory_histogram_metric::managed_heap_mb, cpu_heap_hist_color);
1292 render_memory_histogram_guides(draw_list, cpu_row_top, bar_width, scale_cpu_mb);
1293 mem_row_y += memory_hist_row_height;
1295 if(show_histogram_gpu_memory_)
1297 const ImVec2 gpu_row_top(canvas_pos.x, mem_row_y);
1298 draw_list->AddRectFilled(gpu_row_top,
1299 ImVec2(canvas_pos.x + bar_width, mem_row_y + memory_hist_row_height),
1300 IM_COL32(26, 26, 28, 255));
1301 draw_list->AddText(ImVec2(gpu_row_top.x + 4, gpu_row_top.y + 2),
1302 IM_COL32(180, 180, 200, 200),
"GPU memory (MB)");
1303 render_memory_mb_row(draw_list, profiler, first_vis, last_vis, gpu_row_top, bar_width, eff_start,
1304 entry_w, scale_gpu_mb, memory_histogram_metric::gpu_memory_mb, gpu_mem_hist_color);
1305 render_memory_histogram_guides(draw_list, gpu_row_top, bar_width, scale_gpu_mb);
1306 mem_row_y += memory_hist_row_height;
1308 if(show_histogram_process_rss_)
1310 const ImVec2 rss_row_top(canvas_pos.x, mem_row_y);
1311 draw_list->AddRectFilled(rss_row_top,
1312 ImVec2(canvas_pos.x + bar_width, mem_row_y + memory_hist_row_height),
1313 IM_COL32(26, 26, 28, 255));
1314 draw_list->AddText(ImVec2(rss_row_top.x + 4, rss_row_top.y + 2),
1315 IM_COL32(180, 180, 200, 200),
"Process RSS (MB)");
1316 render_memory_mb_row(draw_list, profiler, first_vis, last_vis, rss_row_top, bar_width, eff_start,
1317 entry_w, scale_rss_mb, memory_histogram_metric::process_rss_mb, process_rss_hist_color);
1318 render_memory_histogram_guides(draw_list, rss_row_top, bar_width, scale_rss_mb);
1321 const float stack_bottom_y = canvas_pos.y + histogram_stack_height();
1322 const float plot_top_y = canvas_pos.y + histogram_plot_top_pad;
1323 render_histogram_cursor(draw_list, plot_top_y, stack_bottom_y, bar_width,
1324 effective_selected, eff_start, entry_w, canvas_pos.x);
1326 ImGui::InvisibleButton(
"##frame_histogram", ImVec2(bar_width, this->histogram_stack_height()));
1328 handle_histogram_input(profiler, frame_count, canvas_pos, bar_width, eff_start, eff_range);
1331void profiler_timeline_panel::handle_histogram_input(performance_profiler* profiler,
1332 uint32_t frame_count,
1338 float entry_w = bar_width / eff_range;
1340 handle_histogram_zoom_pan(frame_count, canvas_pos, bar_width, eff_start, eff_range);
1342 bool hovered = ImGui::IsItemHovered();
1343 bool active = ImGui::IsItemActive();
1345 if(hovered || active)
1347 float mouse_x = ImGui::GetMousePos().x - canvas_pos.x;
1348 int32_t hover_idx =
static_cast<int32_t
>(eff_start + mouse_x / entry_w);
1349 hover_idx = std::clamp(hover_idx, 0,
static_cast<int32_t
>(frame_count) - 1);
1353 const auto* hsnap = profiler->get_frame_snapshot(
static_cast<uint32_t
>(hover_idx));
1354 if(hsnap && hsnap->frame_wall_ms > 0.0f)
1356 const float hms = hsnap->frame_wall_ms;
1357 const float busy_ms = hsnap->frame_busy_ms;
1358 const float wait_ms = std::max(0.0f, hms -
busy_ms);
1359 const float busy_pct = (hms > 0.001f) ? (
busy_ms / hms) * 100.0f : 0.0f;
1361 ImGui::SetNextWindowViewportToCurrent();
1362 ImGui::BeginTooltip();
1363 ImGui::Text(
"Frame %d / %u", hover_idx + 1, frame_count);
1364 ImGui::Text(
"Wall: %.2f ms (%.0f FPS)", hms, hms > 0.001f ? 1000.0f / hms : 0.0f);
1365 ImGui::Text(
"Busy: %.2f ms (%.0f%%)",
busy_ms, busy_pct);
1366 if(wait_ms > 0.001f)
1368 ImGui::Text(
"Wait: %.2f ms (%.0f%%)", wait_ms, 100.0f - busy_pct);
1371 static_cast<std::uint64_t
>(std::max<int64_t>(0, hsnap->cpu_heap_used_bytes)), 0);
1373 static_cast<std::uint64_t
>(std::max<int64_t>(0, hsnap->gpu_memory_used_bytes)), 0);
1375 static_cast<std::uint64_t
>(std::max<int64_t>(0, hsnap->process_resident_bytes)), 0);
1376 if(show_histogram_managed_heap_)
1378 ImGui::Text(
"Managed heap: %s", heap_pretty.c_str());
1380 if(show_histogram_gpu_memory_)
1382 ImGui::Text(
"GPU memory: %s", gpu_pretty.c_str());
1384 if(show_histogram_process_rss_)
1386 ImGui::Text(
"Process RSS: %s", rss_pretty.c_str());
1388 ImGui::EndTooltip();
1392 if(ImGui::IsMouseClicked(ImGuiMouseButton_Left) || is_dragging_cursor_)
1394 selected_frame_ = hover_idx;
1395 auto_follow_ =
false;
1396 last_centered_frame_ = -2;
1397 is_dragging_cursor_ =
true;
1401 if(is_dragging_cursor_ && !ImGui::IsMouseDown(ImGuiMouseButton_Left))
1403 is_dragging_cursor_ =
false;
1406 if(ImGui::IsItemFocused())
1408 if(ImGui::IsKeyPressed(ImGuiKey_LeftArrow) && selected_frame_ > 0)
1411 auto_follow_ =
false;
1412 last_centered_frame_ = -2;
1414 if(ImGui::IsKeyPressed(ImGuiKey_RightArrow) &&
1415 selected_frame_ <
static_cast<int32_t
>(frame_count) - 1)
1418 auto_follow_ =
false;
1419 last_centered_frame_ = -2;
1424void profiler_timeline_panel::handle_histogram_zoom_pan(uint32_t frame_count,
1430 if(!ImGui::IsItemHovered())
1435 float fc_f =
static_cast<float>(frame_count);
1436 float mouse_x = ImGui::GetMousePos().x - canvas_pos.x;
1437 float mouse_frac = std::clamp(mouse_x / bar_width, 0.0f, 1.0f);
1439 float wheel = ImGui::GetIO().MouseWheel;
1442 if(ImGui::GetIO().KeyCtrl)
1444 float mouse_frame = eff_start + mouse_frac * eff_range;
1445 float new_range = eff_range * (1.0f - wheel * 0.15f);
1446 new_range = std::clamp(new_range, 10.0f, fc_f);
1448 hist_start_ = mouse_frame - mouse_frac * new_range;
1449 hist_start_ = std::clamp(hist_start_, 0.0f, std::max(0.0f, fc_f - new_range));
1450 hist_range_ = new_range;
1454 if(ImGui::IsMouseDragging(ImGuiMouseButton_Middle))
1456 float frames_per_px = eff_range / bar_width;
1457 hist_start_ -= ImGui::GetIO().MouseDelta.x * frames_per_px;
1458 hist_start_ = std::clamp(hist_start_, 0.0f, std::max(0.0f, fc_f - eff_range));
1466void profiler_timeline_panel::draw_time_ruler(
double view_start_ns,
1467 double reference_ns,
1468 double ns_per_pixel,
1472 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1474 draw_list->AddRectFilled(canvas_pos,
1475 ImVec2(canvas_pos.x + ruler_width, canvas_pos.y + ruler_height),
1476 IM_COL32(40, 40, 40, 255));
1478 double view_end_ns = view_start_ns +
static_cast<double>(ruler_width) * ns_per_pixel;
1479 double visible_ms = (view_end_ns - view_start_ns) / 1'000'000.0;
1480 double raw_interval = visible_ms / 10.0;
1482 static constexpr std::array nice_intervals = {
1483 0.001, 0.002, 0.005, 0.01, 0.02, 0.05,
1484 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0};
1486 double tick_ms = nice_intervals.back();
1487 for(
double ni : nice_intervals)
1489 if(ni >= raw_interval)
1496 double tick_ns = tick_ms * 1'000'000.0;
1498 double ref_offset = view_start_ns - reference_ns;
1499 double first_tick_offset = std::ceil(ref_offset / tick_ns) * tick_ns;
1501 for(
double offset = first_tick_offset; ; offset += tick_ns)
1503 double abs_ns = reference_ns + offset;
1504 if(abs_ns > view_end_ns)
1509 float x = canvas_pos.x +
static_cast<float>((abs_ns - view_start_ns) / ns_per_pixel);
1510 if(
x < canvas_pos.x)
1515 draw_list->AddLine(ImVec2(
x, canvas_pos.y + ruler_height * 0.5f),
1516 ImVec2(
x, canvas_pos.y + ruler_height),
1517 IM_COL32(200, 200, 200, 180));
1519 double label_ms = offset / 1'000'000.0;
1523 label = fmt::format(
"{:.0f}ms", label_ms);
1525 else if(tick_ms >= 0.01)
1527 label = fmt::format(
"{:.2f}ms", label_ms);
1531 label = fmt::format(
"{:.3f}ms", label_ms);
1534 draw_list->AddText(ImVec2(
x + 2.0f, canvas_pos.y + 2.0f),
1535 IM_COL32(200, 200, 200, 220),
1544void profiler_timeline_panel::draw_timeline()
1547 uint32_t frame_count = profiler->get_frame_count();
1548 validate_timeline_scope_selection(frame_count);
1551 ImGui::TextDisabled(
"Recording...");
1554 if(frame_count == 0)
1556 ImGui::TextDisabled(
"No frame data to display");
1560 int32_t frame_idx = auto_follow_
1561 ?
static_cast<int32_t
>(frame_count) - 1
1563 frame_idx = std::clamp(frame_idx, 0,
static_cast<int32_t
>(frame_count) - 1);
1565 const frame_snapshot* selected_snap = profiler->get_frame_snapshot(
1566 static_cast<uint32_t
>(frame_idx));
1567 if(!selected_snap || selected_snap->frame_end_ns <= selected_snap->frame_start_ns)
1569 ImGui::TextDisabled(
"No valid frame data");
1573 double sel_start =
static_cast<double>(selected_snap->frame_start_ns);
1574 double sel_end =
static_cast<double>(selected_snap->frame_end_ns);
1575 const double sel_duration_ns = sel_end - sel_start;
1576 const bool selection_changed = (frame_idx != last_centered_frame_);
1579 if(selection_changed)
1583 constexpr double frame_fit_padding = 1.1;
1584 if(sel_duration_ns > view_duration_ns_)
1586 view_duration_ns_ = std::clamp(sel_duration_ns * frame_fit_padding,
1587 min_view_duration_ns,
1588 max_view_duration_ns);
1590 last_centered_frame_ = frame_idx;
1594 view_start_ns_ = sel_end - view_duration_ns_ * 0.85;
1596 else if(selection_changed)
1598 const double center = (sel_start + sel_end) / 2.0;
1599 view_start_ns_ =
center - view_duration_ns_ / 2.0;
1603 auto region = ImGui::GetContentRegionAvail();
1604 float lane_content_width = region.x - lane_header_width;
1605 if(lane_content_width < 10.0f)
1610 double ns_per_pixel = view_duration_ns_ /
static_cast<double>(lane_content_width);
1611 double view_end_ns = view_start_ns_ + view_duration_ns_;
1614 std::vector<const frame_snapshot*> visible_frames;
1615 gather_visible_frames(profiler, frame_count, selected_snap, visible_frames);
1616 std::vector<uint32_t> visible_hist_indices;
1617 visible_hist_indices.reserve(visible_frames.size());
1618 for(
const frame_snapshot* f : visible_frames)
1620 visible_hist_indices.push_back(hist_index_of(profiler, f));
1624 std::vector<thread_entry> threads;
1625 collect_unique_threads(visible_frames, threads);
1629 ImGui::TextDisabled(
"No thread data");
1634 float total_height = ruler_height;
1635 for(
const auto& t : threads)
1637 const uint16_t layout_depth =
1638 max_nesting_depth_for_thread_in_view(visible_frames,
t.index, view_start_ns_, view_end_ns);
1639 total_height += lane_height_for_depth(layout_depth);
1641 if(threads.size() > 1)
1643 total_height += thread_lane_spacing *
static_cast<float>(threads.size() - 1);
1646 float timeline_height = std::min(region.y * 0.65f,
1647 std::max(total_height + 10.0f, 100.0f));
1650 ImGui::BeginChild(
"##timeline_scroll", ImVec2(0, timeline_height), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY,
1651 ImGuiWindowFlags_NoScrollWithMouse);
1653 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1654 ImVec2 base_pos = ImGui::GetCursorScreenPos();
1657 draw_time_ruler(view_start_ns_, sel_start, ns_per_pixel,
1659 ImVec2(base_pos.x + lane_header_width, base_pos.y));
1660 ImGui::Dummy(ImVec2(0, ruler_height));
1663 for(
size_t ti = 0; ti < threads.size(); ++ti)
1665 const auto& thread = threads[ti];
1666 const uint16_t layout_depth =
1667 max_nesting_depth_for_thread_in_view(visible_frames, thread.index, view_start_ns_, view_end_ns);
1668 const float lane_height = lane_height_for_depth(layout_depth);
1670 ImGui::Text(
"%s", thread.name.c_str());
1671 ImGui::SameLine(lane_header_width);
1673 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
1675 draw_list->AddRectFilled(canvas_pos,
1676 ImVec2(canvas_pos.x + lane_content_width, canvas_pos.y + lane_height),
1677 IM_COL32(30, 30, 30, 255));
1679 lane_context lc{canvas_pos, lane_content_width, lane_height,
1680 view_start_ns_, view_end_ns, ns_per_pixel};
1682 draw_list->PushClipRect(canvas_pos,
1683 ImVec2(canvas_pos.x + lane_content_width, canvas_pos.y + lane_height),
1685 draw_lane_events(lc, visible_frames, visible_hist_indices, selected_snap,
1686 thread.index, thread.name);
1687 draw_lane_frame_boundaries(lc, visible_frames, selected_snap);
1688 draw_list->PopClipRect();
1690 ImGui::Dummy(ImVec2(0, lane_height));
1691 if(ti + 1 < threads.size())
1693 ImGui::Dummy(ImVec2(0, thread_lane_spacing));
1698 ImGuiIO& io = ImGui::GetIO();
1699 if(ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup))
1701 if(ImGui::IsMouseDragging(ImGuiMouseButton_Middle))
1703 ImGui::SetScrollY(ImGui::GetScrollY() - io.MouseDelta.y);
1704 if(lane_content_width > 0)
1706 const double ns_per_px = view_duration_ns_ /
static_cast<double>(lane_content_width);
1707 view_start_ns_ -=
static_cast<double>(io.MouseDelta.x) * ns_per_px;
1711 const float wheel = io.MouseWheel;
1716 const ImGuiWindow* win = ImGui::GetCurrentWindowRead();
1717 const double graph_left_d =
1719 ?
static_cast<double>(win->InnerRect.Min.x + lane_header_width)
1720 : static_cast<double>(io.MousePos.
x);
1721 double mouse_x =
static_cast<double>(io.MousePos.x) - graph_left_d;
1722 mouse_x = std::clamp(mouse_x, 0.0,
static_cast<double>(lane_content_width));
1723 const double mouse_frac = mouse_x /
static_cast<double>(lane_content_width);
1724 const double mouse_time = view_start_ns_ + mouse_frac * view_duration_ns_;
1726 view_duration_ns_ *= (1.0 -
static_cast<double>(wheel) * 0.15);
1728 std::clamp(view_duration_ns_, min_view_duration_ns, max_view_duration_ns);
1729 view_start_ns_ = mouse_time - mouse_frac * view_duration_ns_;
1733 ImGui::SetScrollY(ImGui::GetScrollY() - wheel * timeline_wheel_scroll_px);
1740 if(ImGui::IsItemHovered())
1742 if(ImGui::IsKeyPressed(ImGuiKey_Escape) && has_timeline_scope_selection_)
1744 has_timeline_scope_selection_ =
false;
1745 selected_scope_label_.clear();
1754void profiler_timeline_panel::gather_visible_frames(performance_profiler* profiler,
1755 uint32_t frame_count,
1756 const frame_snapshot* selected_snap,
1757 std::vector<const frame_snapshot*>& out)
1759 double view_end_ns = view_start_ns_ + view_duration_ns_;
1762 for(uint32_t i = 0;
i < frame_count; ++
i)
1764 const auto* snap = profiler->get_frame_snapshot(i);
1765 if(!snap || snap->frame_end_ns <= snap->frame_start_ns)
1769 double snap_start =
static_cast<double>(snap->frame_start_ns);
1770 double snap_end =
static_cast<double>(snap->frame_end_ns);
1771 if(snap_end >= view_start_ns_ && snap_start <= view_end_ns)
1773 out.push_back(snap);
1779 out.push_back(selected_snap);
1783void profiler_timeline_panel::collect_unique_threads(
1784 const std::vector<const frame_snapshot*>& frames,
1785 std::vector<thread_entry>& out)
1787 for(
const auto*
frame : frames)
1789 for(
const auto& ts :
frame->threads)
1791 uint16_t local_max = 0;
1792 for(
const auto& ev : ts.events)
1794 if(ev.end_ns > ev.start_ns)
1796 local_max = std::max(local_max, ev.depth);
1800 auto it = std::find_if(
out.begin(),
out.end(),
1801 [&](
const thread_entry& e) ->
bool { return e.index == ts.thread_index; });
1805 it->max_depth = std::max(it->max_depth, local_max);
1809 out.push_back({ts.name, ts.thread_index, local_max});
1815void profiler_timeline_panel::draw_lane_events(
1816 const lane_context& lc,
1817 const std::vector<const frame_snapshot*>& visible_frames,
1818 const std::vector<uint32_t>& visible_hist_indices,
1819 const frame_snapshot* selected_snap,
1820 uint16_t thread_index,
1821 const std::string& thread_name)
1823 for(
size_t fi = 0; fi < visible_frames.size(); ++fi)
1825 const frame_snapshot*
frame = visible_frames[fi];
1826 const uint32_t hidx =
1827 fi < visible_hist_indices.size() ? visible_hist_indices[fi] : UINT32_MAX;
1828 const bool is_reference_frame = (
frame == selected_snap);
1830 for(
const auto& ts :
frame->threads)
1832 if(ts.thread_index != thread_index)
1837 for(uint32_t event_idx = 0; event_idx < ts.events.size(); ++event_idx)
1839 const profile_event& ev = ts.events[event_idx];
1840 if(ev.end_ns > ev.start_ns)
1842 timeline_render_event_block(lc, ev, is_reference_frame, thread_name,
this, hidx,
1843 thread_index, event_idx);
1850void profiler_timeline_panel::draw_lane_frame_boundaries(
1851 const lane_context& lc,
1852 const std::vector<const frame_snapshot*>& visible_frames,
1853 const frame_snapshot* selected_snap)
1855 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1857 for(
const auto*
frame : visible_frames)
1859 double boundary =
static_cast<double>(
frame->frame_end_ns);
1860 float bx = lc.canvas_pos.x +
static_cast<float>(
1861 (boundary - lc.view_start_ns) / lc.ns_per_pixel);
1863 if(
bx > lc.canvas_pos.x &&
bx < lc.canvas_pos.x + lc.lane_content_width)
1865 ImU32 line_col = (
frame == selected_snap)
1866 ? IM_COL32(255, 200, 50, 100)
1867 : IM_COL32(255, 255, 255, 40);
1868 draw_list->AddLine(ImVec2(
bx, lc.canvas_pos.y),
1869 ImVec2(
bx, lc.canvas_pos.y + lc.lane_height),
1879void profiler_timeline_panel::draw_aggregate_section()
1887 const auto& data = profiler->get_per_frame_data_read();
1891 ImGui::TextDisabled(
"No profiler scopes recorded yet.");
1896 "Each row is one scope name: wall ms summed over all matching spans per frame (same label merges across threads). "
1897 "Trend uses one Y scale for all rows (max of per-row history max).");
1900 static int sort_mode = 0;
1901 ImGui::AlignTextToFramePadding();
1902 ImGui::TextUnformatted(
"Sort by");
1904 ImGui::SetNextItemWidth(200.0f);
1905 static constexpr std::array<const char*, 4> sort_labels = {
1906 "Average (hot first)",
"Peak (max ms)",
"This frame (partial)",
"Name (A-Z)"};
1907 ImGui::Combo(
"##agg_sort", &sort_mode, sort_labels.data(),
static_cast<int>(sort_labels.size()));
1909 using record_entry = performance_profiler::record_data_t::value_type;
1910 using entry_cptr =
const record_entry*;
1911 std::vector<entry_cptr> rows;
1912 rows.reserve(data.size());
1913 for(
const auto& e : data)
1918 const auto cmp_rows = [](entry_cptr
a, entry_cptr
b) ->
bool
1923 if(
a->second.get_avg() !=
b->second.get_avg())
1925 return a->second.get_avg() >
b->second.get_avg();
1929 if(
a->second.get_max() !=
b->second.get_max())
1931 return a->second.get_max() >
b->second.get_max();
1935 if(
a->second.get_time_since_swap() !=
b->second.get_time_since_swap())
1937 return a->second.get_time_since_swap() >
b->second.get_time_since_swap();
1943 return a->first <
b->first;
1945 std::sort(rows.begin(), rows.end(), cmp_rows);
1947 float scale_avg = 0.0001f;
1948 float scale_hist_max = 0.0001f;
1949 for(entry_cptr ep : rows)
1951 scale_avg = std::max(scale_avg, ep->second.get_avg());
1952 scale_hist_max = std::max(scale_hist_max, ep->second.get_max());
1954 const float spark_ymax = scale_hist_max * 1.05f;
1956 constexpr float table_max_h = 320.0f;
1957 const float avail_h = ImGui::GetContentRegionAvail().y;
1958 const float table_h = std::max(120.0f, std::min(table_max_h, std::max(160.0f, avail_h)));
1961 constexpr ImGuiTableFlags table_flags = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersInnerH |
1962 ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY |
1963 ImGuiTableFlags_Resizable | ImGuiTableFlags_Hideable |
1964 ImGuiTableFlags_Reorderable;
1965 if(ImGui::BeginTable(
"##aggregate_scopes", 7, table_flags, ImVec2(-1.0f, table_h)))
1967 ImGui::TableSetupScrollFreeze(0, 1);
1968 ImGui::TableSetupColumn(
"Share", ImGuiTableColumnFlags_WidthFixed, 76.0f);
1969 ImGui::TableSetupColumn(
"Trend", ImGuiTableColumnFlags_WidthFixed, 140.0f);
1970 ImGui::TableSetupColumn(
"Scope", ImGuiTableColumnFlags_WidthStretch);
1971 ImGui::TableSetupColumn(
"Frame", ImGuiTableColumnFlags_WidthFixed, 88.0f);
1972 ImGui::TableSetupColumn(
"Avg", ImGuiTableColumnFlags_WidthFixed, 64.0f);
1973 ImGui::TableSetupColumn(
"Max", ImGuiTableColumnFlags_WidthFixed, 64.0f);
1974 ImGui::TableSetupColumn(
"Min", ImGuiTableColumnFlags_WidthFixed, 64.0f);
1975 ImGui::TableHeadersRow();
1977 const float bar_h = ImGui::GetTextLineHeight();
1979 for(entry_cptr ep : rows)
1981 const std::string&
name = ep->first;
1982 const performance_profiler::per_frame_data& pfd = ep->second;
1984 ImGui::TableNextRow();
1985 ImGui::PushID(
name.c_str());
1987 ImGui::TableNextColumn();
1988 const float bar_frac = scale_avg > 0.0f ? std::clamp(pfd.get_avg() / scale_avg, 0.0f, 1.0f) : 0.0f;
1989 ImGui::ProgressBar(bar_frac, ImVec2(-1.0f, bar_h),
"");
1990 ImGui::SetItemTooltipEx(
"Average ms vs largest average in this table (%.3f ms).", scale_avg);
1993 ImGui::TableNextColumn();
1995 ImGui::PlotLines(
"##spark",
2002 ImVec2(-1.0f, 36.0f));
2003 ImGui::SetItemTooltipEx(
2004 "Last %u frames: total ms per frame for this name (oldest left, newest right). Y max = %.3f ms.",
2009 ImGui::TableNextColumn();
2010 ImGui::TextUnformatted(
name.c_str());
2011 ImGui::SetItemTooltipEx(
"%s",
name.c_str());
2014 ImGui::TableNextColumn();
2015 ImGui::Text(
"%.3f\n%u", pfd.get_time_since_swap(),
static_cast<unsigned>(pfd.get_samples_since_swap()));
2016 ImGui::SetItemTooltipEx(
"In-progress frame: summed ms so far and number of ended spans.");
2018 ImGui::TableNextColumn();
2019 ImGui::Text(
"%.3f", pfd.get_avg());
2021 ImGui::TableNextColumn();
2022 ImGui::Text(
"%.3f", pfd.get_max());
2024 ImGui::TableNextColumn();
2025 ImGui::Text(
"%.3f", pfd.get_min());
auto gpu_profiler_enabled() -> bool &
Shared with profiler_timeline_panel for gfx view/encoder timing (Render Passes section).
void on_frame_ui_render(rtti::context &ctx, const char *name)
static constexpr uint32_t num_samples
auto get_max() const -> float
auto push_sample(float value) -> void
#define ICON_MDI_CLOCK_OUTLINE
#define ICON_MDI_FIT_TO_PAGE
std::vector< size_t > parent_
void PushFont(Font::Enum _font)
const stats * get_stats()
Hash specialization for batch_key to enable use in std::unordered_map.
unravel::sample_data sample_data
void profiler_draw_gpu_resources_section()
auto get_app_profiler() -> performance_profiler *
void draw_gpu_submit_profiler_ui(const gfx::stats *stats, bool *enable_profiler)
Checkbox to toggle BGFX_DEBUG_PROFILER, then encoder (CPU submit) and view (CPU submit / GPU execute)...
void profiler_draw_eviction_section(eviction_settings &state)
auto format_bytes(std::uint64_t bytes, std::uint8_t num_frac) -> std::string