Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,346 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// 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 os
|
||||
import v2.profiler
|
||||
|
||||
// handle_event processes user input events
|
||||
pub fn handle_event(e &gg.Event, mut app App) {
|
||||
match e.typ {
|
||||
.mouse_down {
|
||||
handle_mouse_click(e, mut app)
|
||||
}
|
||||
.mouse_move {
|
||||
handle_mouse_move(e, mut app)
|
||||
}
|
||||
.key_down {
|
||||
handle_key_down(e, mut app)
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
|
||||
// handle_mouse_click processes mouse click events
|
||||
fn handle_mouse_click(e &gg.Event, mut app App) {
|
||||
x := e.mouse_x
|
||||
y := e.mouse_y
|
||||
|
||||
// Check which region was clicked
|
||||
hist_y := f32(app.header_height)
|
||||
hist_h := f32(app.histogram_height)
|
||||
timeline_y := hist_y + hist_h
|
||||
timeline_h := f32(app.timeline_height)
|
||||
ctrl_y := timeline_y + timeline_h
|
||||
ctrl_h := f32(app.controls_height)
|
||||
|
||||
// Click in histogram area
|
||||
if y >= hist_y && y < hist_y + hist_h {
|
||||
handle_histogram_click(x, y, mut app)
|
||||
return
|
||||
}
|
||||
|
||||
// Click in timeline area
|
||||
if y >= timeline_y && y < timeline_y + timeline_h {
|
||||
handle_timeline_click(x, y, mut app)
|
||||
return
|
||||
}
|
||||
|
||||
// Click in controls area
|
||||
if y >= ctrl_y && y < ctrl_y + ctrl_h {
|
||||
handle_controls_click(x, y, mut app)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handle_histogram_click selects a frame from the histogram
|
||||
fn handle_histogram_click(x f32, y f32, mut app App) {
|
||||
frames := app.cached_frames
|
||||
if frames.len == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
w := f32(app.gg.width)
|
||||
margin := f32(40)
|
||||
bar_area_width := w - margin * 2
|
||||
|
||||
// Calculate frame index
|
||||
relative_x := x - margin
|
||||
if relative_x < 0 || relative_x > bar_area_width {
|
||||
return
|
||||
}
|
||||
|
||||
bar_width := bar_area_width / f32(frames.len)
|
||||
frame_idx := int(relative_x / bar_width)
|
||||
|
||||
if frame_idx >= 0 && frame_idx < frames.len {
|
||||
app.selected_frame = frame_idx
|
||||
// Select first allocation in this frame
|
||||
allocs := app.cached_allocs
|
||||
for i, alloc in allocs {
|
||||
if alloc.frame == u64(frame_idx) {
|
||||
app.selected_alloc = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle_timeline_click scrubs the timeline
|
||||
fn handle_timeline_click(x f32, y f32, mut app App) {
|
||||
// Same logic as histogram click for frame selection
|
||||
handle_histogram_click(x, y, mut app)
|
||||
}
|
||||
|
||||
// handle_controls_click handles filter button clicks
|
||||
fn handle_controls_click(x f32, y f32, mut app App) {
|
||||
// Filter buttons start at x=80
|
||||
btn_start := f32(80)
|
||||
btn_w := f32(60)
|
||||
btn_gap := f32(10)
|
||||
|
||||
relative_x := x - btn_start
|
||||
if relative_x < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
btn_idx := int(relative_x / (btn_w + btn_gap))
|
||||
modes := [FilterMode.all, FilterMode.new_not_freed, FilterMode.large, FilterMode.current_frame]
|
||||
|
||||
if btn_idx >= 0 && btn_idx < modes.len {
|
||||
app.filter_mode = modes[btn_idx]
|
||||
}
|
||||
}
|
||||
|
||||
// handle_mouse_move updates hover state
|
||||
fn handle_mouse_move(e &gg.Event, mut app App) {
|
||||
x := e.mouse_x
|
||||
y := e.mouse_y
|
||||
|
||||
hist_y := f32(app.header_height)
|
||||
hist_h := f32(app.histogram_height)
|
||||
|
||||
// Check if hovering over histogram
|
||||
if y >= hist_y && y < hist_y + hist_h {
|
||||
frames := app.cached_frames
|
||||
if frames.len > 0 {
|
||||
w := f32(app.gg.width)
|
||||
margin := f32(40)
|
||||
bar_area_width := w - margin * 2
|
||||
relative_x := x - margin
|
||||
|
||||
if relative_x >= 0 && relative_x <= bar_area_width {
|
||||
bar_width := bar_area_width / f32(frames.len)
|
||||
app.hover_frame = int(relative_x / bar_width)
|
||||
if app.hover_frame >= frames.len {
|
||||
app.hover_frame = -1
|
||||
}
|
||||
} else {
|
||||
app.hover_frame = -1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
app.hover_frame = -1
|
||||
}
|
||||
}
|
||||
|
||||
// handle_key_down processes keyboard input
|
||||
fn handle_key_down(e &gg.Event, mut app App) {
|
||||
match e.key_code {
|
||||
.left {
|
||||
// Previous frame
|
||||
if app.selected_frame > 0 {
|
||||
app.selected_frame--
|
||||
}
|
||||
}
|
||||
.right {
|
||||
// Next frame
|
||||
frames := app.cached_frames
|
||||
if app.selected_frame < frames.len - 1 {
|
||||
app.selected_frame++
|
||||
}
|
||||
}
|
||||
.up {
|
||||
// Previous allocation
|
||||
if app.selected_alloc > 0 {
|
||||
app.selected_alloc--
|
||||
}
|
||||
}
|
||||
.down {
|
||||
// Next allocation
|
||||
allocs := app.cached_allocs
|
||||
if app.selected_alloc < allocs.len - 1 {
|
||||
app.selected_alloc++
|
||||
}
|
||||
}
|
||||
.enter {
|
||||
// Open source file in editor
|
||||
open_in_editor(app)
|
||||
}
|
||||
.f {
|
||||
// Cycle filter modes
|
||||
app.filter_mode = match app.filter_mode {
|
||||
.all { .new_not_freed }
|
||||
.new_not_freed { .large }
|
||||
.large { .current_frame }
|
||||
.current_frame { .all }
|
||||
}
|
||||
}
|
||||
.r {
|
||||
// Reset profiler data
|
||||
if !app.demo_mode {
|
||||
profiler.reset()
|
||||
}
|
||||
}
|
||||
.escape {
|
||||
// Clear selection
|
||||
app.selected_frame = -1
|
||||
app.selected_alloc = -1
|
||||
}
|
||||
.space {
|
||||
// Toggle demo mode
|
||||
app.demo_mode = !app.demo_mode
|
||||
if app.demo_mode {
|
||||
// Initialize demo data
|
||||
init_demo_data(mut app)
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
|
||||
// open_in_editor opens the selected allocation's source file in VS Code
|
||||
fn open_in_editor(app &App) {
|
||||
if app.selected_alloc < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
allocs := app.cached_allocs
|
||||
if app.selected_alloc >= allocs.len {
|
||||
return
|
||||
}
|
||||
|
||||
alloc := allocs[app.selected_alloc]
|
||||
if alloc.file.len == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Try to open in VS Code with line number
|
||||
cmd := 'code -g "${alloc.file}:${alloc.line}"'
|
||||
os.execute(cmd)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// 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 os
|
||||
import v2.profiler
|
||||
|
||||
fn main() {
|
||||
// Initialize profiler system
|
||||
profiler.profiler_init()
|
||||
|
||||
// Create application state (on stack, like the working example)
|
||||
mut app := App{}
|
||||
|
||||
// Check command line args for mode
|
||||
args := os.args
|
||||
if '--demo' in args {
|
||||
app.demo_mode = true
|
||||
app.live_mode = false
|
||||
init_demo_data(mut app)
|
||||
} else {
|
||||
// Default to live mode - read from snapshot file
|
||||
app.demo_mode = false
|
||||
app.live_mode = true
|
||||
}
|
||||
|
||||
// Create gg context
|
||||
app.gg = gg.new_context(
|
||||
width: window_width
|
||||
height: window_height
|
||||
create_window: true
|
||||
window_title: 'V2 Allocation Profiler'
|
||||
bg_color: bg_color
|
||||
frame_fn: frame
|
||||
event_fn: event_handler
|
||||
user_data: &app
|
||||
)
|
||||
|
||||
// Run the application
|
||||
app.gg.run()
|
||||
}
|
||||
|
||||
// frame is the frame callback for gg
|
||||
fn frame(mut app App) {
|
||||
draw_frame(mut app)
|
||||
}
|
||||
|
||||
// event_handler handles gg events
|
||||
fn event_handler(e &gg.Event, mut app App) {
|
||||
handle_event(e, mut app)
|
||||
}
|
||||
|
||||
// init_demo_data populates the profiler with simulated data for demonstration
|
||||
fn init_demo_data(mut app App) {
|
||||
app.demo_mode = true
|
||||
app.demo_frame = 0
|
||||
|
||||
// Create simulated frame and allocation data
|
||||
app.cached_frames = []profiler.FrameData{cap: 100}
|
||||
app.cached_allocs = []profiler.AllocRecord{cap: 500}
|
||||
|
||||
// Simulate 100 frames of allocation activity
|
||||
mut total_live := u64(0)
|
||||
mut peak := u64(0)
|
||||
mut alloc_idx := 0
|
||||
mut total_allocs := u64(0)
|
||||
mut total_frees := u64(0)
|
||||
|
||||
for frame_num := 0; frame_num < 100; frame_num++ {
|
||||
mut frame_data := profiler.FrameData{
|
||||
frame_num: u64(frame_num)
|
||||
}
|
||||
|
||||
// Simulate some allocations each frame
|
||||
// More allocations early, fewer later (typical startup pattern)
|
||||
num_allocs := if frame_num < 20 { 10 - frame_num / 4 } else { 2 + (frame_num % 3) }
|
||||
|
||||
for _ in 0 .. num_allocs {
|
||||
// Random-ish allocation sizes
|
||||
size := 64 + ((frame_num * 17 + alloc_idx * 31) % 4096)
|
||||
|
||||
app.cached_allocs << profiler.AllocRecord{
|
||||
ptr: unsafe { voidptr(u64(0x1000) + u64(alloc_idx) * 0x100) }
|
||||
size: size
|
||||
frame: u64(frame_num)
|
||||
file: ['main.v', 'parser.v', 'scanner.v', 'types.v', 'gen.v'][alloc_idx % 5]
|
||||
line: 100 + (alloc_idx % 500)
|
||||
timestamp: i64(frame_num) * 16_666_667 + i64(alloc_idx) * 1000
|
||||
freed: false
|
||||
}
|
||||
|
||||
frame_data.new_allocs << alloc_idx
|
||||
frame_data.new_bytes += u64(size)
|
||||
total_live += u64(size)
|
||||
total_allocs++
|
||||
alloc_idx++
|
||||
}
|
||||
|
||||
// Simulate some frees (but not all - create some "leaks")
|
||||
// Free some allocations from previous frames
|
||||
if frame_num > 5 {
|
||||
num_frees := num_allocs - 1 // Always free slightly fewer than we allocate
|
||||
mut freed_count := 0
|
||||
for i := 0; i < app.cached_allocs.len && freed_count < num_frees; i++ {
|
||||
if !app.cached_allocs[i].freed && app.cached_allocs[i].frame < u64(frame_num - 3) {
|
||||
// 80% chance to free
|
||||
if (i * 7 + frame_num * 13) % 10 < 8 {
|
||||
app.cached_allocs[i].freed = true
|
||||
app.cached_allocs[i].free_frame = u64(frame_num)
|
||||
frame_data.freed_idxs << i
|
||||
frame_data.freed_bytes += u64(app.cached_allocs[i].size)
|
||||
total_live -= u64(app.cached_allocs[i].size)
|
||||
total_frees++
|
||||
freed_count++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frame_data.live_bytes = total_live
|
||||
if total_live > peak {
|
||||
peak = total_live
|
||||
}
|
||||
|
||||
app.cached_frames << frame_data
|
||||
}
|
||||
|
||||
// Count leaks
|
||||
mut leak_count := 0
|
||||
for alloc in app.cached_allocs {
|
||||
if !alloc.freed {
|
||||
leak_count++
|
||||
}
|
||||
}
|
||||
|
||||
// Update stats
|
||||
app.cached_stats = profiler.Statistics{
|
||||
live_bytes: total_live
|
||||
peak_bytes: peak
|
||||
total_allocs: total_allocs
|
||||
total_frees: total_frees
|
||||
leak_count: leak_count
|
||||
frame_count: 100
|
||||
}
|
||||
}
|
||||
|
||||
// update_demo_data simulates ongoing activity in demo mode
|
||||
fn update_demo_data(mut app App) {
|
||||
// In demo mode, we just use the static simulated data
|
||||
// For a more dynamic demo, we could add new frames here
|
||||
app.demo_frame++
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// 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
|
||||
|
||||
// Filter modes for allocation view
|
||||
pub enum FilterMode {
|
||||
all // Show all allocations
|
||||
new_not_freed // Show allocations that haven't been freed (potential leaks)
|
||||
large // Show allocations larger than threshold
|
||||
current_frame // Show allocations from selected frame only
|
||||
}
|
||||
|
||||
// App holds the GUI application state
|
||||
pub struct App {
|
||||
pub mut:
|
||||
gg &gg.Context = unsafe { nil }
|
||||
// View state
|
||||
selected_frame int = -1 // -1 means no selection
|
||||
selected_alloc int = -1 // Index of selected allocation
|
||||
filter_mode FilterMode = .all
|
||||
large_threshold int = 1024 // Threshold for "large" filter (bytes)
|
||||
search_text string
|
||||
// Scroll/pan state
|
||||
timeline_scroll int // Horizontal scroll for timeline
|
||||
histogram_scroll int // Scroll for histogram if needed
|
||||
// Cached data (updated each frame)
|
||||
cached_frames []profiler.FrameData
|
||||
cached_allocs []profiler.AllocRecord
|
||||
cached_stats profiler.Statistics
|
||||
// Layout dimensions
|
||||
header_height int = 60
|
||||
histogram_height int = 200
|
||||
timeline_height int = 80
|
||||
controls_height int = 40
|
||||
details_height int = 60
|
||||
// Hover state
|
||||
hover_frame int = -1
|
||||
hover_alloc int = -1
|
||||
// Mode
|
||||
demo_mode bool // Use simulated data for demo (false = live mode)
|
||||
demo_frame int // Current demo frame counter
|
||||
live_mode bool = true // Read from profiler snapshot file
|
||||
// Status
|
||||
status_msg string = 'Waiting for profiler data...'
|
||||
}
|
||||
|
||||
// Colors for the profiler visualization
|
||||
pub const bg_color = gg.Color{30, 30, 30, 255} // Dark gray background
|
||||
pub const new_alloc_color = gg.Color{0, 200, 255, 200} // Cyan for new allocations
|
||||
pub const freed_color = gg.Color{180, 100, 255, 200} // Purple for freed allocations
|
||||
pub const leak_color = gg.Color{255, 100, 100, 255} // Red for leaks/live
|
||||
pub const timeline_cursor = gg.Color{255, 255, 0, 255} // Yellow for timeline cursor
|
||||
pub const text_color = gg.Color{220, 220, 220, 255} // Light gray text
|
||||
pub const header_bg = gg.Color{40, 40, 45, 255} // Slightly lighter header
|
||||
pub const grid_color = gg.Color{60, 60, 60, 255} // Grid lines
|
||||
pub const selected_color = gg.Color{100, 150, 255, 255} // Selection highlight
|
||||
|
||||
// Window dimensions
|
||||
pub const window_width = 1200
|
||||
pub const window_height = 700
|
||||
|
||||
// init_app creates a new App instance (unused, kept for reference)
|
||||
fn init_app_heap() &App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// update_cache refreshes cached profiler data
|
||||
pub fn (mut app App) update_cache() {
|
||||
if app.demo_mode {
|
||||
return
|
||||
}
|
||||
|
||||
if app.live_mode {
|
||||
// Read from snapshot file
|
||||
if snap := profiler.read_snapshot() {
|
||||
app.cached_frames = snap.frames
|
||||
app.cached_allocs = snap.allocs
|
||||
app.cached_stats = profiler.Statistics{
|
||||
live_bytes: snap.live_bytes
|
||||
peak_bytes: snap.peak_bytes
|
||||
total_allocs: snap.total_allocs
|
||||
total_frees: snap.total_frees
|
||||
leak_count: count_leaks(snap.allocs)
|
||||
frame_count: snap.frame_count
|
||||
}
|
||||
app.status_msg = 'Live: Frame ${snap.frame_count}'
|
||||
} else {
|
||||
app.status_msg = 'Waiting for profiler data at ${profiler.profiler_data_path}...'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// In-process mode (same process)
|
||||
app.cached_frames = profiler.get_frames()
|
||||
app.cached_allocs = profiler.get_allocs()
|
||||
app.cached_stats = profiler.get_statistics()
|
||||
}
|
||||
|
||||
fn count_leaks(allocs []profiler.AllocRecord) int {
|
||||
mut count := 0
|
||||
for alloc in allocs {
|
||||
if !alloc.freed {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// get_filtered_allocs returns allocations matching current filter
|
||||
pub fn (app &App) get_filtered_allocs() []profiler.AllocRecord {
|
||||
mut result := []profiler.AllocRecord{}
|
||||
|
||||
allocs := app.cached_allocs
|
||||
|
||||
for alloc in allocs {
|
||||
match app.filter_mode {
|
||||
.all {
|
||||
result << alloc
|
||||
}
|
||||
.new_not_freed {
|
||||
if !alloc.freed {
|
||||
result << alloc
|
||||
}
|
||||
}
|
||||
.large {
|
||||
if alloc.size >= app.large_threshold {
|
||||
result << alloc
|
||||
}
|
||||
}
|
||||
.current_frame {
|
||||
if app.selected_frame >= 0 && alloc.frame == u64(app.selected_frame) {
|
||||
result << alloc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply search filter if text is present
|
||||
if app.search_text.len > 0 {
|
||||
mut filtered := []profiler.AllocRecord{}
|
||||
for alloc in result {
|
||||
if alloc.file.contains(app.search_text) {
|
||||
filtered << alloc
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// get_frame_at_x returns the frame index at the given x coordinate in timeline
|
||||
pub fn (app &App) get_frame_at_x(x f32, timeline_x f32, timeline_width f32) int {
|
||||
frames := app.cached_frames
|
||||
if frames.len == 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
// Calculate which frame this x position corresponds to
|
||||
relative_x := x - timeline_x
|
||||
if relative_x < 0 || relative_x > timeline_width {
|
||||
return -1
|
||||
}
|
||||
|
||||
frame_width := timeline_width / f32(frames.len)
|
||||
frame_idx := int(relative_x / frame_width)
|
||||
|
||||
if frame_idx >= 0 && frame_idx < frames.len {
|
||||
return frame_idx
|
||||
}
|
||||
return -1
|
||||
}
|
||||
Reference in new issue
Block a user