Unravel Engine C++ Reference
Loading...
Searching...
No Matches
profiler_timeline_panel.cpp
Go to the documentation of this file.
2
6#include "../panel.h"
7
8#include <imgui/imgui.h>
9#include <imgui/imgui_internal.h>
14#include <graphics/graphics.h>
15#include <dotnetpp/dotnetpp.h>
16
17#include <algorithm>
18#include <array>
19#include <cstdint>
20#include <vector>
21
22namespace unravel
23{
24
25namespace
26{
27
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);
39
40enum class memory_histogram_metric : uint8_t
41{
42 managed_heap_mb,
43 gpu_memory_mb,
44 process_rss_mb
45};
46
47auto memory_mb_from_snapshot(const frame_snapshot* snap, memory_histogram_metric metric) -> float
48{
49 if(snap == nullptr)
50 {
51 return 0.0f;
52 }
53 switch(metric)
54 {
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;
61 }
62 return 0.0f;
63}
64
65auto memory_hist_inner_height() -> float
66{
67 return memory_hist_row_height - memory_hist_top_pad;
68}
69
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;
76
77constexpr float target_60fps_ms = 16.667f;
78
79constexpr double min_view_duration_ns = 100'000.0;
80constexpr double max_view_duration_ns = 2'000'000'000.0;
81
83constexpr uint16_t max_timeline_lane_depth = 48;
84constexpr float thread_lane_spacing = 4.0f;
85constexpr float timeline_wheel_scroll_px = 40.0f;
86
87auto lane_height_for_depth(uint16_t max_depth) -> float
88{
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;
91}
92
96auto max_nesting_depth_for_thread_in_view(const std::vector<const frame_snapshot*>& frames,
97 uint16_t thread_index,
98 double view_start_ns,
99 double view_end_ns) -> uint16_t
100{
101 struct span
102 {
103 int64_t s{};
104 int64_t e{};
105 };
106
107 std::vector<span> spans;
108 spans.reserve(512);
109
110 for(const frame_snapshot* frame : frames)
111 {
112 if(!frame)
113 {
114 continue;
115 }
116 for(const auto& ts : frame->threads)
117 {
118 if(ts.thread_index != thread_index)
119 {
120 continue;
121 }
122 for(const auto& ev : ts.events)
123 {
124 if(ev.end_ns <= ev.start_ns)
125 {
126 continue;
127 }
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)
131 {
132 continue;
133 }
134 spans.push_back({ev.start_ns, ev.end_ns});
135 }
136 }
137 }
138
139 if(spans.empty())
140 {
141 return 0;
142 }
143
144 std::sort(spans.begin(), spans.end(), [](const span& a, const span& b) -> bool
145 {
146 if(a.s != b.s)
147 {
148 return a.s < b.s;
149 }
150 return a.e > b.e;
151 });
152
153 std::vector<int64_t> active_ends;
154 active_ends.reserve(spans.size());
155 uint16_t max_d = 0;
156
157 for(const span& sp : spans)
158 {
159 while(!active_ends.empty() && active_ends.back() <= sp.s)
160 {
161 active_ends.pop_back();
162 }
163 const auto d = static_cast<uint16_t>(active_ends.size());
164 if(d > max_d)
165 {
166 max_d = d;
167 }
168 active_ends.push_back(sp.e);
169 }
170
171 return max_d;
172}
173
174auto color_from_hash(uint32_t hash) -> ImU32
175{
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;
179
180 float r{}, g{}, b{};
181 ImGui::ColorConvertHSVtoRGB(h, s, v, r, g, b);
182 return IM_COL32(
183 static_cast<int>(r * 255),
184 static_cast<int>(g * 255),
185 static_cast<int>(b * 255),
186 220);
187}
188
189auto dim_color(ImU32 col) -> ImU32
190{
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);
195}
196
197auto format_time(float ms) -> std::string
198{
199 if(ms < 0.001f)
200 {
201 return fmt::format("{:.0f} ns", ms * 1'000'000.0f);
202 }
203 if(ms < 1.0f)
204 {
205 return fmt::format("{:.1f} us", ms * 1'000.0f);
206 }
207 return fmt::format("{:.2f} ms", ms);
208}
209
210using lane_context = profiler_timeline_panel::lane_context;
211
212struct event_lane_geom
213{
214 bool ok{};
215 float x0{};
216 float y0{};
217 float x1{};
218 float y1{};
219};
220
221auto compute_event_geom(const lane_context& lc, const profile_event& ev) -> event_lane_geom
222{
223 event_lane_geom g{};
224 const double ev_start = static_cast<double>(ev.start_ns);
225 const double ev_end = static_cast<double>(ev.end_ns);
226
227 if(ev_end < lc.view_start_ns || ev_start > lc.view_end_ns)
228 {
229 return g;
230 }
231
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);
236
237 if((g.x1 - g.x0) < min_visible_width_px)
238 {
239 return g;
240 }
241
242 g.y0 = lc.canvas_pos.y + static_cast<float>(ev.depth) * row_height;
243 g.y1 = g.y0 + row_height - 1.0f;
244 g.ok = true;
245 return g;
246}
247
248auto hist_index_of(performance_profiler* profiler, const frame_snapshot* snap) -> uint32_t
249{
250 if(!profiler || !snap)
251 {
252 return UINT32_MAX;
253 }
254 const uint32_t n = profiler->get_frame_count();
255 for(uint32_t i = 0; i < n; ++i)
256 {
257 if(profiler->get_frame_snapshot(i) == snap)
258 {
259 return i;
260 }
261 }
262 return UINT32_MAX;
263}
264
265constexpr ImU32 wait_color_selected = IM_COL32(50, 50, 55, 200);
266constexpr ImU32 wait_color_dimmed = IM_COL32(30, 30, 35, 140);
267
268auto compute_cpu_ratio(const profile_event& ev) -> float
269{
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)
273 {
274 return 1.0f;
275 }
276 return std::clamp(static_cast<float>(cpu) / static_cast<float>(wall), 0.0f, 1.0f);
277}
278
279auto histogram_bar_color(float ms) -> ImU32
280{
281 if(ms > 33.333f)
282 {
283 return IM_COL32(200, 50, 50, 200);
284 }
285 if(ms > 16.667f)
286 {
287 return IM_COL32(200, 160, 50, 200);
288 }
289 return IM_COL32(60, 150, 60, 200);
290}
291
293void draw_histogram_column(ImDrawList* draw_list,
294 float x0,
295 float x1,
296 float bottom_y,
297 float ms,
298 float cpu_ratio,
299 float scale_max,
300 bool draw_cpu_split,
301 bool draw_outline)
302{
303 if(ms <= 0.0f || scale_max <= 0.0f)
304 {
305 return;
306 }
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);
311 // Full bar height = frame wall time. Base fill must contrast with the chart background
312 // (30,30,30) so wait time is visible; the old wait color matched the bg and looked empty.
313 constexpr ImU32 hist_wait_fill = IM_COL32(72, 72, 88, 255);
314 if(draw_cpu_split)
315 {
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)
319 {
320 draw_list->AddRectFilled(ImVec2(x0, bottom_y - cpu_h), ImVec2(x1, bottom_y), cpu_col);
321 }
322 }
323 else
324 {
325 draw_list->AddRectFilled(ImVec2(x0, y_top), ImVec2(x1, bottom_y), cpu_col);
326 }
327 if(draw_outline && (x1 - x0) >= 2.0f)
328 {
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);
331 }
332}
333
334void render_histogram_bars(ImDrawList* draw_list,
335 performance_profiler* profiler,
336 int32_t first_frame,
337 int32_t last_frame,
338 ImVec2 canvas_pos,
339 float bottom_y,
340 float bar_width,
341 float hist_start,
342 float entry_w,
343 float scale_max)
344{
345 draw_list->PushClipRect(canvas_pos,
346 ImVec2(canvas_pos.x + bar_width, canvas_pos.y + frame_bar_height), true);
347
348 // When many frames share a pixel, max-pool into one column so spikes stay visible
349 // without emitting thousands of overlapping ImGui primitives.
350 constexpr float min_column_px = 1.0f;
351 const bool use_pixel_buckets = entry_w < min_column_px;
352 // Always show busy vs wait (matches Frame Loop). Skip outlines when columns are thin.
353 constexpr bool draw_cpu_split = true;
354 const bool draw_outline = entry_w >= 4.0f;
355
356 if(use_pixel_buckets && entry_w > 0.0f)
357 {
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)
360 {
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);
365 if(i1 < i0)
366 {
367 continue;
368 }
369 float peak_ms = 0.0f;
370 float peak_cpu = 1.0f;
371 for(int32_t i = i0; i <= i1; ++i)
372 {
373 const auto* snap = profiler->get_frame_snapshot(static_cast<uint32_t>(i));
374 if(snap == nullptr || snap->frame_wall_ms <= 0.0f)
375 {
376 continue;
377 }
378 if(snap->frame_wall_ms > peak_ms)
379 {
380 peak_ms = snap->frame_wall_ms;
381 peak_cpu = snap->frame_cpu_ratio;
382 }
383 }
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);
387 }
388 }
389 else
390 {
391 for(int32_t i = first_frame; i <= last_frame; ++i)
392 {
393 const auto* snap = profiler->get_frame_snapshot(static_cast<uint32_t>(i));
394 if(snap == nullptr || snap->frame_wall_ms <= 0.0f)
395 {
396 continue;
397 }
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,
401 x0,
402 x1,
403 bottom_y,
404 snap->frame_wall_ms,
405 snap->frame_cpu_ratio,
406 scale_max,
407 draw_cpu_split,
408 draw_outline);
409 }
410 }
411
412 draw_list->PopClipRect();
413}
414
415void render_histogram_guides(ImDrawList* draw_list,
416 ImVec2 canvas_pos,
417 float bar_width,
418 float bottom_y,
419 float scale_max)
420{
421 constexpr float target_30fps_ms = 33.333f;
422
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)");
429
430 if(target_30fps_ms < scale_max)
431 {
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)");
438 }
439}
440
442void render_memory_histogram_guides(ImDrawList* draw_list,
443 ImVec2 row_top_left,
444 float bar_width,
445 float scale_max_mb)
446{
447 if(scale_max_mb <= 0.001f)
448 {
449 return;
450 }
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);
455
456 auto bytes_for_mb_frac = [](float mb, float frac) -> uint64_t
457 {
458 const double b = static_cast<double>(mb) * static_cast<double>(megabyte_divisor) * static_cast<double>(frac);
459 if(b <= 0.0)
460 {
461 return 0u;
462 }
463 return static_cast<uint64_t>(b);
464 };
465
466 // Mid-scale reference (50% of current vertical max).
467 {
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());
473 }
474
475 // Top of plot = max of scale (bars map to this row height).
476 {
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,
483 label.c_str());
484 }
485}
486
487void render_memory_mb_row(ImDrawList* draw_list,
488 performance_profiler* profiler,
489 int32_t first_frame,
490 int32_t last_frame,
491 ImVec2 row_top_left,
492 float bar_width,
493 float hist_start,
494 float entry_w,
495 float scale_max_mb,
496 memory_histogram_metric metric,
497 ImU32 fill_col)
498{
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),
503 true);
504
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;
508
509 auto draw_mem_col = [&](float x0, float x1, float mb)
510 {
511 const float h_frac =
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)
517 {
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);
520 }
521 };
522
523 if(use_pixel_buckets)
524 {
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)
527 {
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);
532 if(i1 < i0)
533 {
534 continue;
535 }
536 float peak_mb = 0.0f;
537 for(int32_t i = i0; i <= i1; ++i)
538 {
539 const auto* snap = profiler->get_frame_snapshot(static_cast<uint32_t>(i));
540 if(snap != nullptr)
541 {
542 peak_mb = std::max(peak_mb, memory_mb_from_snapshot(snap, metric));
543 }
544 }
545 draw_mem_col(row_top_left.x + static_cast<float>(col),
546 row_top_left.x + static_cast<float>(col + 1),
547 peak_mb);
548 }
549 }
550 else
551 {
552 for(int32_t i = first_frame; i <= last_frame; ++i)
553 {
554 const auto* snap = profiler->get_frame_snapshot(static_cast<uint32_t>(i));
555 if(snap == nullptr)
556 {
557 continue;
558 }
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));
562 }
563 }
564
565 draw_list->PopClipRect();
566}
567
568void render_live_sample_row(ImDrawList* draw_list,
569 ImVec2 row_top_left,
570 float bar_width,
571 const sample_data& samples,
572 float scale_max,
573 ImU32 fill_col,
574 bool is_frame_ms_row,
575 const sample_data* busy_samples = nullptr)
576{
577 const int n = static_cast<int>(sample_data::num_samples);
578 if(n <= 0 || scale_max <= 0.0f)
579 {
580 return;
581 }
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);
585
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)),
589 true);
590
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);
593
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;
601
602 auto busy_ratio_at = [&](int sample_col, float wall_ms) -> float
603 {
604 if(!draw_busy_split || wall_ms <= 0.001f)
605 {
606 return 1.0f;
607 }
608 const int bidx = (busy_offset + sample_col) % n;
609 return std::clamp(busy_vals[bidx] / wall_ms, 0.0f, 1.0f);
610 };
611
612 if(use_pixel_buckets)
613 {
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)
617 {
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));
621 float peak = 0.0f;
622 float peak_ratio = 1.0f;
623 for(int s = s0; s <= s1; ++s)
624 {
625 const int idx = (offset + s) % n;
626 const float v = vals[idx];
627 if(v > peak)
628 {
629 peak = v;
630 peak_ratio = busy_ratio_at(s, v);
631 }
632 }
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);
635 if(is_frame_ms_row)
636 {
637 draw_histogram_column(draw_list, x0, x1, bottom_y, peak, peak_ratio, scale_max, draw_busy_split,
638 false);
639 }
640 else
641 {
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);
646 }
647 }
648 }
649 else
650 {
651 for(int col = 0; col < n; ++col)
652 {
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;
657 if(is_frame_ms_row)
658 {
659 draw_histogram_column(draw_list,
660 x0,
661 x1,
662 bottom_y,
663 v,
664 busy_ratio_at(col, v),
665 scale_max,
666 draw_busy_split,
667 draw_outline);
668 }
669 else
670 {
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);
675 if(draw_outline)
676 {
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);
679 }
680 }
681 }
682 }
683
684 draw_list->PopClipRect();
685}
686
687void render_histogram_cursor(ImDrawList* draw_list,
688 float plot_top_y,
689 float bottom_y,
690 float bar_width,
691 int32_t selected_idx,
692 float hist_start,
693 float entry_w,
694 float canvas_x)
695{
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)
698 {
699 return;
700 }
701
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);
705
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));
713}
714
715} // namespace
716
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,
723 uint16_t thread_idx,
724 uint32_t event_idx)
725{
726 const event_lane_geom g = compute_event_geom(lc, ev);
727 if(!g.ok)
728 {
729 return;
730 }
731
732 const float x0 = g.x0;
733 const float y0 = g.y0;
734 const float x1 = g.x1;
735 const float y1 = g.y1;
736
737 const bool is_scope_selected =
738 panel != nullptr && panel->is_timeline_scope_selected(hist_frame_idx, thread_idx, event_idx);
739
740 if(panel != nullptr && hist_frame_idx != UINT32_MAX &&
741 ImGui::IsMouseClicked(ImGuiMouseButton_Left) && ImGui::IsWindowHovered(ImGuiHoveredFlags_None))
742 {
743 const ImVec2 mouse = ImGui::GetMousePos();
744 if(mouse.x >= x0 && mouse.x <= x1 && mouse.y >= y0 && mouse.y <= y1)
745 {
746 panel->set_timeline_scope_selection(hist_frame_idx, thread_idx, event_idx, ev.name());
747 }
748 }
749
750 ImDrawList* draw_list = ImGui::GetWindowDrawList();
751
752 ImU32 cpu_color = color_from_hash(ev.color_hash);
753 if(!is_reference_frame)
754 {
755 cpu_color = dim_color(cpu_color);
756 }
757
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;
761
762 draw_list->AddRectFilled(ImVec2(x0, y0), ImVec2(cpu_x1, y1), cpu_color);
763
764 if(cpu_ratio < 0.99f)
765 {
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);
768 }
769
770 draw_list->AddRect(ImVec2(x0, y0), ImVec2(x1, y1), IM_COL32(0, 0, 0, 80));
771
772 if(is_scope_selected)
773 {
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);
776 }
777
778 const float rect_h = y1 - y0;
779 constexpr float min_text_width_px = 8.0f;
780 if(rect_w > min_text_width_px)
781 {
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);
789
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;
792
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();
796 }
797
798 const ImVec2 mouse = ImGui::GetMousePos();
799 if(mouse.x >= x0 && mouse.x <= x1 && mouse.y >= y0 && mouse.y <= y1)
800 {
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);
804
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)
811 {
812 ImGui::Text("Idle: %s (%.0f%%)", format_time(wait_ms).c_str(),
813 (1.0f - cpu_ratio) * 100.0f);
814 }
815 ImGui::Text("Depth: %d", ev.depth);
816 ImGui::Text("Thread: %s", thread_name.c_str());
817 ImGui::EndTooltip();
818 }
819}
820
821void profiler_timeline_panel::validate_timeline_scope_selection(uint32_t frame_count)
822{
823 if(!has_timeline_scope_selection_)
824 {
825 return;
826 }
827 if(frame_count == 0 || selected_scope_hist_frame_ >= frame_count)
828 {
829 has_timeline_scope_selection_ = false;
830 selected_scope_label_.clear();
831 return;
832 }
833
834 auto* profiler = get_app_profiler();
835 const frame_snapshot* snap = profiler->get_frame_snapshot(selected_scope_hist_frame_);
836 if(!snap)
837 {
838 has_timeline_scope_selection_ = false;
839 selected_scope_label_.clear();
840 return;
841 }
842
843 for(const auto& ts : snap->threads)
844 {
845 if(ts.thread_index != selected_scope_thread_index_)
846 {
847 continue;
848 }
849 if(selected_scope_event_index_ >= ts.events.size())
850 {
851 has_timeline_scope_selection_ = false;
852 selected_scope_label_.clear();
853 }
854 return;
855 }
856
857 has_timeline_scope_selection_ = false;
858 selected_scope_label_.clear();
859}
860
861profiler_timeline_panel::profiler_timeline_panel(imgui_panels* parent, const char* name)
862 : name_(name)
863 , parent_(parent)
864{
865}
866
868{
869 if(show_request_)
870 {
871 show_request_ = false;
872 show_ = true;
873 ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size * 0.5f, ImGuiCond_Once);
874 }
875
876 if(!show_)
877 {
878 return;
879 }
880
881 if(ImGui::Begin(name, &show_))
882 {
883 draw_ui(ctx);
884 }
885 ImGui::End();
886}
887
889{
890 show_request_ = s;
891}
892
893void profiler_timeline_panel::draw_ui(rtti::context& ctx)
894{
895 draw_recording_toolbar();
896
897 ImGui::Separator();
898
899 draw_frame_selector_bar();
900
901 ImGui::Separator();
902
903 if(has_timeline_scope_selection_ && !selected_scope_label_.empty())
904 {
905 ImGui::TextDisabled("Selected:");
906 ImGui::SameLine();
907 ImGui::TextUnformatted(selected_scope_label_.c_str());
908 }
909
910 draw_timeline();
911
912 ImGui::Separator();
913
914 draw_aggregate_section();
915
916 draw_profiler_bottom_sections(ctx);
917}
918
919// ============================================================================
920// Recording toolbar
921// ============================================================================
922
923void profiler_timeline_panel::draw_recording_toolbar()
924{
925 auto* profiler = get_app_profiler();
926 const bool is_recording = profiler->get_recording_state() == recording_state::recording;
927
928 if(is_recording)
929 {
930 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f));
931 }
932 if(ImGui::Button(ICON_MDI_RECORD " Record"))
933 {
934 if(is_recording)
935 {
936 profiler->set_recording_state(recording_state::paused);
937 auto_follow_ = false;
938 const uint32_t count = profiler->get_frame_count();
939 if(count > 0)
940 {
941 selected_frame_ = static_cast<int32_t>(count - 1);
942 last_centered_frame_ = -2;
943 }
944 }
945 else
946 {
947 profiler->set_recording_state(recording_state::recording);
948 auto_follow_ = true;
949 last_centered_frame_ = -2;
950 }
951 }
952 if(is_recording)
953 {
954 ImGui::PopStyleColor();
955 }
956
957 ImGui::SameLine();
958 ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
959 ImGui::SameLine();
960
961 if(ImGui::Button(ICON_MDI_DELETE " Clear"))
962 {
963 profiler->clear_history();
964 selected_frame_ = -1;
965 last_centered_frame_ = -2;
966 has_timeline_scope_selection_ = false;
967 selected_scope_label_.clear();
968 }
969
970 uint32_t frame_count = profiler->get_frame_count();
971
972 ImGui::SameLine();
973 ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
974 ImGui::SameLine();
975
976 if(frame_count > 0)
977 {
978 int32_t display_idx = auto_follow_ ? static_cast<int32_t>(frame_count - 1) : selected_frame_;
979 if(display_idx >= 0)
980 {
981 ImGui::Text("Frame %d / %u", display_idx + 1, frame_count);
982 }
983 else
984 {
985 ImGui::Text("%u frames", frame_count);
986 }
987 }
988 else
989 {
990 ImGui::TextDisabled("No frames captured");
991 }
992
993 ImGui::SameLine();
994 ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
995 ImGui::SameLine();
996
997 if(ImGui::Button(ICON_MDI_FIT_TO_PAGE " Fit"))
998 {
999 last_centered_frame_ = -2;
1000 view_duration_ns_ = 20'000'000.0;
1001 hist_start_ = 0.0f;
1002 hist_range_ = 0.0f;
1003 }
1004
1005 ImGui::SameLine();
1006 ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
1007 ImGui::SameLine();
1008
1009 {
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; // default 256
1014 for(int i = 0; i < static_cast<int>(IM_ARRAYSIZE(history_presets)); ++i)
1015 {
1016 if(history_presets[i] == current_cap)
1017 {
1018 current_idx = i;
1019 break;
1020 }
1021 }
1022 ImGui::SetNextItemWidth(80.0f);
1023 if(ImGui::Combo("History", &current_idx, history_labels, IM_ARRAYSIZE(history_labels)))
1024 {
1025 profiler->set_max_frame_history(history_presets[current_idx]);
1026 if(selected_frame_ >= 0)
1027 {
1028 const uint32_t count = profiler->get_frame_count();
1029 if(count == 0)
1030 {
1031 selected_frame_ = -1;
1032 }
1033 else
1034 {
1035 selected_frame_ = std::min(selected_frame_, static_cast<int32_t>(count) - 1);
1036 }
1037 }
1038 last_centered_frame_ = -2;
1039 }
1040 ImGui::SetItemTooltipEx("Max captured frames. Lower values reduce histogram cost.");
1041 }
1042
1043 ImGui::SameLine();
1044 ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
1045 ImGui::SameLine();
1046
1047 double visible_ms = view_duration_ns_ / 1'000'000.0;
1048 if(visible_ms >= 1.0)
1049 {
1050 ImGui::Text("%.1f ms visible", visible_ms);
1051 }
1052 else
1053 {
1054 ImGui::Text("%.0f us visible", visible_ms * 1000.0);
1055 }
1056}
1057
1058// ============================================================================
1059// Frame selector histogram
1060// ============================================================================
1061
1062auto profiler_timeline_panel::histogram_stack_height() const -> float
1063{
1064 float h = frame_bar_height;
1065 if(show_histogram_managed_heap_)
1066 {
1067 h += memory_hist_row_height;
1068 }
1069 if(show_histogram_gpu_memory_)
1070 {
1071 h += memory_hist_row_height;
1072 }
1073 if(show_histogram_process_rss_)
1074 {
1075 h += memory_hist_row_height;
1076 }
1077 return h;
1078}
1079
1080void profiler_timeline_panel::draw_profiler_bottom_sections(rtti::context& ctx)
1081{
1082 if(parent_ == nullptr)
1083 {
1084 return;
1085 }
1086 ImGui::PushID("profiler_bottom");
1087 if(ImGui::CollapsingHeader(ICON_MDI_CHIP "\tRender Passes"))
1088 {
1091 ImGui::PopFont();
1092 }
1094 if(ctx.has<settings>())
1095 {
1096 profiler_draw_eviction_section(ctx.get<settings>().graphics.eviction);
1097 }
1098 ImGui::PopID();
1099}
1100
1101void profiler_timeline_panel::draw_live_histogram_stack(float bar_width)
1102{
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;
1109
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");
1116
1117 float row_y = frame_bottom;
1118 if(show_histogram_managed_heap_)
1119 {
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;
1125 }
1126 if(show_histogram_gpu_memory_)
1127 {
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;
1133 }
1134 if(show_histogram_process_rss_)
1135 {
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);
1140 }
1141
1142 ImGui::Dummy(ImVec2(bar_width, histogram_stack_height()));
1143}
1144
1145void profiler_timeline_panel::draw_frame_selector_bar()
1146{
1147 auto* profiler = get_app_profiler();
1148
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)
1153 {
1154 frame_ms = static_cast<float>(frame_end - frame_start) / 1'000'000.0f;
1155 }
1156 frame_time_history_.push_sample(frame_ms);
1157
1158 const uint32_t frame_count = profiler->get_frame_count();
1159 float frame_busy_ms = frame_ms;
1160 if(frame_count > 0)
1161 {
1162 const frame_snapshot* latest = profiler->get_frame_snapshot(frame_count - 1);
1163 if(latest != nullptr)
1164 {
1165 frame_busy_ms = latest->frame_busy_ms;
1166 }
1167 }
1168 frame_busy_ms_history_.push_sample(frame_busy_ms);
1169
1170 ImGui::Checkbox("Managed heap", &show_histogram_managed_heap_);
1171 ImGui::SameLine();
1172 ImGui::Checkbox("GPU memory", &show_histogram_gpu_memory_);
1173 ImGui::SameLine();
1174 ImGui::Checkbox("Process RSS", &show_histogram_process_rss_);
1175
1176 // Use captured snapshots whenever available (recording and paused) so bar count
1177 // matches History capacity. Live rolling samples are only for the empty pre-record state.
1178 if(frame_count == 0)
1179 {
1180 const float cpu_mb = static_cast<float>(dotnet::gc_get_used_size()) / megabyte_divisor;
1181 cpu_heap_mb_history_.push_sample(cpu_mb);
1182 float gpu_mb = 0.0f;
1183 auto* stats = gfx::get_stats();
1184 if(stats)
1185 {
1186 gpu_mb = static_cast<float>(stats->gpuMemoryUsed) / megabyte_divisor;
1187 }
1188 gpu_memory_mb_history_.push_sample(gpu_mb);
1189 const float rss_mb =
1190 static_cast<float>(platform::get_process_resident_set_bytes()) / megabyte_divisor;
1191 process_rss_mb_history_.push_sample(rss_mb);
1192 auto region = ImGui::GetContentRegionAvail();
1193 draw_live_histogram_stack(region.x);
1194 return;
1195 }
1196
1197 auto region = ImGui::GetContentRegionAvail();
1198 float bar_width = region.x;
1199 if(bar_width < 10.0f)
1200 {
1201 return;
1202 }
1203
1204 draw_frame_histogram(profiler, frame_count, bar_width);
1205}
1206
1207void profiler_timeline_panel::draw_frame_histogram(performance_profiler* profiler,
1208 uint32_t frame_count,
1209 float bar_width)
1210{
1211 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1212 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
1213 const float frame_bottom_y = canvas_pos.y + frame_bar_height;
1214
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));
1218
1219 if(auto_follow_)
1220 {
1221 eff_start = std::max(0.0f, fc_f - eff_range);
1222 hist_start_ = eff_start;
1223 }
1224
1225 float entry_w = bar_width / eff_range;
1226
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)));
1230
1231 draw_list->AddRectFilled(canvas_pos,
1232 ImVec2(canvas_pos.x + bar_width, frame_bottom_y),
1233 IM_COL32(30, 30, 30, 255));
1234
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)
1240 {
1241 const auto* snap = profiler->get_frame_snapshot(static_cast<uint32_t>(i));
1242 if(snap == nullptr)
1243 {
1244 continue;
1245 }
1246 max_frame_ms = std::max(max_frame_ms, snap->frame_wall_ms);
1247 if(show_histogram_managed_heap_)
1248 {
1249 max_cpu_mb =
1250 std::max(max_cpu_mb, static_cast<float>(snap->cpu_heap_used_bytes) / megabyte_divisor);
1251 }
1252 if(show_histogram_gpu_memory_)
1253 {
1254 max_gpu_mb =
1255 std::max(max_gpu_mb, static_cast<float>(snap->gpu_memory_used_bytes) / megabyte_divisor);
1256 }
1257 if(show_histogram_process_rss_)
1258 {
1259 max_rss_mb =
1260 std::max(max_rss_mb, static_cast<float>(snap->process_resident_bytes) / megabyte_divisor);
1261 }
1262 }
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;
1267
1268 int32_t effective_selected = auto_follow_
1269 ? static_cast<int32_t>(frame_count) - 1
1270 : selected_frame_;
1271 effective_selected = std::clamp(effective_selected, 0, static_cast<int32_t>(frame_count) - 1);
1272
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");
1276
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);
1280
1281 float mem_row_y = frame_bottom_y;
1282 if(show_histogram_managed_heap_)
1283 {
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;
1294 }
1295 if(show_histogram_gpu_memory_)
1296 {
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;
1307 }
1308 if(show_histogram_process_rss_)
1309 {
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);
1319 }
1320
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);
1325
1326 ImGui::InvisibleButton("##frame_histogram", ImVec2(bar_width, this->histogram_stack_height()));
1327
1328 handle_histogram_input(profiler, frame_count, canvas_pos, bar_width, eff_start, eff_range);
1329}
1330
1331void profiler_timeline_panel::handle_histogram_input(performance_profiler* profiler,
1332 uint32_t frame_count,
1333 ImVec2 canvas_pos,
1334 float bar_width,
1335 float eff_start,
1336 float eff_range)
1337{
1338 float entry_w = bar_width / eff_range;
1339
1340 handle_histogram_zoom_pan(frame_count, canvas_pos, bar_width, eff_start, eff_range);
1341
1342 bool hovered = ImGui::IsItemHovered();
1343 bool active = ImGui::IsItemActive();
1344
1345 if(hovered || active)
1346 {
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);
1350
1351 if(hovered)
1352 {
1353 const auto* hsnap = profiler->get_frame_snapshot(static_cast<uint32_t>(hover_idx));
1354 if(hsnap && hsnap->frame_wall_ms > 0.0f)
1355 {
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;
1360
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)
1367 {
1368 ImGui::Text("Wait: %.2f ms (%.0f%%)", wait_ms, 100.0f - busy_pct);
1369 }
1370 const auto heap_pretty = format_bytes(
1371 static_cast<std::uint64_t>(std::max<int64_t>(0, hsnap->cpu_heap_used_bytes)), 0);
1372 const auto gpu_pretty = format_bytes(
1373 static_cast<std::uint64_t>(std::max<int64_t>(0, hsnap->gpu_memory_used_bytes)), 0);
1374 const auto rss_pretty = format_bytes(
1375 static_cast<std::uint64_t>(std::max<int64_t>(0, hsnap->process_resident_bytes)), 0);
1376 if(show_histogram_managed_heap_)
1377 {
1378 ImGui::Text("Managed heap: %s", heap_pretty.c_str());
1379 }
1380 if(show_histogram_gpu_memory_)
1381 {
1382 ImGui::Text("GPU memory: %s", gpu_pretty.c_str());
1383 }
1384 if(show_histogram_process_rss_)
1385 {
1386 ImGui::Text("Process RSS: %s", rss_pretty.c_str());
1387 }
1388 ImGui::EndTooltip();
1389 }
1390 }
1391
1392 if(ImGui::IsMouseClicked(ImGuiMouseButton_Left) || is_dragging_cursor_)
1393 {
1394 selected_frame_ = hover_idx;
1395 auto_follow_ = false;
1396 last_centered_frame_ = -2;
1397 is_dragging_cursor_ = true;
1398 }
1399 }
1400
1401 if(is_dragging_cursor_ && !ImGui::IsMouseDown(ImGuiMouseButton_Left))
1402 {
1403 is_dragging_cursor_ = false;
1404 }
1405
1406 if(ImGui::IsItemFocused())
1407 {
1408 if(ImGui::IsKeyPressed(ImGuiKey_LeftArrow) && selected_frame_ > 0)
1409 {
1410 selected_frame_--;
1411 auto_follow_ = false;
1412 last_centered_frame_ = -2;
1413 }
1414 if(ImGui::IsKeyPressed(ImGuiKey_RightArrow) &&
1415 selected_frame_ < static_cast<int32_t>(frame_count) - 1)
1416 {
1417 selected_frame_++;
1418 auto_follow_ = false;
1419 last_centered_frame_ = -2;
1420 }
1421 }
1422}
1423
1424void profiler_timeline_panel::handle_histogram_zoom_pan(uint32_t frame_count,
1425 ImVec2 canvas_pos,
1426 float bar_width,
1427 float eff_start,
1428 float eff_range)
1429{
1430 if(!ImGui::IsItemHovered())
1431 {
1432 return;
1433 }
1434
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);
1438
1439 float wheel = ImGui::GetIO().MouseWheel;
1440 if(wheel != 0.0f)
1441 {
1442 if(ImGui::GetIO().KeyCtrl)
1443 {
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);
1447
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;
1451 }
1452 }
1453
1454 if(ImGui::IsMouseDragging(ImGuiMouseButton_Middle))
1455 {
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));
1459 }
1460}
1461
1462// ============================================================================
1463// Time ruler
1464// ============================================================================
1465
1466void profiler_timeline_panel::draw_time_ruler(double view_start_ns,
1467 double reference_ns,
1468 double ns_per_pixel,
1469 float ruler_width,
1470 ImVec2 canvas_pos)
1471{
1472 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1473
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));
1477
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;
1481
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};
1485
1486 double tick_ms = nice_intervals.back();
1487 for(double ni : nice_intervals)
1488 {
1489 if(ni >= raw_interval)
1490 {
1491 tick_ms = ni;
1492 break;
1493 }
1494 }
1495
1496 double tick_ns = tick_ms * 1'000'000.0;
1497
1498 double ref_offset = view_start_ns - reference_ns;
1499 double first_tick_offset = std::ceil(ref_offset / tick_ns) * tick_ns;
1500
1501 for(double offset = first_tick_offset; ; offset += tick_ns)
1502 {
1503 double abs_ns = reference_ns + offset;
1504 if(abs_ns > view_end_ns)
1505 {
1506 break;
1507 }
1508
1509 float x = canvas_pos.x + static_cast<float>((abs_ns - view_start_ns) / ns_per_pixel);
1510 if(x < canvas_pos.x)
1511 {
1512 continue;
1513 }
1514
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));
1518
1519 double label_ms = offset / 1'000'000.0;
1520 std::string label;
1521 if(tick_ms >= 1.0)
1522 {
1523 label = fmt::format("{:.0f}ms", label_ms);
1524 }
1525 else if(tick_ms >= 0.01)
1526 {
1527 label = fmt::format("{:.2f}ms", label_ms);
1528 }
1529 else
1530 {
1531 label = fmt::format("{:.3f}ms", label_ms);
1532 }
1533
1534 draw_list->AddText(ImVec2(x + 2.0f, canvas_pos.y + 2.0f),
1535 IM_COL32(200, 200, 200, 220),
1536 label.c_str());
1537 }
1538}
1539
1540// ============================================================================
1541// Timeline (multi-frame, shared time axis)
1542// ============================================================================
1543
1544void profiler_timeline_panel::draw_timeline()
1545{
1546 auto* profiler = get_app_profiler();
1547 uint32_t frame_count = profiler->get_frame_count();
1548 validate_timeline_scope_selection(frame_count);
1549 if(profiler->get_recording_state() == recording_state::recording)
1550 {
1551 ImGui::TextDisabled("Recording...");
1552 return;
1553 }
1554 if(frame_count == 0)
1555 {
1556 ImGui::TextDisabled("No frame data to display");
1557 return;
1558 }
1559
1560 int32_t frame_idx = auto_follow_
1561 ? static_cast<int32_t>(frame_count) - 1
1562 : selected_frame_;
1563 frame_idx = std::clamp(frame_idx, 0, static_cast<int32_t>(frame_count) - 1);
1564
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)
1568 {
1569 ImGui::TextDisabled("No valid frame data");
1570 return;
1571 }
1572
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_);
1577
1578 // -- View positioning ------------------------------------------------
1579 if(selection_changed)
1580 {
1581 // Grow the visible window to fit a longer frame; do not shrink for short ones
1582 // (preserves manual zoom-out and avoids fighting zoom-in every redraw).
1583 constexpr double frame_fit_padding = 1.1;
1584 if(sel_duration_ns > view_duration_ns_)
1585 {
1586 view_duration_ns_ = std::clamp(sel_duration_ns * frame_fit_padding,
1587 min_view_duration_ns,
1588 max_view_duration_ns);
1589 }
1590 last_centered_frame_ = frame_idx;
1591 }
1592 if(auto_follow_)
1593 {
1594 view_start_ns_ = sel_end - view_duration_ns_ * 0.85;
1595 }
1596 else if(selection_changed)
1597 {
1598 const double center = (sel_start + sel_end) / 2.0;
1599 view_start_ns_ = center - view_duration_ns_ / 2.0;
1600 }
1601
1602 // -- Layout ----------------------------------------------------------
1603 auto region = ImGui::GetContentRegionAvail();
1604 float lane_content_width = region.x - lane_header_width;
1605 if(lane_content_width < 10.0f)
1606 {
1607 return;
1608 }
1609
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_;
1612
1613 // -- Gather visible frames -------------------------------------------
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)
1619 {
1620 visible_hist_indices.push_back(hist_index_of(profiler, f));
1621 }
1622
1623 // -- Collect unique threads across visible frames --------------------
1624 std::vector<thread_entry> threads;
1625 collect_unique_threads(visible_frames, threads);
1626
1627 if(threads.empty())
1628 {
1629 ImGui::TextDisabled("No thread data");
1630 return;
1631 }
1632
1633 // -- Compute total height (must match layout below + spacing between lanes) --
1634 float total_height = ruler_height;
1635 for(const auto& t : threads)
1636 {
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);
1640 }
1641 if(threads.size() > 1)
1642 {
1643 total_height += thread_lane_spacing * static_cast<float>(threads.size() - 1);
1644 }
1645
1646 float timeline_height = std::min(region.y * 0.65f,
1647 std::max(total_height + 10.0f, 100.0f));
1648
1649 // -- Begin scrollable child ------------------------------------------
1650 ImGui::BeginChild("##timeline_scroll", ImVec2(0, timeline_height), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY,
1651 ImGuiWindowFlags_NoScrollWithMouse);
1652
1653 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1654 ImVec2 base_pos = ImGui::GetCursorScreenPos();
1655
1656 // Time ruler
1657 draw_time_ruler(view_start_ns_, sel_start, ns_per_pixel,
1658 lane_content_width,
1659 ImVec2(base_pos.x + lane_header_width, base_pos.y));
1660 ImGui::Dummy(ImVec2(0, ruler_height));
1661
1662 // -- Thread lanes ----------------------------------------------------
1663 for(size_t ti = 0; ti < threads.size(); ++ti)
1664 {
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);
1669
1670 ImGui::Text("%s", thread.name.c_str());
1671 ImGui::SameLine(lane_header_width);
1672
1673 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
1674
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));
1678
1679 lane_context lc{canvas_pos, lane_content_width, lane_height,
1680 view_start_ns_, view_end_ns, ns_per_pixel};
1681
1682 draw_list->PushClipRect(canvas_pos,
1683 ImVec2(canvas_pos.x + lane_content_width, canvas_pos.y + lane_height),
1684 true);
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();
1689
1690 ImGui::Dummy(ImVec2(0, lane_height));
1691 if(ti + 1 < threads.size())
1692 {
1693 ImGui::Dummy(ImVec2(0, thread_lane_spacing));
1694 }
1695 }
1696
1697 // Input while timeline child is active (correct scroll + zoom coordinates)
1698 ImGuiIO& io = ImGui::GetIO();
1699 if(ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup))
1700 {
1701 if(ImGui::IsMouseDragging(ImGuiMouseButton_Middle))
1702 {
1703 ImGui::SetScrollY(ImGui::GetScrollY() - io.MouseDelta.y);
1704 if(lane_content_width > 0)
1705 {
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;
1708 }
1709 }
1710
1711 const float wheel = io.MouseWheel;
1712 if(wheel != 0.0f)
1713 {
1714 if(io.KeyCtrl)
1715 {
1716 const ImGuiWindow* win = ImGui::GetCurrentWindowRead();
1717 const double graph_left_d =
1718 win != nullptr
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_;
1725
1726 view_duration_ns_ *= (1.0 - static_cast<double>(wheel) * 0.15);
1727 view_duration_ns_ =
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_;
1730 }
1731 else
1732 {
1733 ImGui::SetScrollY(ImGui::GetScrollY() - wheel * timeline_wheel_scroll_px);
1734 }
1735 }
1736 }
1737
1738 ImGui::EndChild();
1739
1740 if(ImGui::IsItemHovered())
1741 {
1742 if(ImGui::IsKeyPressed(ImGuiKey_Escape) && has_timeline_scope_selection_)
1743 {
1744 has_timeline_scope_selection_ = false;
1745 selected_scope_label_.clear();
1746 }
1747 }
1748}
1749
1750// ============================================================================
1751// Timeline helpers
1752// ============================================================================
1753
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)
1758{
1759 double view_end_ns = view_start_ns_ + view_duration_ns_;
1760 out.reserve(32);
1761
1762 for(uint32_t i = 0; i < frame_count; ++i)
1763 {
1764 const auto* snap = profiler->get_frame_snapshot(i);
1765 if(!snap || snap->frame_end_ns <= snap->frame_start_ns)
1766 {
1767 continue;
1768 }
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)
1772 {
1773 out.push_back(snap);
1774 }
1775 }
1776
1777 if(out.empty())
1778 {
1779 out.push_back(selected_snap);
1780 }
1781}
1782
1783void profiler_timeline_panel::collect_unique_threads(
1784 const std::vector<const frame_snapshot*>& frames,
1785 std::vector<thread_entry>& out)
1786{
1787 for(const auto* frame : frames)
1788 {
1789 for(const auto& ts : frame->threads)
1790 {
1791 uint16_t local_max = 0;
1792 for(const auto& ev : ts.events)
1793 {
1794 if(ev.end_ns > ev.start_ns)
1795 {
1796 local_max = std::max(local_max, ev.depth);
1797 }
1798 }
1799
1800 auto it = std::find_if(out.begin(), out.end(),
1801 [&](const thread_entry& e) -> bool { return e.index == ts.thread_index; });
1802
1803 if(it != out.end())
1804 {
1805 it->max_depth = std::max(it->max_depth, local_max);
1806 }
1807 else
1808 {
1809 out.push_back({ts.name, ts.thread_index, local_max});
1810 }
1811 }
1812 }
1813}
1814
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)
1822{
1823 for(size_t fi = 0; fi < visible_frames.size(); ++fi)
1824 {
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);
1829
1830 for(const auto& ts : frame->threads)
1831 {
1832 if(ts.thread_index != thread_index)
1833 {
1834 continue;
1835 }
1836
1837 for(uint32_t event_idx = 0; event_idx < ts.events.size(); ++event_idx)
1838 {
1839 const profile_event& ev = ts.events[event_idx];
1840 if(ev.end_ns > ev.start_ns)
1841 {
1842 timeline_render_event_block(lc, ev, is_reference_frame, thread_name, this, hidx,
1843 thread_index, event_idx);
1844 }
1845 }
1846 }
1847 }
1848}
1849
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)
1854{
1855 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1856
1857 for(const auto* frame : visible_frames)
1858 {
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);
1862
1863 if(bx > lc.canvas_pos.x && bx < lc.canvas_pos.x + lc.lane_content_width)
1864 {
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),
1870 line_col);
1871 }
1872 }
1873}
1874
1875// ============================================================================
1876// Aggregate data section
1877// ============================================================================
1878
1879void profiler_timeline_panel::draw_aggregate_section()
1880{
1881 if(!ImGui::CollapsingHeader(ICON_MDI_CLOCK_OUTLINE "\tAggregate"))
1882 {
1883 return;
1884 }
1885
1886 auto* profiler = get_app_profiler();
1887 const auto& data = profiler->get_per_frame_data_read();
1888
1889 if(data.empty())
1890 {
1891 ImGui::TextDisabled("No profiler scopes recorded yet.");
1892 return;
1893 }
1894
1895 ImGui::TextWrapped(
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).");
1898 ImGui::Spacing();
1899
1900 static int sort_mode = 0;
1901 ImGui::AlignTextToFramePadding();
1902 ImGui::TextUnformatted("Sort by");
1903 ImGui::SameLine();
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()));
1908
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)
1914 {
1915 rows.push_back(&e);
1916 }
1917
1918 const auto cmp_rows = [](entry_cptr a, entry_cptr b) -> bool
1919 {
1920 switch(sort_mode)
1921 {
1922 case 0:
1923 if(a->second.get_avg() != b->second.get_avg())
1924 {
1925 return a->second.get_avg() > b->second.get_avg();
1926 }
1927 break;
1928 case 1:
1929 if(a->second.get_max() != b->second.get_max())
1930 {
1931 return a->second.get_max() > b->second.get_max();
1932 }
1933 break;
1934 case 2:
1935 if(a->second.get_time_since_swap() != b->second.get_time_since_swap())
1936 {
1937 return a->second.get_time_since_swap() > b->second.get_time_since_swap();
1938 }
1939 break;
1940 default:
1941 break;
1942 }
1943 return a->first < b->first;
1944 };
1945 std::sort(rows.begin(), rows.end(), cmp_rows);
1946
1947 float scale_avg = 0.0001f;
1948 float scale_hist_max = 0.0001f;
1949 for(entry_cptr ep : rows)
1950 {
1951 scale_avg = std::max(scale_avg, ep->second.get_avg());
1952 scale_hist_max = std::max(scale_hist_max, ep->second.get_max());
1953 }
1954 const float spark_ymax = scale_hist_max * 1.05f;
1955
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)));
1959
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)))
1966 {
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();
1976
1977 const float bar_h = ImGui::GetTextLineHeight();
1978
1979 for(entry_cptr ep : rows)
1980 {
1981 const std::string& name = ep->first;
1982 const performance_profiler::per_frame_data& pfd = ep->second;
1983
1984 ImGui::TableNextRow();
1985 ImGui::PushID(name.c_str());
1986
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);
1991
1992
1993 ImGui::TableNextColumn();
1994 const sample_data& hist = pfd.get_history();
1995 ImGui::PlotLines("##spark",
1996 hist.get_values(),
1997 static_cast<int>(sample_data::num_samples),
1998 hist.get_offset(),
1999 nullptr,
2000 0.0f,
2001 spark_ymax,
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.",
2006 spark_ymax);
2007
2008
2009 ImGui::TableNextColumn();
2010 ImGui::TextUnformatted(name.c_str());
2011 ImGui::SetItemTooltipEx("%s", name.c_str());
2012
2013
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.");
2017
2018 ImGui::TableNextColumn();
2019 ImGui::Text("%.3f", pfd.get_avg());
2020
2021 ImGui::TableNextColumn();
2022 ImGui::Text("%.3f", pfd.get_max());
2023
2024 ImGui::TableNextColumn();
2025 ImGui::Text("%.3f", pfd.get_min());
2026
2027 ImGui::PopID();
2028 }
2029 ImGui::EndTable();
2030 }
2031 ImGui::PopFont();
2032}
2033
2034} // namespace unravel
entt::handle b
entt::handle a
auto gpu_profiler_enabled() -> bool &
Shared with profiler_timeline_panel for gfx view/encoder timing (Render Passes section).
Definition panel.h:75
void on_frame_ui_render(rtti::context &ctx, const char *name)
static constexpr uint32_t num_samples
Definition profiler.h:28
auto get_max() const -> float
Definition profiler.h:107
auto push_sample(float value) -> void
Definition profiler.h:48
float y
float x
uint32_t frame
Definition graphics.cpp:23
std::string name
Definition hub.cpp:33
#define ICON_MDI_CLOCK_OUTLINE
#define ICON_MDI_RECORD
#define ICON_MDI_CHIP
#define ICON_MDI_FIT_TO_PAGE
#define ICON_MDI_DELETE
std::vector< size_t > parent_
void PushFont(Font::Enum _font)
Definition imgui.cpp:646
Definition imgui.h:25
bgfx::Stats stats
Definition graphics.h:31
const stats * get_stats()
Definition graphics.cpp:450
auto get_process_resident_set_bytes() -> int64_t
Hash specialization for batch_key to enable use in std::unordered_map.
auto get_app_profiler() -> performance_profiler *
Definition profiler.cpp:99
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
float busy_ms
Definition profiler.cpp:23
auto has() const -> bool
Definition context.hpp:28
auto get() -> T &
Definition context.hpp:35