176 lines
4.8 KiB
V
176 lines
4.8 KiB
V
// 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()
|
|
}
|