panasyn 0.1.0

A lightweight GPU-accelerated terminal emulator for macOS and Linux.
use std::alloc::{GlobalAlloc, Layout};
use std::sync::atomic::{AtomicU64, Ordering};

/// Global allocation counters.
static ALLOC_COUNT: AtomicU64 = AtomicU64::new(0);
static BYTES_ALLOCATED: AtomicU64 = AtomicU64::new(0);
static BYTES_DEALLOCATED: AtomicU64 = AtomicU64::new(0);
static ALLOC_COUNT_PEAK: AtomicU64 = AtomicU64::new(0);

/// A global allocator wrapper that counts allocations.
/// Use with `#[global_allocator]` for profiling builds.
pub struct CountingAllocator<A: GlobalAlloc>(pub A);

// SAFETY: CountingAllocator preserves the GlobalAlloc contract by forwarding
// each allocation operation to the wrapped allocator with the original pointers
// and layouts, while only updating atomic counters around those calls.
unsafe impl<A: GlobalAlloc> GlobalAlloc for CountingAllocator<A> {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
        let size = layout.size() as u64;
        let prev = BYTES_ALLOCATED.fetch_add(size, Ordering::Relaxed);
        let total = prev + size;
        let deallocd = BYTES_DEALLOCATED.load(Ordering::Relaxed);
        let current = total.saturating_sub(deallocd);
        let peak = ALLOC_COUNT_PEAK.load(Ordering::Relaxed);
        if current > peak {
            ALLOC_COUNT_PEAK.store(current, Ordering::Relaxed);
        }
        // SAFETY: The caller upholds GlobalAlloc::alloc requirements; this wrapper
        // forwards the exact same layout to the inner allocator.
        unsafe { self.0.alloc(layout) }
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        BYTES_DEALLOCATED.fetch_add(layout.size() as u64, Ordering::Relaxed);
        // SAFETY: The caller provides a pointer/layout pair previously allocated
        // by this allocator; forwarding preserves the inner allocator contract.
        unsafe {
            self.0.dealloc(ptr, layout);
        }
    }

    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
        ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
        let size = layout.size() as u64;
        BYTES_ALLOCATED.fetch_add(size, Ordering::Relaxed);
        // SAFETY: The caller upholds GlobalAlloc::alloc_zeroed requirements; this
        // wrapper forwards the exact same layout to the inner allocator.
        unsafe { self.0.alloc_zeroed(layout) }
    }

    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
        ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
        let old_size = layout.size() as u64;
        let new_size_u64 = new_size as u64;
        if new_size_u64 > old_size {
            BYTES_ALLOCATED.fetch_add(new_size_u64 - old_size, Ordering::Relaxed);
        } else {
            BYTES_DEALLOCATED.fetch_add(old_size - new_size_u64, Ordering::Relaxed);
        }
        // SAFETY: The caller upholds GlobalAlloc::realloc requirements; this
        // wrapper forwards the original pointer/layout and requested size.
        unsafe { self.0.realloc(ptr, layout, new_size) }
    }
}

impl<A: GlobalAlloc> CountingAllocator<A> {
    pub fn reset() {
        ALLOC_COUNT.store(0, Ordering::Relaxed);
        BYTES_ALLOCATED.store(0, Ordering::Relaxed);
        BYTES_DEALLOCATED.store(0, Ordering::Relaxed);
        ALLOC_COUNT_PEAK.store(0, Ordering::Relaxed);
    }

    pub fn alloc_count() -> u64 {
        ALLOC_COUNT.load(Ordering::Relaxed)
    }

    pub fn bytes_allocated() -> u64 {
        BYTES_ALLOCATED.load(Ordering::Relaxed)
    }

    pub fn bytes_deallocated() -> u64 {
        BYTES_DEALLOCATED.load(Ordering::Relaxed)
    }

    pub fn bytes_current() -> i64 {
        BYTES_ALLOCATED.load(Ordering::Relaxed) as i64
            - BYTES_DEALLOCATED.load(Ordering::Relaxed) as i64
    }

    pub fn peak_bytes() -> u64 {
        ALLOC_COUNT_PEAK.load(Ordering::Relaxed)
    }
}

/// Per-operation allocation tracker.
/// Records allocation deltas around a specific operation.
#[derive(Debug, Clone, Default)]
pub struct OpAllocations {
    pub alloc_count: u64,
    pub bytes_allocated: u64,
}

/// Track allocations during a closure.
pub fn track_allocations<F, T>(f: F) -> (T, OpAllocations)
where
    F: FnOnce() -> T,
{
    let before = (
        ALLOC_COUNT.load(Ordering::Relaxed),
        BYTES_ALLOCATED.load(Ordering::Relaxed),
    );
    let result = f();
    let after = (
        ALLOC_COUNT.load(Ordering::Relaxed),
        BYTES_ALLOCATED.load(Ordering::Relaxed),
    );
    (
        result,
        OpAllocations {
            alloc_count: after.0 - before.0,
            bytes_allocated: after.1 - before.1,
        },
    )
}

/// Profile results for a replay run.
#[derive(Debug, Clone)]
pub struct ProfileResult {
    pub total_allocations: u64,
    pub total_bytes_allocated: u64,
    pub bytes_per_frame: u64,
    pub allocs_per_frame: u64,
    pub bytes_per_keypress: u64,
    pub allocs_per_keypress: u64,
    pub scroll_allocations: u64,
    pub scroll_bytes: u64,
    pub parser_allocations: u64,
    pub parser_bytes: u64,
    pub peak_memory_bytes: u64,
}

impl ProfileResult {
    pub fn print(&self) {
        println!("=== Allocation Profile ===");
        println!("  Total allocations:     {}", self.total_allocations);
        println!(
            "  Total bytes allocated: {} ({:.2} KB)",
            self.total_bytes_allocated,
            self.total_bytes_allocated as f64 / 1024.0
        );
        println!("  Allocs/frame:          {:.2}", self.allocs_per_frame);
        println!("  Bytes/frame:           {:.2}", self.bytes_per_frame);
        println!("  Allocs/keypress:       {:.2}", self.allocs_per_keypress);
        println!("  Bytes/keypress:        {:.2}", self.bytes_per_keypress);
        println!("  Scroll allocs:         {}", self.scroll_allocations);
        println!("  Scroll bytes:          {}", self.scroll_bytes);
        println!("  Parser allocs:         {}", self.parser_allocations);
        println!("  Parser bytes:          {}", self.parser_bytes);
        println!(
            "  Peak memory:           {} ({:.2} KB)",
            self.peak_memory_bytes,
            self.peak_memory_bytes as f64 / 1024.0
        );
    }
}

/// Print profiling integration guide.
pub fn print_profiling_guide() {
    println!("=== Profiling Guide ===");
    println!();
    println!("To enable allocation counting:");
    println!("  1. Add to src/main.rs:");
    println!("     use replay::profiling::CountingAllocator;");
    println!("     use std::alloc::System;");
    println!("     #[global_allocator]");
    println!("     static ALLOC: CountingAllocator<System> = CountingAllocator(System);");
    println!();
    println!("  2. Run with --profile flag to see allocation stats");
    println!();
    println!("External profilers:");
    println!(
        "  macOS Instruments: xcrun xctrace record --template 'Allocations' --launch ./target/release/panasyn"
    );
    println!("  Tracy:     Add tracy-client dependency and TRACY_ENABLE=1");
    println!("  heaptrack: heaptrack ./target/release/panasyn");
    println!("  dhat:      cargo build --features dhat-heap && DHAT=1 ./target/release/panasyn");
}