Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

@@ -0,0 +1,127 @@
// 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 time
import sync
// AllocRecord tracks a single allocation
pub struct AllocRecord {
pub:
ptr voidptr
size int
frame u64
file string // Source file
line int // Source line
timestamp i64 // Monotonic time in nanoseconds
pub mut:
freed bool
free_frame u64
}
// FrameData aggregates allocation info for a single frame
pub struct FrameData {
pub:
frame_num u64
pub mut:
new_bytes u64
freed_bytes u64
live_bytes u64
new_allocs []int // Indices into allocs array
freed_idxs []int // Indices of allocations freed this frame
}
// ProfilerState holds all profiling data
@[heap]
pub struct ProfilerState {
pub mut:
enabled bool
current_frame u64
allocs []AllocRecord
alloc_map map[voidptr]int // ptr -> index in allocs
frames []FrameData
peak_bytes u64
live_bytes u64
total_allocs u64
total_frees u64
mu &sync.Mutex = unsafe { nil }
}
// Global profiler state singleton
__global profiler_state = &ProfilerState{}
// init_profiler_state initializes the global profiler state
// Must be called before using the profiler
pub fn init_profiler_state() {
profiler_state.mu = sync.new_mutex()
profiler_state.enabled = false
profiler_state.current_frame = 0
profiler_state.allocs = []AllocRecord{cap: 10000}
profiler_state.frames = []FrameData{cap: 1000}
profiler_state.peak_bytes = 0
profiler_state.live_bytes = 0
profiler_state.total_allocs = 0
profiler_state.total_frees = 0
}
// get_state returns the global profiler state
pub fn get_state() &ProfilerState {
return profiler_state
}
// Statistics returns summary stats
pub struct Statistics {
pub:
live_bytes u64
peak_bytes u64
total_allocs u64
total_frees u64
leak_count int
frame_count u64
}
// get_statistics returns current profiler statistics
pub fn get_statistics() Statistics {
if profiler_state.mu == unsafe { nil } {
return Statistics{}
}
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
mut leak_count := 0
for alloc in profiler_state.allocs {
if !alloc.freed {
leak_count++
}
}
return Statistics{
live_bytes: profiler_state.live_bytes
peak_bytes: profiler_state.peak_bytes
total_allocs: profiler_state.total_allocs
total_frees: profiler_state.total_frees
leak_count: leak_count
frame_count: profiler_state.current_frame
}
}
// format_bytes converts bytes to a human-readable string
pub fn format_bytes(bytes u64) string {
if bytes < 1024 {
return '${bytes} B'
} else if bytes < 1024 * 1024 {
return '${f64(bytes) / 1024.0:.1} KB'
} else if bytes < 1024 * 1024 * 1024 {
return '${f64(bytes) / (1024.0 * 1024.0):.1} MB'
} else {
return '${f64(bytes) / (1024.0 * 1024.0 * 1024.0):.2} GB'
}
}
// now_mono returns the current monotonic time in nanoseconds
fn now_mono() i64 {
return time.sys_mono_now()
}
@@ -0,0 +1,76 @@
// 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 context
// Allocator interface - can be swapped at runtime
// Similar to Jai's context.allocator design
pub struct Allocator {
pub:
alloc_fn fn (size int, ctx voidptr) voidptr = default_alloc
free_fn fn (ptr voidptr, ctx voidptr) = default_free
realloc_fn fn (ptr voidptr, new_size int, ctx voidptr) voidptr = default_realloc
ctx voidptr // User data for allocator implementation
}
// Thread-local context (implicit parameter)
// This allows all allocations in a scope to be tracked without explicit annotations
@[thread_local]
__global context = Context{}
pub struct Context {
pub mut:
allocator Allocator = default_allocator
}
// Default allocator - just wraps malloc/free/realloc
pub const default_allocator = Allocator{
alloc_fn: default_alloc
free_fn: default_free
realloc_fn: default_realloc
ctx: unsafe { nil }
}
fn default_alloc(size int, ctx voidptr) voidptr {
return unsafe { C.malloc(size) }
}
fn default_free(ptr voidptr, ctx voidptr) {
unsafe { C.free(ptr) }
}
fn default_realloc(ptr voidptr, new_size int, ctx voidptr) voidptr {
return unsafe { C.realloc(ptr, new_size) }
}
// User code calls these - they use context.allocator implicitly
// This provides the Jai-like implicit allocation tracking
pub fn alloc(size int) voidptr {
return context.allocator.alloc_fn(size, context.allocator.ctx)
}
pub fn free(ptr voidptr) {
context.allocator.free_fn(ptr, context.allocator.ctx)
}
pub fn realloc(ptr voidptr, new_size int) voidptr {
return context.allocator.realloc_fn(ptr, new_size, context.allocator.ctx)
}
// Scoped allocator helper - saves and restores the allocator
// Usage:
// old := profiler.push_allocator(my_allocator)
// defer { profiler.pop_allocator(old) }
// // ... all allocations here use my_allocator
pub fn push_allocator(a Allocator) Allocator {
old := context.allocator
context.allocator = a
return old
}
pub fn pop_allocator(old Allocator) {
context.allocator = old
}
+175
View File
@@ -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()
}
@@ -0,0 +1,204 @@
// 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 time
// profiler_alloc is the allocation function that records allocations
fn profiler_alloc(size int, ctx voidptr) voidptr {
ptr := unsafe { C.malloc(size) }
if ptr == unsafe { nil } {
return ptr
}
if profiler_state.mu == unsafe { nil } || !profiler_state.enabled {
return ptr
}
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
idx := profiler_state.allocs.len
profiler_state.allocs << AllocRecord{
ptr: ptr
size: size
frame: profiler_state.current_frame
file: '' // Will be set by caller if needed
line: 0
timestamp: time.sys_mono_now()
freed: false
}
profiler_state.alloc_map[ptr] = idx
profiler_state.live_bytes += u64(size)
profiler_state.total_allocs++
if profiler_state.live_bytes > profiler_state.peak_bytes {
profiler_state.peak_bytes = profiler_state.live_bytes
}
// Add to current frame's new allocs
if profiler_state.frames.len > 0 {
frame_idx := profiler_state.frames.len - 1
profiler_state.frames[frame_idx].new_allocs << idx
profiler_state.frames[frame_idx].new_bytes += u64(size)
}
return ptr
}
// profiler_alloc_with_location records allocation with source location
pub fn profiler_alloc_with_location(size int, file string, line int) voidptr {
ptr := unsafe { C.malloc(size) }
if ptr == unsafe { nil } {
return ptr
}
if profiler_state.mu == unsafe { nil } || !profiler_state.enabled {
return ptr
}
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
idx := profiler_state.allocs.len
profiler_state.allocs << AllocRecord{
ptr: ptr
size: size
frame: profiler_state.current_frame
file: file
line: line
timestamp: time.sys_mono_now()
freed: false
}
profiler_state.alloc_map[ptr] = idx
profiler_state.live_bytes += u64(size)
profiler_state.total_allocs++
if profiler_state.live_bytes > profiler_state.peak_bytes {
profiler_state.peak_bytes = profiler_state.live_bytes
}
// Add to current frame's new allocs
if profiler_state.frames.len > 0 {
frame_idx := profiler_state.frames.len - 1
profiler_state.frames[frame_idx].new_allocs << idx
profiler_state.frames[frame_idx].new_bytes += u64(size)
}
return ptr
}
// profiler_free records a free operation
fn profiler_free(ptr voidptr, ctx voidptr) {
if ptr == unsafe { nil } {
return
}
if profiler_state.mu != unsafe { nil } && profiler_state.enabled {
profiler_state.mu.lock()
if idx := profiler_state.alloc_map[ptr] {
profiler_state.allocs[idx].freed = true
profiler_state.allocs[idx].free_frame = profiler_state.current_frame
alloc_size := u64(profiler_state.allocs[idx].size)
if profiler_state.live_bytes >= alloc_size {
profiler_state.live_bytes -= alloc_size
}
profiler_state.total_frees++
// Add to current frame's freed list
if profiler_state.frames.len > 0 {
frame_idx := profiler_state.frames.len - 1
profiler_state.frames[frame_idx].freed_idxs << idx
profiler_state.frames[frame_idx].freed_bytes += alloc_size
}
// Remove from map
profiler_state.alloc_map.delete(ptr)
}
profiler_state.mu.unlock()
}
unsafe { C.free(ptr) }
}
// profiler_realloc records a realloc operation
fn profiler_realloc(ptr voidptr, new_size int, ctx voidptr) voidptr {
if ptr == unsafe { nil } {
return profiler_alloc(new_size, ctx)
}
if new_size == 0 {
profiler_free(ptr, ctx)
return unsafe { nil }
}
// Record the free of old allocation
old_size := 0
if profiler_state.mu != unsafe { nil } && profiler_state.enabled {
profiler_state.mu.lock()
if idx := profiler_state.alloc_map[ptr] {
unsafe {
*(&old_size) = profiler_state.allocs[idx].size
}
}
profiler_state.mu.unlock()
}
new_ptr := unsafe { C.realloc(ptr, new_size) }
if new_ptr == unsafe { nil } {
return new_ptr
}
if profiler_state.mu != unsafe { nil } && profiler_state.enabled {
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
// Remove old entry
if idx := profiler_state.alloc_map[ptr] {
profiler_state.allocs[idx].freed = true
profiler_state.allocs[idx].free_frame = profiler_state.current_frame
if profiler_state.live_bytes >= u64(old_size) {
profiler_state.live_bytes -= u64(old_size)
}
profiler_state.alloc_map.delete(ptr)
}
// Add new entry
new_idx := profiler_state.allocs.len
profiler_state.allocs << AllocRecord{
ptr: new_ptr
size: new_size
frame: profiler_state.current_frame
file: ''
line: 0
timestamp: time.sys_mono_now()
freed: false
}
profiler_state.alloc_map[new_ptr] = new_idx
profiler_state.live_bytes += u64(new_size)
if profiler_state.live_bytes > profiler_state.peak_bytes {
profiler_state.peak_bytes = profiler_state.live_bytes
}
}
return new_ptr
}
// profiler_allocator returns an Allocator that tracks all allocations
pub fn profiler_allocator() Allocator {
return Allocator{
alloc_fn: profiler_alloc
free_fn: profiler_free
realloc_fn: profiler_realloc
ctx: unsafe { nil }
}
}
@@ -0,0 +1,215 @@
// 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
// profiler_init initializes the profiler system
// Must be called before enabling profiling
pub fn profiler_init() {
init_profiler_state()
}
// profiler_enable enables allocation tracking
pub fn profiler_enable() {
if profiler_state.mu == unsafe { nil } {
init_profiler_state()
}
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
profiler_state.enabled = true
// Start first frame
if profiler_state.frames.len == 0 {
profiler_state.frames << FrameData{
frame_num: 0
new_bytes: 0
freed_bytes: 0
live_bytes: profiler_state.live_bytes
}
}
}
// profiler_disable disables allocation tracking
pub fn profiler_disable() {
if profiler_state.mu == unsafe { nil } {
return
}
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
profiler_state.enabled = false
}
// profiler_is_enabled returns whether profiling is active
pub fn profiler_is_enabled() bool {
if profiler_state.mu == unsafe { nil } {
return false
}
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
return profiler_state.enabled
}
// frame_end signals the end of a frame and collects frame data
// Call this at the end of each game/application frame
pub fn frame_end() {
if profiler_state.mu == unsafe { nil } || !profiler_state.enabled {
return
}
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
// Finalize current frame's live bytes
if profiler_state.frames.len > 0 {
profiler_state.frames[profiler_state.frames.len - 1].live_bytes = profiler_state.live_bytes
}
// Increment frame counter
profiler_state.current_frame++
// Start new frame
profiler_state.frames << FrameData{
frame_num: profiler_state.current_frame
new_bytes: 0
freed_bytes: 0
live_bytes: profiler_state.live_bytes
}
}
// get_frame_count returns the current frame number
pub fn get_frame_count() u64 {
if profiler_state.mu == unsafe { nil } {
return 0
}
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
return profiler_state.current_frame
}
// get_frames returns a copy of all frame data
pub fn get_frames() []FrameData {
if profiler_state.mu == unsafe { nil } {
return []
}
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
return profiler_state.frames.clone()
}
// get_allocs returns a copy of all allocation records
pub fn get_allocs() []AllocRecord {
if profiler_state.mu == unsafe { nil } {
return []
}
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
return profiler_state.allocs.clone()
}
// get_leaks returns allocations that were never freed
pub fn get_leaks() []AllocRecord {
if profiler_state.mu == unsafe { nil } {
return []
}
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
mut leaks := []AllocRecord{}
for alloc in profiler_state.allocs {
if !alloc.freed {
leaks << alloc
}
}
return leaks
}
// get_allocs_for_frame returns allocations made in a specific frame
pub fn get_allocs_for_frame(frame_num u64) []AllocRecord {
if profiler_state.mu == unsafe { nil } {
return []
}
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
mut frame_allocs := []AllocRecord{}
for alloc in profiler_state.allocs {
if alloc.frame == frame_num {
frame_allocs << alloc
}
}
return frame_allocs
}
// get_live_allocs returns currently live (not freed) allocations
pub fn get_live_allocs() []AllocRecord {
if profiler_state.mu == unsafe { nil } {
return []
}
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
mut live := []AllocRecord{}
for alloc in profiler_state.allocs {
if !alloc.freed {
live << alloc
}
}
return live
}
// reset clears all profiler data
pub fn reset() {
if profiler_state.mu == unsafe { nil } {
return
}
profiler_state.mu.lock()
defer {
profiler_state.mu.unlock()
}
profiler_state.allocs.clear()
profiler_state.alloc_map.clear()
profiler_state.frames.clear()
profiler_state.current_frame = 0
profiler_state.live_bytes = 0
profiler_state.peak_bytes = 0
profiler_state.total_allocs = 0
profiler_state.total_frees = 0
// Start fresh frame
if profiler_state.enabled {
profiler_state.frames << FrameData{
frame_num: 0
}
}
}
// use_profiler_allocator enables the profiler allocator in the current context
// Returns the old allocator for restoration
pub fn use_profiler_allocator() Allocator {
return push_allocator(profiler_allocator())
}
// restore_allocator restores the previous allocator
pub fn restore_allocator(old Allocator) {
pop_allocator(old)
}