347 lines
9.4 KiB
V
347 lines
9.4 KiB
V
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
|
|
// Use of this source code is governed by an MIT license
|
|
// that can be found in the LICENSE file.
|
|
module main
|
|
|
|
import gg
|
|
import v2.profiler
|
|
|
|
// draw_header renders the top stats bar
|
|
fn draw_header(mut app App) {
|
|
ctx := app.gg
|
|
w := f32(ctx.width)
|
|
|
|
// Background
|
|
ctx.draw_rect_filled(0, 0, w, f32(app.header_height), header_bg)
|
|
|
|
// Get stats
|
|
stats := app.cached_stats
|
|
|
|
// Format and draw stats
|
|
heap_str := 'HEAP: ${profiler.format_bytes(stats.live_bytes)}'
|
|
peak_str := 'Peak: ${profiler.format_bytes(stats.peak_bytes)}'
|
|
allocs_str := '${stats.total_allocs} allocs'
|
|
frees_str := '${stats.total_frees} frees'
|
|
leaks_str := '${stats.leak_count} leaks'
|
|
|
|
// Draw stats with spacing
|
|
mut x := f32(20)
|
|
y := f32(22)
|
|
|
|
ctx.draw_text(int(x), int(y), heap_str, color: text_color, size: 20)
|
|
x += 180
|
|
|
|
ctx.draw_text(int(x), int(y), peak_str, color: text_color, size: 20)
|
|
x += 160
|
|
|
|
ctx.draw_text(int(x), int(y), allocs_str, color: new_alloc_color, size: 20)
|
|
x += 140
|
|
|
|
ctx.draw_text(int(x), int(y), frees_str, color: freed_color, size: 20)
|
|
x += 120
|
|
|
|
// Show leaks in red if there are any
|
|
leak_clr := if stats.leak_count > 0 { leak_color } else { text_color }
|
|
ctx.draw_text(int(x), int(y), leaks_str, color: leak_clr, size: 20)
|
|
|
|
// Mode and frame counter on right
|
|
mode_str := if app.demo_mode {
|
|
'DEMO'
|
|
} else if app.live_mode {
|
|
'LIVE'
|
|
} else {
|
|
'LOCAL'
|
|
}
|
|
ctx.draw_text(int(w) - 250, int(y), mode_str, color: gg.Color{100, 255, 100, 255}, size: 20)
|
|
|
|
frame_str := 'Frame: ${stats.frame_count}'
|
|
ctx.draw_text(int(w) - 150, int(y), frame_str, color: text_color, size: 20)
|
|
|
|
// Separator line
|
|
ctx.draw_line(0, f32(app.header_height), w, f32(app.header_height), grid_color)
|
|
}
|
|
|
|
// draw_histogram renders the allocation histogram
|
|
fn draw_histogram(mut app App) {
|
|
ctx := app.gg
|
|
w := f32(ctx.width)
|
|
|
|
// Histogram area
|
|
hist_y := f32(app.header_height)
|
|
hist_h := f32(app.histogram_height)
|
|
center_y := hist_y + hist_h / 2
|
|
|
|
// Background
|
|
ctx.draw_rect_filled(0, hist_y, w, hist_h, bg_color)
|
|
|
|
// Center line
|
|
ctx.draw_line(0, center_y, w, center_y, grid_color)
|
|
|
|
// Get frame data (always use cached data - it's populated by update_cache from snapshot or demo)
|
|
frames := app.cached_frames
|
|
if frames.len == 0 {
|
|
ctx.draw_text(int(w / 2) - 80, int(center_y) - 10, 'No frame data',
|
|
color: text_color
|
|
size: 18
|
|
)
|
|
return
|
|
}
|
|
|
|
// Calculate max values for scaling
|
|
mut max_new := u64(1)
|
|
mut max_freed := u64(1)
|
|
for frame in frames {
|
|
if frame.new_bytes > max_new {
|
|
max_new = frame.new_bytes
|
|
}
|
|
if frame.freed_bytes > max_freed {
|
|
max_freed = frame.freed_bytes
|
|
}
|
|
}
|
|
|
|
// Bar dimensions
|
|
margin := f32(40)
|
|
bar_area_width := w - margin * 2
|
|
bar_width := bar_area_width / f32(frames.len)
|
|
if bar_width < 1 {
|
|
return
|
|
}
|
|
bar_gap := bar_width * 0.1
|
|
actual_bar_w := bar_width - bar_gap
|
|
|
|
max_bar_height := (hist_h / 2) - 10
|
|
|
|
// Draw bars for each frame
|
|
for i, frame in frames {
|
|
x := margin + f32(i) * bar_width
|
|
|
|
// New allocations (cyan, above center)
|
|
if frame.new_bytes > 0 {
|
|
new_height := f32(frame.new_bytes) / f32(max_new) * max_bar_height
|
|
bar_color := if i == app.selected_frame { selected_color } else { new_alloc_color }
|
|
ctx.draw_rect_filled(x, center_y - new_height, actual_bar_w, new_height, bar_color)
|
|
}
|
|
|
|
// Freed allocations (purple, below center)
|
|
if frame.freed_bytes > 0 {
|
|
freed_height := f32(frame.freed_bytes) / f32(max_freed) * max_bar_height
|
|
bar_color := if i == app.selected_frame {
|
|
gg.Color{150, 130, 255, 200}
|
|
} else {
|
|
freed_color
|
|
}
|
|
ctx.draw_rect_filled(x, center_y, actual_bar_w, freed_height, bar_color)
|
|
}
|
|
|
|
// Hover highlight
|
|
if i == app.hover_frame && i != app.selected_frame {
|
|
ctx.draw_rect_empty(x - 1, hist_y + 5, actual_bar_w + 2, hist_h - 10, timeline_cursor)
|
|
}
|
|
}
|
|
|
|
// Legend
|
|
ctx.draw_rect_filled(10, hist_y + 10, 12, 12, new_alloc_color)
|
|
ctx.draw_text(26, int(hist_y) + 8, 'New', color: text_color, size: 14)
|
|
|
|
ctx.draw_rect_filled(70, hist_y + 10, 12, 12, freed_color)
|
|
ctx.draw_text(86, int(hist_y) + 8, 'Freed', color: text_color, size: 14)
|
|
|
|
// Separator line
|
|
ctx.draw_line(0, hist_y + hist_h, w, hist_y + hist_h, grid_color)
|
|
}
|
|
|
|
// draw_timeline renders the frame timeline with scrubber
|
|
fn draw_timeline(mut app App) {
|
|
ctx := app.gg
|
|
w := f32(ctx.width)
|
|
|
|
// Timeline area
|
|
timeline_y := f32(app.header_height + app.histogram_height)
|
|
timeline_h := f32(app.timeline_height)
|
|
|
|
// Background
|
|
ctx.draw_rect_filled(0, timeline_y, w, timeline_h, header_bg)
|
|
|
|
frames := app.cached_frames
|
|
if frames.len == 0 {
|
|
return
|
|
}
|
|
|
|
// Draw frame ticks
|
|
margin := f32(40)
|
|
tick_area_width := w - margin * 2
|
|
tick_spacing := tick_area_width / f32(frames.len)
|
|
|
|
// Draw tick marks
|
|
for i := 0; i < frames.len; i++ {
|
|
x := margin + f32(i) * tick_spacing
|
|
tick_height := if i % 10 == 0 { f32(15) } else { f32(8) }
|
|
ctx.draw_line(x, timeline_y + 20, x, timeline_y + 20 + tick_height, grid_color)
|
|
|
|
// Frame number labels every 10 frames
|
|
if i % 10 == 0 && tick_spacing > 3 {
|
|
ctx.draw_text(int(x) - 10, int(timeline_y) + 40, '${i}', color: text_color, size: 12)
|
|
}
|
|
}
|
|
|
|
// Draw selected frame cursor
|
|
if app.selected_frame >= 0 && app.selected_frame < frames.len {
|
|
cursor_x := margin + f32(app.selected_frame) * tick_spacing
|
|
// Triangle cursor
|
|
draw_triangle_filled(ctx, cursor_x, timeline_y + 15, cursor_x - 8, timeline_y + 5,
|
|
|
|
cursor_x + 8, timeline_y + 5, timeline_cursor)
|
|
|
|
// Vertical line
|
|
ctx.draw_line(cursor_x, timeline_y + 15, cursor_x, timeline_y + timeline_h - 5,
|
|
timeline_cursor)
|
|
|
|
// Frame info
|
|
frame := frames[app.selected_frame]
|
|
info := 'Frame ${app.selected_frame}: +${profiler.format_bytes(frame.new_bytes)} / -${profiler.format_bytes(frame.freed_bytes)}'
|
|
ctx.draw_text(int(w / 2) - 100, int(timeline_y) + 55, info, color: timeline_cursor, size: 16)
|
|
}
|
|
|
|
// Separator line
|
|
ctx.draw_line(0, timeline_y + timeline_h, w, timeline_y + timeline_h, grid_color)
|
|
}
|
|
|
|
// draw_controls renders the filter controls
|
|
fn draw_controls(mut app App) {
|
|
ctx := app.gg
|
|
w := f32(ctx.width)
|
|
|
|
// Controls area
|
|
ctrl_y := f32(app.header_height + app.histogram_height + app.timeline_height)
|
|
ctrl_h := f32(app.controls_height)
|
|
|
|
// Background
|
|
ctx.draw_rect_filled(0, ctrl_y, w, ctrl_h, bg_color)
|
|
|
|
// Filter buttons
|
|
mut x := f32(20)
|
|
y := ctrl_y + 10
|
|
|
|
// Filter label
|
|
ctx.draw_text(int(x), int(y), 'Filter:', color: text_color, size: 16)
|
|
x += 60
|
|
|
|
// Filter mode buttons
|
|
filters := ['All', 'Leaks', 'Large', 'Frame']
|
|
modes := [FilterMode.all, FilterMode.new_not_freed, FilterMode.large, FilterMode.current_frame]
|
|
|
|
for i, label in filters {
|
|
btn_w := f32(60)
|
|
btn_h := f32(24)
|
|
|
|
// Highlight active filter
|
|
btn_color := if app.filter_mode == modes[i] { selected_color } else { header_bg }
|
|
ctx.draw_rect_filled(x, y - 2, btn_w, btn_h, btn_color)
|
|
ctx.draw_rect_empty(x, y - 2, btn_w, btn_h, grid_color)
|
|
|
|
ctx.draw_text(int(x) + 8, int(y), label, color: text_color, size: 14)
|
|
x += btn_w + 10
|
|
}
|
|
|
|
// Separator line
|
|
ctx.draw_line(0, ctrl_y + ctrl_h, w, ctrl_y + ctrl_h, grid_color)
|
|
}
|
|
|
|
// draw_details renders the selection details panel
|
|
fn draw_details(mut app App) {
|
|
ctx := app.gg
|
|
w := f32(ctx.width)
|
|
h := f32(ctx.height)
|
|
|
|
// Details area
|
|
details_y := f32(app.header_height + app.histogram_height + app.timeline_height +
|
|
app.controls_height)
|
|
details_h := h - details_y
|
|
|
|
// Background
|
|
ctx.draw_rect_filled(0, details_y, w, details_h, header_bg)
|
|
|
|
// Show selected allocation info or instructions
|
|
if app.selected_alloc >= 0 {
|
|
allocs := app.cached_allocs
|
|
if app.selected_alloc < allocs.len {
|
|
alloc := allocs[app.selected_alloc]
|
|
|
|
// Format allocation info
|
|
ptr_str := 'Address: 0x${voidptr(alloc.ptr):p}'
|
|
size_str := 'Size: ${profiler.format_bytes(u64(alloc.size))} (${alloc.size} bytes)'
|
|
frame_str := 'Allocated: frame ${alloc.frame}'
|
|
status_str := if alloc.freed {
|
|
'Status: FREED (frame ${alloc.free_frame})'
|
|
} else {
|
|
'Status: LIVE (potential leak)'
|
|
}
|
|
|
|
status_color := if alloc.freed { freed_color } else { leak_color }
|
|
|
|
mut y := details_y + 15
|
|
ctx.draw_text(20, int(y), ptr_str, color: text_color, size: 16)
|
|
|
|
ctx.draw_text(250, int(y), size_str, color: text_color, size: 16)
|
|
|
|
ctx.draw_text(500, int(y), frame_str, color: text_color, size: 16)
|
|
|
|
ctx.draw_text(700, int(y), status_str, color: status_color, size: 16)
|
|
|
|
// Source location if available
|
|
if alloc.file.len > 0 {
|
|
y += 25
|
|
loc_str := 'Source: ${alloc.file}:${alloc.line}'
|
|
ctx.draw_text(20, int(y), loc_str, color: new_alloc_color, size: 16)
|
|
}
|
|
}
|
|
} else {
|
|
// Instructions
|
|
ctx.draw_text(20, int(details_y) + 20,
|
|
'Click on a histogram bar to select a frame, or use keyboard arrows to navigate',
|
|
color: text_color
|
|
size: 16
|
|
)
|
|
}
|
|
}
|
|
|
|
// draw_frame is the main frame callback
|
|
pub fn draw_frame(mut app App) {
|
|
if app.gg == unsafe { nil } {
|
|
return
|
|
}
|
|
app.gg.begin()
|
|
|
|
// Update data
|
|
if app.demo_mode {
|
|
update_demo_data(mut app)
|
|
} else {
|
|
app.update_cache()
|
|
}
|
|
|
|
// Draw all sections
|
|
draw_header(mut app)
|
|
draw_histogram(mut app)
|
|
draw_timeline(mut app)
|
|
draw_controls(mut app)
|
|
draw_details(mut app)
|
|
|
|
// Draw status message if in live mode with no data
|
|
if app.live_mode && app.cached_frames.len == 0 {
|
|
ctx := app.gg
|
|
w := f32(ctx.width)
|
|
h := f32(ctx.height)
|
|
ctx.draw_text(int(w / 2) - 200, int(h / 2), app.status_msg,
|
|
color: text_color
|
|
size: 20
|
|
)
|
|
}
|
|
|
|
app.gg.end()
|
|
}
|
|
|
|
// Helper: draw a filled triangle
|
|
fn draw_triangle_filled(ctx &gg.Context, x1 f32, y1 f32, x2 f32, y2 f32, x3 f32, y3 f32, c gg.Color) {
|
|
ctx.draw_convex_poly([x1, y1, x2, y2, x3, y3], c)
|
|
}
|