| 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 time |
| 7 | import sync |
| 8 | |
| 9 | // AllocRecord tracks a single allocation |
| 10 | pub struct AllocRecord { |
| 11 | pub: |
| 12 | ptr voidptr |
| 13 | size int |
| 14 | frame u64 |
| 15 | file string // Source file |
| 16 | line int // Source line |
| 17 | timestamp i64 // Monotonic time in nanoseconds |
| 18 | pub mut: |
| 19 | freed bool |
| 20 | free_frame u64 |
| 21 | } |
| 22 | |
| 23 | // FrameData aggregates allocation info for a single frame |
| 24 | pub struct FrameData { |
| 25 | pub: |
| 26 | frame_num u64 |
| 27 | pub mut: |
| 28 | new_bytes u64 |
| 29 | freed_bytes u64 |
| 30 | live_bytes u64 |
| 31 | new_allocs []int // Indices into allocs array |
| 32 | freed_idxs []int // Indices of allocations freed this frame |
| 33 | } |
| 34 | |
| 35 | // ProfilerState holds all profiling data |
| 36 | @[heap] |
| 37 | pub struct ProfilerState { |
| 38 | pub mut: |
| 39 | enabled bool |
| 40 | current_frame u64 |
| 41 | allocs []AllocRecord |
| 42 | alloc_map map[voidptr]int // ptr -> index in allocs |
| 43 | frames []FrameData |
| 44 | peak_bytes u64 |
| 45 | live_bytes u64 |
| 46 | total_allocs u64 |
| 47 | total_frees u64 |
| 48 | mu &sync.Mutex = unsafe { nil } |
| 49 | } |
| 50 | |
| 51 | // Global profiler state singleton |
| 52 | __global profiler_state = &ProfilerState{} |
| 53 | |
| 54 | // init_profiler_state initializes the global profiler state |
| 55 | // Must be called before using the profiler |
| 56 | pub fn init_profiler_state() { |
| 57 | profiler_state.mu = sync.new_mutex() |
| 58 | profiler_state.enabled = false |
| 59 | profiler_state.current_frame = 0 |
| 60 | profiler_state.allocs = []AllocRecord{cap: 10000} |
| 61 | profiler_state.frames = []FrameData{cap: 1000} |
| 62 | profiler_state.peak_bytes = 0 |
| 63 | profiler_state.live_bytes = 0 |
| 64 | profiler_state.total_allocs = 0 |
| 65 | profiler_state.total_frees = 0 |
| 66 | } |
| 67 | |
| 68 | // get_state returns the global profiler state |
| 69 | pub fn get_state() &ProfilerState { |
| 70 | return profiler_state |
| 71 | } |
| 72 | |
| 73 | // Statistics returns summary stats |
| 74 | pub struct Statistics { |
| 75 | pub: |
| 76 | live_bytes u64 |
| 77 | peak_bytes u64 |
| 78 | total_allocs u64 |
| 79 | total_frees u64 |
| 80 | leak_count int |
| 81 | frame_count u64 |
| 82 | } |
| 83 | |
| 84 | // get_statistics returns current profiler statistics |
| 85 | pub fn get_statistics() Statistics { |
| 86 | if profiler_state.mu == unsafe { nil } { |
| 87 | return Statistics{} |
| 88 | } |
| 89 | profiler_state.mu.lock() |
| 90 | defer { |
| 91 | profiler_state.mu.unlock() |
| 92 | } |
| 93 | |
| 94 | mut leak_count := 0 |
| 95 | for alloc in profiler_state.allocs { |
| 96 | if !alloc.freed { |
| 97 | leak_count++ |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | return Statistics{ |
| 102 | live_bytes: profiler_state.live_bytes |
| 103 | peak_bytes: profiler_state.peak_bytes |
| 104 | total_allocs: profiler_state.total_allocs |
| 105 | total_frees: profiler_state.total_frees |
| 106 | leak_count: leak_count |
| 107 | frame_count: profiler_state.current_frame |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | // format_bytes converts bytes to a human-readable string |
| 112 | pub fn format_bytes(bytes u64) string { |
| 113 | if bytes < 1024 { |
| 114 | return '${bytes} B' |
| 115 | } else if bytes < 1024 * 1024 { |
| 116 | return '${f64(bytes) / 1024.0:.1} KB' |
| 117 | } else if bytes < 1024 * 1024 * 1024 { |
| 118 | return '${f64(bytes) / (1024.0 * 1024.0):.1} MB' |
| 119 | } else { |
| 120 | return '${f64(bytes) / (1024.0 * 1024.0 * 1024.0):.2} GB' |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | // now_mono returns the current monotonic time in nanoseconds |
| 125 | fn now_mono() i64 { |
| 126 | return time.sys_mono_now() |
| 127 | } |
| 128 | |