| 1 | // Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved. |
| 2 | // Use of this source code is governed by an MIT license |
| 3 | // that can be found in the LICENSE file. |
| 4 | module main |
| 5 | |
| 6 | import gg |
| 7 | import v2.profiler |
| 8 | |
| 9 | // draw_header renders the top stats bar |
| 10 | fn draw_header(mut app App) { |
| 11 | ctx := app.gg |
| 12 | w := f32(ctx.width) |
| 13 | |
| 14 | // Background |
| 15 | ctx.draw_rect_filled(0, 0, w, f32(app.header_height), header_bg) |
| 16 | |
| 17 | // Get stats |
| 18 | stats := app.cached_stats |
| 19 | |
| 20 | // Format and draw stats |
| 21 | heap_str := 'HEAP: ${profiler.format_bytes(stats.live_bytes)}' |
| 22 | peak_str := 'Peak: ${profiler.format_bytes(stats.peak_bytes)}' |
| 23 | allocs_str := '${stats.total_allocs} allocs' |
| 24 | frees_str := '${stats.total_frees} frees' |
| 25 | leaks_str := '${stats.leak_count} leaks' |
| 26 | |
| 27 | // Draw stats with spacing |
| 28 | mut x := f32(20) |
| 29 | y := f32(22) |
| 30 | |
| 31 | ctx.draw_text(int(x), int(y), heap_str, color: text_color, size: 20) |
| 32 | x += 180 |
| 33 | |
| 34 | ctx.draw_text(int(x), int(y), peak_str, color: text_color, size: 20) |
| 35 | x += 160 |
| 36 | |
| 37 | ctx.draw_text(int(x), int(y), allocs_str, color: new_alloc_color, size: 20) |
| 38 | x += 140 |
| 39 | |
| 40 | ctx.draw_text(int(x), int(y), frees_str, color: freed_color, size: 20) |
| 41 | x += 120 |
| 42 | |
| 43 | // Show leaks in red if there are any |
| 44 | leak_clr := if stats.leak_count > 0 { leak_color } else { text_color } |
| 45 | ctx.draw_text(int(x), int(y), leaks_str, color: leak_clr, size: 20) |
| 46 | |
| 47 | // Mode and frame counter on right |
| 48 | mode_str := if app.demo_mode { |
| 49 | 'DEMO' |
| 50 | } else if app.live_mode { |
| 51 | 'LIVE' |
| 52 | } else { |
| 53 | 'LOCAL' |
| 54 | } |
| 55 | ctx.draw_text(int(w) - 250, int(y), mode_str, color: gg.Color{100, 255, 100, 255}, size: 20) |
| 56 | |
| 57 | frame_str := 'Frame: ${stats.frame_count}' |
| 58 | ctx.draw_text(int(w) - 150, int(y), frame_str, color: text_color, size: 20) |
| 59 | |
| 60 | // Separator line |
| 61 | ctx.draw_line(0, f32(app.header_height), w, f32(app.header_height), grid_color) |
| 62 | } |
| 63 | |
| 64 | // draw_histogram renders the allocation histogram |
| 65 | fn draw_histogram(mut app App) { |
| 66 | ctx := app.gg |
| 67 | w := f32(ctx.width) |
| 68 | |
| 69 | // Histogram area |
| 70 | hist_y := f32(app.header_height) |
| 71 | hist_h := f32(app.histogram_height) |
| 72 | center_y := hist_y + hist_h / 2 |
| 73 | |
| 74 | // Background |
| 75 | ctx.draw_rect_filled(0, hist_y, w, hist_h, bg_color) |
| 76 | |
| 77 | // Center line |
| 78 | ctx.draw_line(0, center_y, w, center_y, grid_color) |
| 79 | |
| 80 | // Get frame data (always use cached data - it's populated by update_cache from snapshot or demo) |
| 81 | frames := app.cached_frames |
| 82 | if frames.len == 0 { |
| 83 | ctx.draw_text(int(w / 2) - 80, int(center_y) - 10, 'No frame data', |
| 84 | color: text_color |
| 85 | size: 18 |
| 86 | ) |
| 87 | return |
| 88 | } |
| 89 | |
| 90 | // Calculate max values for scaling |
| 91 | mut max_new := u64(1) |
| 92 | mut max_freed := u64(1) |
| 93 | for frame in frames { |
| 94 | if frame.new_bytes > max_new { |
| 95 | max_new = frame.new_bytes |
| 96 | } |
| 97 | if frame.freed_bytes > max_freed { |
| 98 | max_freed = frame.freed_bytes |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | // Bar dimensions |
| 103 | margin := f32(40) |
| 104 | bar_area_width := w - margin * 2 |
| 105 | bar_width := bar_area_width / f32(frames.len) |
| 106 | if bar_width < 1 { |
| 107 | return |
| 108 | } |
| 109 | bar_gap := bar_width * 0.1 |
| 110 | actual_bar_w := bar_width - bar_gap |
| 111 | |
| 112 | max_bar_height := (hist_h / 2) - 10 |
| 113 | |
| 114 | // Draw bars for each frame |
| 115 | for i, frame in frames { |
| 116 | x := margin + f32(i) * bar_width |
| 117 | |
| 118 | // New allocations (cyan, above center) |
| 119 | if frame.new_bytes > 0 { |
| 120 | new_height := f32(frame.new_bytes) / f32(max_new) * max_bar_height |
| 121 | bar_color := if i == app.selected_frame { selected_color } else { new_alloc_color } |
| 122 | ctx.draw_rect_filled(x, center_y - new_height, actual_bar_w, new_height, bar_color) |
| 123 | } |
| 124 | |
| 125 | // Freed allocations (purple, below center) |
| 126 | if frame.freed_bytes > 0 { |
| 127 | freed_height := f32(frame.freed_bytes) / f32(max_freed) * max_bar_height |
| 128 | bar_color := if i == app.selected_frame { |
| 129 | gg.Color{150, 130, 255, 200} |
| 130 | } else { |
| 131 | freed_color |
| 132 | } |
| 133 | ctx.draw_rect_filled(x, center_y, actual_bar_w, freed_height, bar_color) |
| 134 | } |
| 135 | |
| 136 | // Hover highlight |
| 137 | if i == app.hover_frame && i != app.selected_frame { |
| 138 | ctx.draw_rect_empty(x - 1, hist_y + 5, actual_bar_w + 2, hist_h - 10, timeline_cursor) |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | // Legend |
| 143 | ctx.draw_rect_filled(10, hist_y + 10, 12, 12, new_alloc_color) |
| 144 | ctx.draw_text(26, int(hist_y) + 8, 'New', color: text_color, size: 14) |
| 145 | |
| 146 | ctx.draw_rect_filled(70, hist_y + 10, 12, 12, freed_color) |
| 147 | ctx.draw_text(86, int(hist_y) + 8, 'Freed', color: text_color, size: 14) |
| 148 | |
| 149 | // Separator line |
| 150 | ctx.draw_line(0, hist_y + hist_h, w, hist_y + hist_h, grid_color) |
| 151 | } |
| 152 | |
| 153 | // draw_timeline renders the frame timeline with scrubber |
| 154 | fn draw_timeline(mut app App) { |
| 155 | ctx := app.gg |
| 156 | w := f32(ctx.width) |
| 157 | |
| 158 | // Timeline area |
| 159 | timeline_y := f32(app.header_height + app.histogram_height) |
| 160 | timeline_h := f32(app.timeline_height) |
| 161 | |
| 162 | // Background |
| 163 | ctx.draw_rect_filled(0, timeline_y, w, timeline_h, header_bg) |
| 164 | |
| 165 | frames := app.cached_frames |
| 166 | if frames.len == 0 { |
| 167 | return |
| 168 | } |
| 169 | |
| 170 | // Draw frame ticks |
| 171 | margin := f32(40) |
| 172 | tick_area_width := w - margin * 2 |
| 173 | tick_spacing := tick_area_width / f32(frames.len) |
| 174 | |
| 175 | // Draw tick marks |
| 176 | for i := 0; i < frames.len; i++ { |
| 177 | x := margin + f32(i) * tick_spacing |
| 178 | tick_height := if i % 10 == 0 { f32(15) } else { f32(8) } |
| 179 | ctx.draw_line(x, timeline_y + 20, x, timeline_y + 20 + tick_height, grid_color) |
| 180 | |
| 181 | // Frame number labels every 10 frames |
| 182 | if i % 10 == 0 && tick_spacing > 3 { |
| 183 | ctx.draw_text(int(x) - 10, int(timeline_y) + 40, '${i}', color: text_color, size: 12) |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | // Draw selected frame cursor |
| 188 | if app.selected_frame >= 0 && app.selected_frame < frames.len { |
| 189 | cursor_x := margin + f32(app.selected_frame) * tick_spacing |
| 190 | // Triangle cursor |
| 191 | draw_triangle_filled(ctx, cursor_x, timeline_y + 15, cursor_x - 8, timeline_y + 5, |
| 192 | |
| 193 | cursor_x + 8, timeline_y + 5, timeline_cursor) |
| 194 | |
| 195 | // Vertical line |
| 196 | ctx.draw_line(cursor_x, timeline_y + 15, cursor_x, timeline_y + timeline_h - 5, |
| 197 | timeline_cursor) |
| 198 | |
| 199 | // Frame info |
| 200 | frame := frames[app.selected_frame] |
| 201 | info := 'Frame ${app.selected_frame}: +${profiler.format_bytes(frame.new_bytes)} / -${profiler.format_bytes(frame.freed_bytes)}' |
| 202 | ctx.draw_text(int(w / 2) - 100, int(timeline_y) + 55, info, color: timeline_cursor, size: 16) |
| 203 | } |
| 204 | |
| 205 | // Separator line |
| 206 | ctx.draw_line(0, timeline_y + timeline_h, w, timeline_y + timeline_h, grid_color) |
| 207 | } |
| 208 | |
| 209 | // draw_controls renders the filter controls |
| 210 | fn draw_controls(mut app App) { |
| 211 | ctx := app.gg |
| 212 | w := f32(ctx.width) |
| 213 | |
| 214 | // Controls area |
| 215 | ctrl_y := f32(app.header_height + app.histogram_height + app.timeline_height) |
| 216 | ctrl_h := f32(app.controls_height) |
| 217 | |
| 218 | // Background |
| 219 | ctx.draw_rect_filled(0, ctrl_y, w, ctrl_h, bg_color) |
| 220 | |
| 221 | // Filter buttons |
| 222 | mut x := f32(20) |
| 223 | y := ctrl_y + 10 |
| 224 | |
| 225 | // Filter label |
| 226 | ctx.draw_text(int(x), int(y), 'Filter:', color: text_color, size: 16) |
| 227 | x += 60 |
| 228 | |
| 229 | // Filter mode buttons |
| 230 | filters := ['All', 'Leaks', 'Large', 'Frame'] |
| 231 | modes := [FilterMode.all, FilterMode.new_not_freed, FilterMode.large, FilterMode.current_frame] |
| 232 | |
| 233 | for i, label in filters { |
| 234 | btn_w := f32(60) |
| 235 | btn_h := f32(24) |
| 236 | |
| 237 | // Highlight active filter |
| 238 | btn_color := if app.filter_mode == modes[i] { selected_color } else { header_bg } |
| 239 | ctx.draw_rect_filled(x, y - 2, btn_w, btn_h, btn_color) |
| 240 | ctx.draw_rect_empty(x, y - 2, btn_w, btn_h, grid_color) |
| 241 | |
| 242 | ctx.draw_text(int(x) + 8, int(y), label, color: text_color, size: 14) |
| 243 | x += btn_w + 10 |
| 244 | } |
| 245 | |
| 246 | // Separator line |
| 247 | ctx.draw_line(0, ctrl_y + ctrl_h, w, ctrl_y + ctrl_h, grid_color) |
| 248 | } |
| 249 | |
| 250 | // draw_details renders the selection details panel |
| 251 | fn draw_details(mut app App) { |
| 252 | ctx := app.gg |
| 253 | w := f32(ctx.width) |
| 254 | h := f32(ctx.height) |
| 255 | |
| 256 | // Details area |
| 257 | details_y := f32(app.header_height + app.histogram_height + app.timeline_height + |
| 258 | app.controls_height) |
| 259 | details_h := h - details_y |
| 260 | |
| 261 | // Background |
| 262 | ctx.draw_rect_filled(0, details_y, w, details_h, header_bg) |
| 263 | |
| 264 | // Show selected allocation info or instructions |
| 265 | if app.selected_alloc >= 0 { |
| 266 | allocs := app.cached_allocs |
| 267 | if app.selected_alloc < allocs.len { |
| 268 | alloc := allocs[app.selected_alloc] |
| 269 | |
| 270 | // Format allocation info |
| 271 | ptr_str := 'Address: 0x${voidptr(alloc.ptr):p}' |
| 272 | size_str := 'Size: ${profiler.format_bytes(u64(alloc.size))} (${alloc.size} bytes)' |
| 273 | frame_str := 'Allocated: frame ${alloc.frame}' |
| 274 | status_str := if alloc.freed { |
| 275 | 'Status: FREED (frame ${alloc.free_frame})' |
| 276 | } else { |
| 277 | 'Status: LIVE (potential leak)' |
| 278 | } |
| 279 | |
| 280 | status_color := if alloc.freed { freed_color } else { leak_color } |
| 281 | |
| 282 | mut y := details_y + 15 |
| 283 | ctx.draw_text(20, int(y), ptr_str, color: text_color, size: 16) |
| 284 | |
| 285 | ctx.draw_text(250, int(y), size_str, color: text_color, size: 16) |
| 286 | |
| 287 | ctx.draw_text(500, int(y), frame_str, color: text_color, size: 16) |
| 288 | |
| 289 | ctx.draw_text(700, int(y), status_str, color: status_color, size: 16) |
| 290 | |
| 291 | // Source location if available |
| 292 | if alloc.file.len > 0 { |
| 293 | y += 25 |
| 294 | loc_str := 'Source: ${alloc.file}:${alloc.line}' |
| 295 | ctx.draw_text(20, int(y), loc_str, color: new_alloc_color, size: 16) |
| 296 | } |
| 297 | } |
| 298 | } else { |
| 299 | // Instructions |
| 300 | ctx.draw_text(20, int(details_y) + 20, |
| 301 | 'Click on a histogram bar to select a frame, or use keyboard arrows to navigate', |
| 302 | color: text_color |
| 303 | size: 16 |
| 304 | ) |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | // draw_frame is the main frame callback |
| 309 | pub fn draw_frame(mut app App) { |
| 310 | if app.gg == unsafe { nil } { |
| 311 | return |
| 312 | } |
| 313 | app.gg.begin() |
| 314 | |
| 315 | // Update data |
| 316 | if app.demo_mode { |
| 317 | update_demo_data(mut app) |
| 318 | } else { |
| 319 | app.update_cache() |
| 320 | } |
| 321 | |
| 322 | // Draw all sections |
| 323 | draw_header(mut app) |
| 324 | draw_histogram(mut app) |
| 325 | draw_timeline(mut app) |
| 326 | draw_controls(mut app) |
| 327 | draw_details(mut app) |
| 328 | |
| 329 | // Draw status message if in live mode with no data |
| 330 | if app.live_mode && app.cached_frames.len == 0 { |
| 331 | ctx := app.gg |
| 332 | w := f32(ctx.width) |
| 333 | h := f32(ctx.height) |
| 334 | ctx.draw_text(int(w / 2) - 200, int(h / 2), app.status_msg, |
| 335 | color: text_color |
| 336 | size: 20 |
| 337 | ) |
| 338 | } |
| 339 | |
| 340 | app.gg.end() |
| 341 | } |
| 342 | |
| 343 | // Helper: draw a filled triangle |
| 344 | fn draw_triangle_filled(ctx &gg.Context, x1 f32, y1 f32, x2 f32, y2 f32, x3 f32, y3 f32, c gg.Color) { |
| 345 | ctx.draw_convex_poly([x1, y1, x2, y2, x3, y3], c) |
| 346 | } |
| 347 | |