From a76973f101edbe08f2df8b6ee19784ede8bfe966 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sat, 31 Jan 2026 06:35:20 +0300 Subject: [PATCH] v2: profiler fixes --- cmd/v2/example_profiled_app.v | 71 ++++++++++++++ cmd/v2/guiprof/draw.v | 37 +++++-- cmd/v2/guiprof/events.v | 12 +-- cmd/v2/guiprof/main.v | 14 ++- cmd/v2/guiprof/state.v | 45 ++++++++- vlib/v2/profiler/ipc.v | 175 ++++++++++++++++++++++++++++++++++ 6 files changed, 331 insertions(+), 23 deletions(-) create mode 100644 cmd/v2/example_profiled_app.v create mode 100644 vlib/v2/profiler/ipc.v diff --git a/cmd/v2/example_profiled_app.v b/cmd/v2/example_profiled_app.v new file mode 100644 index 000000000..31aa01249 --- /dev/null +++ b/cmd/v2/example_profiled_app.v @@ -0,0 +1,71 @@ +// Example application that writes profiler data for live monitoring +// Run with: ./v -enable-globals run cmd/v2/guiprof/example_app.v +// Then run guiprof in another terminal to see live data +module main + +import time +import v2.profiler + +fn main() { + println('Starting example app with profiler...') + println('Run guiprof in another terminal to see live data') + println('Press Ctrl+C to stop') + println('') + + // Initialize profiler + profiler.profiler_init() + profiler.profiler_enable() + + // Use the profiler allocator to track allocations + old_alloc := profiler.use_profiler_allocator() + defer { + profiler.restore_allocator(old_alloc) + } + + // Simulate an application loop + mut frame := 0 + mut allocations := []voidptr{cap: 100} + + for { + frame++ + + // Simulate allocations (using profiler.alloc directly) + num_allocs := 3 + (frame % 5) + for _ in 0 .. num_allocs { + size := 64 + (frame * 17) % 1024 + ptr := profiler.alloc(size) + if ptr != unsafe { nil } { + allocations << ptr + } + } + + // Simulate frees (free some old allocations) + if allocations.len > 50 { + num_frees := allocations.len / 4 + for _ in 0 .. num_frees { + if allocations.len > 0 { + ptr := allocations.pop() + profiler.free(ptr) + } + } + } + + // End frame and write snapshot for live monitoring + profiler.frame_end_with_snapshot() + + // Print progress + stats := profiler.get_statistics() + if frame % 10 == 0 { + println('Frame ${frame}: ${profiler.format_bytes(stats.live_bytes)} live, ${stats.total_allocs} allocs, ${stats.total_frees} frees') + } + + // Simulate ~30 fps + time.sleep(33 * time.millisecond) + + // Run for a while then exit + if frame > 500 { + println('Done! Ran 500 frames.') + break + } + } +} diff --git a/cmd/v2/guiprof/draw.v b/cmd/v2/guiprof/draw.v index d2dddab2a..26b4b3ea5 100644 --- a/cmd/v2/guiprof/draw.v +++ b/cmd/v2/guiprof/draw.v @@ -15,7 +15,7 @@ fn draw_header(mut app App) { ctx.draw_rect_filled(0, 0, w, f32(app.header_height), header_bg) // Get stats - stats := if app.demo_mode { app.cached_stats } else { profiler.get_statistics() } + stats := app.cached_stats // Format and draw stats heap_str := 'HEAP: ${profiler.format_bytes(stats.live_bytes)}' @@ -44,7 +44,16 @@ fn draw_header(mut app App) { 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) - // Frame counter on right + // 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) @@ -68,8 +77,8 @@ fn draw_histogram(mut app App) { // Center line ctx.draw_line(0, center_y, w, center_y, grid_color) - // Get frame data - frames := if app.demo_mode { app.cached_frames } else { profiler.get_frames() } + // 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 @@ -153,7 +162,7 @@ fn draw_timeline(mut app App) { // Background ctx.draw_rect_filled(0, timeline_y, w, timeline_h, header_bg) - frames := if app.demo_mode { app.cached_frames } else { profiler.get_frames() } + frames := app.cached_frames if frames.len == 0 { return } @@ -253,7 +262,7 @@ fn draw_details(mut app App) { // Show selected allocation info or instructions if app.selected_alloc >= 0 { - allocs := if app.demo_mode { app.cached_allocs } else { profiler.get_allocs() } + allocs := app.cached_allocs if app.selected_alloc < allocs.len { alloc := allocs[app.selected_alloc] @@ -301,10 +310,7 @@ pub fn draw_frame(mut app App) { } app.gg.begin() - // Draw a test rectangle to verify rendering works - app.gg.draw_rect_filled(100, 100, 200, 100, gg.Color{255, 0, 0, 255}) - - // Update demo data if in demo mode + // Update data if app.demo_mode { update_demo_data(mut app) } else { @@ -318,6 +324,17 @@ pub fn draw_frame(mut app 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() } diff --git a/cmd/v2/guiprof/events.v b/cmd/v2/guiprof/events.v index b50f0c803..85c223188 100644 --- a/cmd/v2/guiprof/events.v +++ b/cmd/v2/guiprof/events.v @@ -57,7 +57,7 @@ fn handle_mouse_click(e &gg.Event, mut app App) { // handle_histogram_click selects a frame from the histogram fn handle_histogram_click(x f32, y f32, mut app App) { - frames := if app.demo_mode { app.cached_frames } else { profiler.get_frames() } + frames := app.cached_frames if frames.len == 0 { return } @@ -78,7 +78,7 @@ fn handle_histogram_click(x f32, y f32, mut app App) { if frame_idx >= 0 && frame_idx < frames.len { app.selected_frame = frame_idx // Select first allocation in this frame - allocs := if app.demo_mode { app.cached_allocs } else { profiler.get_allocs() } + allocs := app.cached_allocs for i, alloc in allocs { if alloc.frame == u64(frame_idx) { app.selected_alloc = i @@ -124,7 +124,7 @@ fn handle_mouse_move(e &gg.Event, mut app App) { // Check if hovering over histogram if y >= hist_y && y < hist_y + hist_h { - frames := if app.demo_mode { app.cached_frames } else { profiler.get_frames() } + frames := app.cached_frames if frames.len > 0 { w := f32(app.gg.width) margin := f32(40) @@ -157,7 +157,7 @@ fn handle_key_down(e &gg.Event, mut app App) { } .right { // Next frame - frames := if app.demo_mode { app.cached_frames } else { profiler.get_frames() } + frames := app.cached_frames if app.selected_frame < frames.len - 1 { app.selected_frame++ } @@ -170,7 +170,7 @@ fn handle_key_down(e &gg.Event, mut app App) { } .down { // Next allocation - allocs := if app.demo_mode { app.cached_allocs } else { profiler.get_allocs() } + allocs := app.cached_allocs if app.selected_alloc < allocs.len - 1 { app.selected_alloc++ } @@ -217,7 +217,7 @@ fn open_in_editor(app &App) { return } - allocs := if app.demo_mode { app.cached_allocs } else { profiler.get_allocs() } + allocs := app.cached_allocs if app.selected_alloc >= allocs.len { return } diff --git a/cmd/v2/guiprof/main.v b/cmd/v2/guiprof/main.v index 0b07a1ef2..dfa963f94 100644 --- a/cmd/v2/guiprof/main.v +++ b/cmd/v2/guiprof/main.v @@ -4,6 +4,7 @@ module main import gg +import os import v2.profiler fn main() { @@ -13,8 +14,17 @@ fn main() { // Create application state (on stack, like the working example) mut app := App{} - // Initialize with demo data - init_demo_data(mut 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( diff --git a/cmd/v2/guiprof/state.v b/cmd/v2/guiprof/state.v index 64ea158ab..a351ff57b 100644 --- a/cmd/v2/guiprof/state.v +++ b/cmd/v2/guiprof/state.v @@ -40,9 +40,12 @@ pub mut: // Hover state hover_frame int = -1 hover_alloc int = -1 - // Demo mode - demo_mode bool = true // Use simulated data for demo - demo_frame int // Current demo frame counter + // 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 @@ -70,16 +73,48 @@ 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 := if app.demo_mode { app.cached_allocs } else { profiler.get_allocs() } + allocs := app.cached_allocs for alloc in allocs { match app.filter_mode { @@ -120,7 +155,7 @@ pub fn (app &App) get_filtered_allocs() []profiler.AllocRecord { // 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 := if app.demo_mode { app.cached_frames } else { profiler.get_frames() } + frames := app.cached_frames if frames.len == 0 { return -1 } diff --git a/vlib/v2/profiler/ipc.v b/vlib/v2/profiler/ipc.v new file mode 100644 index 000000000..92c8584ea --- /dev/null +++ b/vlib/v2/profiler/ipc.v @@ -0,0 +1,175 @@ +// Copyright (c) 2026 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module profiler + +import os +import time + +// Default path for profiler data exchange +pub const profiler_data_path = '/tmp/v2_profiler.dat' + +// Snapshot represents a point-in-time view of profiler state for IPC +pub struct Snapshot { +pub mut: + timestamp i64 + frame_count u64 + live_bytes u64 + peak_bytes u64 + total_allocs u64 + total_frees u64 + frames []FrameData + allocs []AllocRecord +} + +// write_snapshot writes the current profiler state to the shared file +// Call this at the end of each frame in the profiled application +pub fn write_snapshot() { + write_snapshot_to(profiler_data_path) +} + +// write_snapshot_to writes profiler state to a specific path +pub fn write_snapshot_to(path string) { + if profiler_state.mu == unsafe { nil } { + return + } + + profiler_state.mu.lock() + defer { + profiler_state.mu.unlock() + } + + // Build snapshot data as simple text format (easy to parse) + mut sb := []u8{cap: 4096} + + // Header with stats + sb << 'V2PROF1\n'.bytes() // Version marker + sb << 'T:${time.sys_mono_now()}\n'.bytes() + sb << 'F:${profiler_state.current_frame}\n'.bytes() + sb << 'L:${profiler_state.live_bytes}\n'.bytes() + sb << 'P:${profiler_state.peak_bytes}\n'.bytes() + sb << 'A:${profiler_state.total_allocs}\n'.bytes() + sb << 'R:${profiler_state.total_frees}\n'.bytes() + + // Frame data (last 200 frames max) + start_frame := if profiler_state.frames.len > 200 { + profiler_state.frames.len - 200 + } else { + 0 + } + sb << 'FRAMES:${profiler_state.frames.len - start_frame}\n'.bytes() + for i := start_frame; i < profiler_state.frames.len; i++ { + frame := profiler_state.frames[i] + sb << '${frame.frame_num},${frame.new_bytes},${frame.freed_bytes},${frame.live_bytes}\n'.bytes() + } + + // Allocation records (last 1000 max, for performance) + start_alloc := if profiler_state.allocs.len > 1000 { + profiler_state.allocs.len - 1000 + } else { + 0 + } + sb << 'ALLOCS:${profiler_state.allocs.len - start_alloc}\n'.bytes() + for i := start_alloc; i < profiler_state.allocs.len; i++ { + alloc := profiler_state.allocs[i] + freed_val := if alloc.freed { '1' } else { '0' } + // ptr,size,frame,freed,free_frame,file,line + sb << '${u64(alloc.ptr)},${alloc.size},${alloc.frame},${freed_val},${alloc.free_frame},${alloc.file},${alloc.line}\n'.bytes() + } + + sb << 'END\n'.bytes() + + // Write atomically by writing to temp file then renaming + tmp_path := path + '.tmp' + os.write_file(tmp_path, sb.bytestr()) or { return } + os.rename(tmp_path, path) or { return } +} + +// read_snapshot reads profiler state from the shared file +// Returns none if file doesn't exist or is invalid +pub fn read_snapshot() ?Snapshot { + return read_snapshot_from(profiler_data_path) +} + +// read_snapshot_from reads profiler state from a specific path +pub fn read_snapshot_from(path string) ?Snapshot { + content := os.read_file(path) or { return none } + lines := content.split_into_lines() + + if lines.len < 7 || lines[0] != 'V2PROF1' { + return none + } + + mut snap := Snapshot{} + + // Parse header + snap.timestamp = parse_value(lines[1], 'T:') + snap.frame_count = u64(parse_value(lines[2], 'F:')) + snap.live_bytes = u64(parse_value(lines[3], 'L:')) + snap.peak_bytes = u64(parse_value(lines[4], 'P:')) + snap.total_allocs = u64(parse_value(lines[5], 'A:')) + snap.total_frees = u64(parse_value(lines[6], 'R:')) + + mut line_idx := 7 + + // Parse frames + if line_idx < lines.len && lines[line_idx].starts_with('FRAMES:') { + num_frames := lines[line_idx].all_after('FRAMES:').int() + line_idx++ + for _ in 0 .. num_frames { + if line_idx >= lines.len { + break + } + parts := lines[line_idx].split(',') + if parts.len >= 4 { + snap.frames << FrameData{ + frame_num: u64(parts[0].i64()) + new_bytes: u64(parts[1].i64()) + freed_bytes: u64(parts[2].i64()) + live_bytes: u64(parts[3].i64()) + } + } + line_idx++ + } + } + + // Parse allocs + if line_idx < lines.len && lines[line_idx].starts_with('ALLOCS:') { + num_allocs := lines[line_idx].all_after('ALLOCS:').int() + line_idx++ + for _ in 0 .. num_allocs { + if line_idx >= lines.len { + break + } + parts := lines[line_idx].split(',') + if parts.len >= 7 { + snap.allocs << AllocRecord{ + ptr: unsafe { voidptr(u64(parts[0].i64())) } + size: parts[1].int() + frame: u64(parts[2].i64()) + freed: parts[3] == '1' + free_frame: u64(parts[4].i64()) + file: parts[5] + line: parts[6].int() + } + } + line_idx++ + } + } + + return snap +} + +fn parse_value(line string, prefix string) i64 { + if line.starts_with(prefix) { + return line.all_after(prefix).i64() + } + return 0 +} + +// frame_end_with_snapshot ends the frame and writes a snapshot +// Use this instead of frame_end() when you want live monitoring +pub fn frame_end_with_snapshot() { + frame_end() + write_snapshot() +} -- 2.39.5