v / vlib / v2 / profiler / ipc.v
175 lines · 152 sloc · 4.82 KB · a76973f101edbe08f2df8b6ee19784ede8bfe966
Raw
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.
4module profiler
5
6import os
7import time
8
9// Default path for profiler data exchange
10pub const profiler_data_path = '/tmp/v2_profiler.dat'
11
12// Snapshot represents a point-in-time view of profiler state for IPC
13pub struct Snapshot {
14pub 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
27pub fn write_snapshot() {
28 write_snapshot_to(profiler_data_path)
29}
30
31// write_snapshot_to writes profiler state to a specific path
32pub 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
90pub 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
95pub 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
163fn 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
172pub fn frame_end_with_snapshot() {
173 frame_end()
174 write_snapshot()
175}
176