| 1 | // Copyright (c) 2026 Alexander Medvednikov. 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 profiler |
| 5 | |
| 6 | import os |
| 7 | import time |
| 8 | |
| 9 | // Default path for profiler data exchange |
| 10 | pub const profiler_data_path = '/tmp/v2_profiler.dat' |
| 11 | |
| 12 | // Snapshot represents a point-in-time view of profiler state for IPC |
| 13 | pub struct Snapshot { |
| 14 | pub mut: |
| 15 | timestamp i64 |
| 16 | frame_count u64 |
| 17 | live_bytes u64 |
| 18 | peak_bytes u64 |
| 19 | total_allocs u64 |
| 20 | total_frees u64 |
| 21 | frames []FrameData |
| 22 | allocs []AllocRecord |
| 23 | } |
| 24 | |
| 25 | // write_snapshot writes the current profiler state to the shared file |
| 26 | // Call this at the end of each frame in the profiled application |
| 27 | pub fn write_snapshot() { |
| 28 | write_snapshot_to(profiler_data_path) |
| 29 | } |
| 30 | |
| 31 | // write_snapshot_to writes profiler state to a specific path |
| 32 | pub fn write_snapshot_to(path string) { |
| 33 | if profiler_state.mu == unsafe { nil } { |
| 34 | return |
| 35 | } |
| 36 | |
| 37 | profiler_state.mu.lock() |
| 38 | defer { |
| 39 | profiler_state.mu.unlock() |
| 40 | } |
| 41 | |
| 42 | // Build snapshot data as simple text format (easy to parse) |
| 43 | mut sb := []u8{cap: 4096} |
| 44 | |
| 45 | // Header with stats |
| 46 | sb << 'V2PROF1\n'.bytes() // Version marker |
| 47 | sb << 'T:${time.sys_mono_now()}\n'.bytes() |
| 48 | sb << 'F:${profiler_state.current_frame}\n'.bytes() |
| 49 | sb << 'L:${profiler_state.live_bytes}\n'.bytes() |
| 50 | sb << 'P:${profiler_state.peak_bytes}\n'.bytes() |
| 51 | sb << 'A:${profiler_state.total_allocs}\n'.bytes() |
| 52 | sb << 'R:${profiler_state.total_frees}\n'.bytes() |
| 53 | |
| 54 | // Frame data (last 200 frames max) |
| 55 | start_frame := if profiler_state.frames.len > 200 { |
| 56 | profiler_state.frames.len - 200 |
| 57 | } else { |
| 58 | 0 |
| 59 | } |
| 60 | sb << 'FRAMES:${profiler_state.frames.len - start_frame}\n'.bytes() |
| 61 | for i := start_frame; i < profiler_state.frames.len; i++ { |
| 62 | frame := profiler_state.frames[i] |
| 63 | sb << '${frame.frame_num},${frame.new_bytes},${frame.freed_bytes},${frame.live_bytes}\n'.bytes() |
| 64 | } |
| 65 | |
| 66 | // Allocation records (last 1000 max, for performance) |
| 67 | start_alloc := if profiler_state.allocs.len > 1000 { |
| 68 | profiler_state.allocs.len - 1000 |
| 69 | } else { |
| 70 | 0 |
| 71 | } |
| 72 | sb << 'ALLOCS:${profiler_state.allocs.len - start_alloc}\n'.bytes() |
| 73 | for i := start_alloc; i < profiler_state.allocs.len; i++ { |
| 74 | alloc := profiler_state.allocs[i] |
| 75 | freed_val := if alloc.freed { '1' } else { '0' } |
| 76 | // ptr,size,frame,freed,free_frame,file,line |
| 77 | sb << '${u64(alloc.ptr)},${alloc.size},${alloc.frame},${freed_val},${alloc.free_frame},${alloc.file},${alloc.line}\n'.bytes() |
| 78 | } |
| 79 | |
| 80 | sb << 'END\n'.bytes() |
| 81 | |
| 82 | // Write atomically by writing to temp file then renaming |
| 83 | tmp_path := path + '.tmp' |
| 84 | os.write_file(tmp_path, sb.bytestr()) or { return } |
| 85 | os.rename(tmp_path, path) or { return } |
| 86 | } |
| 87 | |
| 88 | // read_snapshot reads profiler state from the shared file |
| 89 | // Returns none if file doesn't exist or is invalid |
| 90 | pub fn read_snapshot() ?Snapshot { |
| 91 | return read_snapshot_from(profiler_data_path) |
| 92 | } |
| 93 | |
| 94 | // read_snapshot_from reads profiler state from a specific path |
| 95 | pub fn read_snapshot_from(path string) ?Snapshot { |
| 96 | content := os.read_file(path) or { return none } |
| 97 | lines := content.split_into_lines() |
| 98 | |
| 99 | if lines.len < 7 || lines[0] != 'V2PROF1' { |
| 100 | return none |
| 101 | } |
| 102 | |
| 103 | mut snap := Snapshot{} |
| 104 | |
| 105 | // Parse header |
| 106 | snap.timestamp = parse_value(lines[1], 'T:') |
| 107 | snap.frame_count = u64(parse_value(lines[2], 'F:')) |
| 108 | snap.live_bytes = u64(parse_value(lines[3], 'L:')) |
| 109 | snap.peak_bytes = u64(parse_value(lines[4], 'P:')) |
| 110 | snap.total_allocs = u64(parse_value(lines[5], 'A:')) |
| 111 | snap.total_frees = u64(parse_value(lines[6], 'R:')) |
| 112 | |
| 113 | mut line_idx := 7 |
| 114 | |
| 115 | // Parse frames |
| 116 | if line_idx < lines.len && lines[line_idx].starts_with('FRAMES:') { |
| 117 | num_frames := lines[line_idx].all_after('FRAMES:').int() |
| 118 | line_idx++ |
| 119 | for _ in 0 .. num_frames { |
| 120 | if line_idx >= lines.len { |
| 121 | break |
| 122 | } |
| 123 | parts := lines[line_idx].split(',') |
| 124 | if parts.len >= 4 { |
| 125 | snap.frames << FrameData{ |
| 126 | frame_num: u64(parts[0].i64()) |
| 127 | new_bytes: u64(parts[1].i64()) |
| 128 | freed_bytes: u64(parts[2].i64()) |
| 129 | live_bytes: u64(parts[3].i64()) |
| 130 | } |
| 131 | } |
| 132 | line_idx++ |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | // Parse allocs |
| 137 | if line_idx < lines.len && lines[line_idx].starts_with('ALLOCS:') { |
| 138 | num_allocs := lines[line_idx].all_after('ALLOCS:').int() |
| 139 | line_idx++ |
| 140 | for _ in 0 .. num_allocs { |
| 141 | if line_idx >= lines.len { |
| 142 | break |
| 143 | } |
| 144 | parts := lines[line_idx].split(',') |
| 145 | if parts.len >= 7 { |
| 146 | snap.allocs << AllocRecord{ |
| 147 | ptr: unsafe { voidptr(u64(parts[0].i64())) } |
| 148 | size: parts[1].int() |
| 149 | frame: u64(parts[2].i64()) |
| 150 | freed: parts[3] == '1' |
| 151 | free_frame: u64(parts[4].i64()) |
| 152 | file: parts[5] |
| 153 | line: parts[6].int() |
| 154 | } |
| 155 | } |
| 156 | line_idx++ |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | return snap |
| 161 | } |
| 162 | |
| 163 | fn parse_value(line string, prefix string) i64 { |
| 164 | if line.starts_with(prefix) { |
| 165 | return line.all_after(prefix).i64() |
| 166 | } |
| 167 | return 0 |
| 168 | } |
| 169 | |
| 170 | // frame_end_with_snapshot ends the frame and writes a snapshot |
| 171 | // Use this instead of frame_end() when you want live monitoring |
| 172 | pub fn frame_end_with_snapshot() { |
| 173 | frame_end() |
| 174 | write_snapshot() |
| 175 | } |
| 176 | |