Skip to content

Fix memory leak#13

Merged
dak2 merged 1 commit intomainfrom
fix-memory-leak
Feb 5, 2026
Merged

Fix memory leak#13
dak2 merged 1 commit intomainfrom
fix-memory-leak

Conversation

@dak2
Copy link
Owner

@dak2 dak2 commented Feb 4, 2026

Add arena allocator to fix memory leak.

Before the fix, Box::leak was causing a memory leak because it utilized a 'static lifetime, abandoning ownership of heap memory,
which would then not be freed until program termination.

The problem may become more serious when a large number of files are inspected.

Therefore, I utilized the arena allocator mechanism to free up memory.

//! Memory leak comparison test
//!

#[allow(deprecated)]
use methodray_core::parser::{parse_ruby_source, ParseSession};
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};

struct TrackingAllocator;

static ALLOCATED: AtomicUsize = AtomicUsize::new(0);
static DEALLOCATED: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for TrackingAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        let ptr = System.alloc(layout);
        if !ptr.is_null() {
            ALLOCATED.fetch_add(layout.size(), Ordering::SeqCst);
        }
        ptr
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        System.dealloc(ptr, layout);
        DEALLOCATED.fetch_add(layout.size(), Ordering::SeqCst);
    }

    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
        let new_ptr = System.realloc(ptr, layout, new_size);
        if !new_ptr.is_null() {
            DEALLOCATED.fetch_add(layout.size(), Ordering::SeqCst);
            ALLOCATED.fetch_add(new_size, Ordering::SeqCst);
        }
        new_ptr
    }
}

#[global_allocator]
static GLOBAL: TrackingAllocator = TrackingAllocator;

fn get_stats() -> (usize, usize, usize) {
    let allocated = ALLOCATED.load(Ordering::SeqCst);
    let deallocated = DEALLOCATED.load(Ordering::SeqCst);
    let leaked = allocated.saturating_sub(deallocated);
    (allocated, deallocated, leaked)
}

fn format_bytes(bytes: usize) -> String {
    if bytes >= 1024 * 1024 {
        format!("{:.2} MB", bytes as f64 / (1024.0 * 1024.0))
    } else if bytes >= 1024 {
        format!("{:.2} KB", bytes as f64 / 1024.0)
    } else {
        format!("{} bytes", bytes)
    }
}

fn main() {
    println!("==============================================");
    println!("  Strict Memory Leak Comparison Test");
    println!("==============================================\n");

    let source = r#"
class User
  def initialize(name)
    @name = name
  end
  def greet
    "Hello, #{@name}!"
  end
end
"#.repeat(100); // ~10KB per file

    let iterations = 100;

    println!("Source size: {} bytes", source.len());
    println!("Iterations: {}", iterations);
    println!();

    // ===== Test 1: Box::leak (old API) =====
    println!("===== Test 1: Box::leak (OLD API) =====");
    let (_, _, baseline) = get_stats();

    for i in 0..iterations {
        #[allow(deprecated)]
        let _ = parse_ruby_source(&source, format!("file_{}.rb", i));
    }

    let (_, _, after_boxleak) = get_stats();
    let boxleak_leaked = after_boxleak - baseline;

    println!("  Memory leaked: {}", format_bytes(boxleak_leaked));
    println!("  Expected (source × iterations): {}", format_bytes(source.len() * iterations));
    println!();

    // ===== Test 2: ParseSession - single session =====
    println!("===== Test 2: ParseSession (single session) =====");
    let (_, _, before_session) = get_stats();

    {
        let session = ParseSession::new();
        for i in 0..iterations {
            let _ = session.parse_source(&source, &format!("file_{}.rb", i));
        }
        let (_, _, during_session) = get_stats();
        println!("  During session (before drop): {}", format_bytes(during_session - before_session));
    } // session drops here

    let (_, _, after_session) = get_stats();
    let session_leaked = after_session - before_session;

    println!("  After session drop: {}", format_bytes(session_leaked));
    println!();

    // ===== Test 3: ParseSession - multiple sessions =====
    println!("===== Test 3: ParseSession (100 separate sessions) =====");
    let (_, _, before_multi) = get_stats();

    for i in 0..iterations {
        let session = ParseSession::new();
        let _ = session.parse_source(&source, &format!("file_{}.rb", i));
        // session drops each iteration
    }

    let (_, _, after_multi) = get_stats();
    let multi_leaked = after_multi - before_multi;

    println!("  Memory leaked after all sessions: {}", format_bytes(multi_leaked));
    println!();

    // ===== Test 4: Repeated cycles to check accumulation =====
    println!("===== Test 4: Memory accumulation check (5 cycles × 100 files) =====");

    for cycle in 0..5 {
        let (_, _, before_cycle) = get_stats();

        // Box::leak
        for i in 0..100 {
            #[allow(deprecated)]
            let _ = parse_ruby_source(&source, format!("leak_{}_{}.rb", cycle, i));
        }

        let (_, _, after_leak) = get_stats();
        let leak_growth = after_leak - before_cycle;

        // ParseSession
        {
            let session = ParseSession::new();
            for i in 0..100 {
                let _ = session.parse_source(&source, &format!("arena_{}_{}.rb", cycle, i));
            }
        }

        let (_, _, after_arena) = get_stats();
        let arena_growth = after_arena - after_leak;

        println!("  Cycle {}: Box::leak +{}, ParseSession +{}",
            cycle, format_bytes(leak_growth), format_bytes(arena_growth));
    }

    println!();
    println!("==============================================");
    println!("  Final Summary");
    println!("==============================================");
    let (total_alloc, total_dealloc, total_leaked) = get_stats();
    println!();
    println!("Total allocated:   {}", format_bytes(total_alloc));
    println!("Total deallocated: {}", format_bytes(total_dealloc));
    println!("Total leaked:      {}", format_bytes(total_leaked));
    println!();
}
dak2 method-ray (fix-memory-leak) % cargo run --release --example strict_leak_test 2>&1
   Compiling methodray-core v0.1.0 (/Users/dak2/Dev/Sources/method-ray/rust)
    Finished `release` profile [optimized] target(s) in 0.19s
     Running `target/release/examples/strict_leak_test`
==============================================
  Strict Memory Leak Comparison Test
==============================================

Source size: 10300 bytes
Iterations: 100

===== Test 1: Box::leak (OLD API) =====
  Memory leaked: 1005.86 KB
  Expected (source × iterations): 1005.86 KB

===== Test 2: ParseSession (single session) =====
  During session (before drop): 1.49 MB
  After session drop: 0 bytes

===== Test 3: ParseSession (100 separate sessions) =====
  Memory leaked after all sessions: 0 bytes

===== Test 4: Memory accumulation check (5 cycles × 100 files) =====
  Cycle 0: Box::leak +1005.86 KB, ParseSession +0 bytes
  Cycle 1: Box::leak +1005.86 KB, ParseSession +0 bytes
  Cycle 2: Box::leak +1005.86 KB, ParseSession +0 bytes
  Cycle 3: Box::leak +1005.86 KB, ParseSession +0 bytes
  Cycle 4: Box::leak +1005.86 KB, ParseSession +0 bytes

==============================================
  Final Summary
==============================================

Total allocated:   16.91 MB
Total deallocated: 11.01 MB
Total leaked:      5.91 MB

Add arena allocator to fix memory leak.

Before the fix, `Box::leak` was causing a memory leak because it utilized a `'static` lifetime,
abandoning ownership of heap memory,
which would then not be freed until program termination.

The problem may become more serious when a large number of files are inspected.

Therefore, I utilized the arena allocator mechanism to free up memory.
@dak2 dak2 merged commit 8bbda00 into main Feb 5, 2026
2 checks passed
@dak2 dak2 deleted the fix-memory-leak branch February 5, 2026 13:03
@dak2 dak2 mentioned this pull request Feb 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant