ruvector-scipix 2.0.4

Rust OCR engine for scientific documents - extract LaTeX, MathML from math equations, research papers, and technical diagrams with ONNX GPU acceleration
Documentation
// Performance validation tests
//
// Tests latency, memory usage, throughput, and ensures no memory leaks

use super::*;
use std::time::{Duration, Instant};
use tokio;

#[tokio::test]
async fn test_performance_latency_within_bounds() {
    let test_server = TestServer::start()
        .await
        .expect("Failed to start test server");

    let image = images::generate_simple_equation("x + y");
    image.save("/tmp/perf_latency.png").unwrap();

    // Measure latency
    let start = Instant::now();
    let result = test_server
        .process_image("/tmp/perf_latency.png", OutputFormat::LaTeX)
        .await
        .expect("Processing failed");
    let latency = start.elapsed();

    println!("Latency: {:?}", latency);
    println!("Confidence: {}", result.confidence);

    // Assert latency is within bounds (<100ms for simple equation)
    assert!(latency.as_millis() < 100, "Latency too high: {:?}", latency);

    test_server.shutdown().await;
}

#[tokio::test]
async fn test_performance_memory_usage_limits() {
    let test_server = TestServer::start()
        .await
        .expect("Failed to start test server");

    // Get initial memory usage
    let initial_memory = get_memory_usage();

    // Process multiple images
    for i in 0..100 {
        let eq = format!("x + {}", i);
        let image = images::generate_simple_equation(&eq);
        let path = format!("/tmp/perf_mem_{}.png", i);
        image.save(&path).unwrap();

        test_server
            .process_image(&path, OutputFormat::LaTeX)
            .await
            .expect("Processing failed");

        // Clean up
        std::fs::remove_file(&path).unwrap();
    }

    // Get final memory usage
    let final_memory = get_memory_usage();
    let memory_increase = final_memory - initial_memory;

    println!("Memory increase: {} MB", memory_increase / 1024 / 1024);

    // Assert memory usage is reasonable (<100MB increase)
    assert!(
        memory_increase < 100 * 1024 * 1024,
        "Memory usage too high: {} bytes",
        memory_increase
    );

    test_server.shutdown().await;
}

#[tokio::test]
async fn test_performance_no_memory_leaks() {
    let test_server = TestServer::start()
        .await
        .expect("Failed to start test server");

    let image = images::generate_simple_equation("leak test");
    image.save("/tmp/leak_test.png").unwrap();

    // Process same image many times
    let iterations = 1000;
    let mut memory_samples = Vec::new();

    for i in 0..iterations {
        test_server
            .process_image("/tmp/leak_test.png", OutputFormat::LaTeX)
            .await
            .expect("Processing failed");

        // Sample memory every 100 iterations
        if i % 100 == 0 {
            memory_samples.push(get_memory_usage());
        }
    }

    // Check for linear memory growth (leak indicator)
    let first_sample = memory_samples[0];
    let last_sample = *memory_samples.last().unwrap();
    let growth_rate = (last_sample - first_sample) as f64 / iterations as f64;

    println!("Memory growth rate: {} bytes/iteration", growth_rate);
    println!("Samples: {:?}", memory_samples);

    // Growth rate should be minimal (<1KB per iteration)
    assert!(
        growth_rate < 1024.0,
        "Possible memory leak detected: {} bytes/iteration",
        growth_rate
    );

    test_server.shutdown().await;
}

#[tokio::test]
async fn test_performance_throughput() {
    let test_server = TestServer::start()
        .await
        .expect("Failed to start test server");

    // Create test images
    let image_count = 50;
    for i in 0..image_count {
        let eq = format!("throughput_{}", i);
        let image = images::generate_simple_equation(&eq);
        image.save(&format!("/tmp/throughput_{}.png", i)).unwrap();
    }

    // Measure throughput
    let start = Instant::now();

    for i in 0..image_count {
        test_server
            .process_image(&format!("/tmp/throughput_{}.png", i), OutputFormat::LaTeX)
            .await
            .expect("Processing failed");
    }

    let duration = start.elapsed();
    let throughput = image_count as f64 / duration.as_secs_f64();

    println!("Throughput: {:.2} images/second", throughput);
    println!("Total time: {:?} for {} images", duration, image_count);

    // Assert reasonable throughput (>5 images/second)
    assert!(
        throughput > 5.0,
        "Throughput too low: {:.2} images/s",
        throughput
    );

    // Cleanup
    for i in 0..image_count {
        std::fs::remove_file(&format!("/tmp/throughput_{}.png", i)).unwrap();
    }

    test_server.shutdown().await;
}

#[tokio::test]
async fn test_performance_concurrent_throughput() {
    let test_server = TestServer::start()
        .await
        .expect("Failed to start test server");

    // Create test image
    let image = images::generate_simple_equation("concurrent");
    image.save("/tmp/concurrent_throughput.png").unwrap();

    let concurrent_requests = 20;
    let start = Instant::now();

    // Spawn concurrent requests
    let mut handles = vec![];
    for _ in 0..concurrent_requests {
        let server = test_server.clone();
        let handle = tokio::spawn(async move {
            server
                .process_image("/tmp/concurrent_throughput.png", OutputFormat::LaTeX)
                .await
        });
        handles.push(handle);
    }

    // Wait for all to complete
    let results = futures::future::join_all(handles).await;
    let duration = start.elapsed();

    let success_count = results.iter().filter(|r| r.is_ok()).count();
    let throughput = concurrent_requests as f64 / duration.as_secs_f64();

    println!("Concurrent throughput: {:.2} req/second", throughput);
    println!("Success rate: {}/{}", success_count, concurrent_requests);

    assert!(
        success_count == concurrent_requests,
        "All requests should succeed"
    );
    assert!(
        throughput > 10.0,
        "Concurrent throughput too low: {:.2}",
        throughput
    );

    test_server.shutdown().await;
}

#[tokio::test]
async fn test_performance_latency_percentiles() {
    let test_server = TestServer::start()
        .await
        .expect("Failed to start test server");

    let iterations = 100;
    let mut latencies = Vec::new();

    for i in 0..iterations {
        let eq = format!("p{}", i);
        let image = images::generate_simple_equation(&eq);
        let path = format!("/tmp/percentile_{}.png", i);
        image.save(&path).unwrap();

        let start = Instant::now();
        test_server
            .process_image(&path, OutputFormat::LaTeX)
            .await
            .expect("Processing failed");
        let latency = start.elapsed();

        latencies.push(latency.as_micros());

        std::fs::remove_file(&path).unwrap();
    }

    // Sort latencies
    latencies.sort();

    // Calculate percentiles
    let p50 = latencies[50];
    let p95 = latencies[95];
    let p99 = latencies[99];

    println!("Latency percentiles:");
    println!("  P50: {} μs ({} ms)", p50, p50 / 1000);
    println!("  P95: {} μs ({} ms)", p95, p95 / 1000);
    println!("  P99: {} μs ({} ms)", p99, p99 / 1000);

    // Assert percentile targets
    assert!(p50 < 100_000, "P50 latency too high: {} μs", p50); // <100ms
    assert!(p95 < 200_000, "P95 latency too high: {} μs", p95); // <200ms
    assert!(p99 < 500_000, "P99 latency too high: {} μs", p99); // <500ms

    test_server.shutdown().await;
}

#[tokio::test]
async fn test_performance_batch_efficiency() {
    let test_server = TestServer::start()
        .await
        .expect("Failed to start test server");

    // Create test images
    let batch_size = 10;
    let mut paths = Vec::new();

    for i in 0..batch_size {
        let eq = format!("batch_{}", i);
        let image = images::generate_simple_equation(&eq);
        let path = format!("/tmp/batch_eff_{}.png", i);
        image.save(&path).unwrap();
        paths.push(path);
    }

    // Measure sequential processing
    let start_sequential = Instant::now();
    for path in &paths {
        test_server
            .process_image(path, OutputFormat::LaTeX)
            .await
            .expect("Processing failed");
    }
    let sequential_time = start_sequential.elapsed();

    // Measure batch processing
    let start_batch = Instant::now();
    test_server
        .process_batch(
            &paths.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
            OutputFormat::LaTeX,
        )
        .await
        .expect("Batch processing failed");
    let batch_time = start_batch.elapsed();

    println!("Sequential time: {:?}", sequential_time);
    println!("Batch time: {:?}", batch_time);
    println!(
        "Speedup: {:.2}x",
        sequential_time.as_secs_f64() / batch_time.as_secs_f64()
    );

    // Batch should be faster
    assert!(
        batch_time < sequential_time,
        "Batch processing should be faster"
    );

    // Cleanup
    for path in paths {
        std::fs::remove_file(&path).unwrap();
    }

    test_server.shutdown().await;
}

#[tokio::test]
async fn test_performance_cold_start_warmup() {
    // Measure cold start
    let start_cold = Instant::now();
    let test_server = TestServer::start()
        .await
        .expect("Failed to start test server");
    let cold_start_time = start_cold.elapsed();

    println!("Cold start time: {:?}", cold_start_time);

    // First request (warmup)
    let image = images::generate_simple_equation("warmup");
    image.save("/tmp/warmup.png").unwrap();

    let start_first = Instant::now();
    test_server
        .process_image("/tmp/warmup.png", OutputFormat::LaTeX)
        .await
        .expect("Processing failed");
    let first_request_time = start_first.elapsed();

    // Second request (warmed up)
    let start_second = Instant::now();
    test_server
        .process_image("/tmp/warmup.png", OutputFormat::LaTeX)
        .await
        .expect("Processing failed");
    let second_request_time = start_second.elapsed();

    println!("First request time: {:?}", first_request_time);
    println!("Second request time: {:?}", second_request_time);

    // Cold start should be reasonable (<5s)
    assert!(
        cold_start_time.as_secs() < 5,
        "Cold start too slow: {:?}",
        cold_start_time
    );

    // Second request should be faster (model loaded)
    assert!(
        second_request_time < first_request_time,
        "Warmed up request should be faster"
    );

    test_server.shutdown().await;
}

// Helper function to get current memory usage
fn get_memory_usage() -> usize {
    #[cfg(target_os = "linux")]
    {
        // Read from /proc/self/statm
        if let Ok(content) = std::fs::read_to_string("/proc/self/statm") {
            if let Some(rss) = content.split_whitespace().nth(1) {
                if let Ok(pages) = rss.parse::<usize>() {
                    // Convert pages to bytes (assuming 4KB pages)
                    return pages * 4096;
                }
            }
        }
    }

    // Fallback for other platforms or if reading fails
    0
}