jsdet-core 0.1.0

Core WASM-sandboxed JavaScript detonation engine
Documentation
/// Throughput benchmark — proves jsdet can handle 1M+ detonations.
///
/// This isn't a test that validates correctness — it's a test that
/// validates SCALE. If jsdet can't do 1M detonations on one machine
/// in reasonable time, it's not ready.
use std::sync::Arc;
use std::time::Instant;

use jsdet_core::{CompiledModule, EmptyBridge, SandboxConfig};

/// Simple script detonation — baseline throughput.
#[test]
fn throughput_simple_scripts() {
    let module = CompiledModule::new().unwrap();
    let bridge = Arc::new(EmptyBridge);
    let config = SandboxConfig {
        timeout_ms: 100,
        max_fuel: 10_000_000,
        drain_timers: false,
        ..SandboxConfig::detonation()
    };

    let scripts = vec![
        "var x = 1 + 2;".to_string(),
        "var y = 'hello' + ' world';".to_string(),
        "var z = JSON.parse('{\"a\":1}');".to_string(),
    ];

    // Warm up.
    for _ in 0..3 {
        let _ = module.execute(&scripts, bridge.clone(), &config);
    }

    let iterations = 100;
    let start = Instant::now();
    for _ in 0..iterations {
        let result = module.execute(&scripts, bridge.clone(), &config).unwrap();
        assert_eq!(result.scripts_executed, 3);
    }
    let elapsed = start.elapsed();

    let per_detonation_us = elapsed.as_micros() / iterations;
    let detonations_per_second = 1_000_000 / per_detonation_us.max(1);
    let detonations_per_hour = detonations_per_second * 3600;

    eprintln!();
    eprintln!("=== Throughput: Simple Scripts ===");
    eprintln!("  Iterations:    {iterations}");
    eprintln!("  Total:         {:.1}ms", elapsed.as_millis());
    eprintln!("  Per detonation: {}us", per_detonation_us);
    eprintln!(
        "  Rate:          {}/sec ({}/hour)",
        detonations_per_second, detonations_per_hour
    );
    eprintln!();

    // Must achieve at least 100 detonations/second.
    assert!(
        detonations_per_second >= 100,
        "throughput too low: {detonations_per_second}/sec (need 100+)"
    );
}

/// Realistic phishing script — what actually gets detonated.
#[test]
fn throughput_phishing_scripts() {
    let module = CompiledModule::new().unwrap();
    let bridge = Arc::new(jsdet_core::EmptyBridge);
    let config = SandboxConfig {
        timeout_ms: 200,
        max_fuel: 50_000_000,
        drain_timers: false,
        ..SandboxConfig::detonation()
    };

    // Realistic phishing kit behavior — pure JS, no browser APIs.
    let script = r#"
        var config = JSON.parse('{"redirect":"https://evil.com","token":"abc123"}');
        var target = config.redirect;
        var parts = target.split('/');
        var domain = parts.length > 2 ? parts[2] : 'unknown';
        var encoded = '';
        for (var i = 0; i < target.length; i++) {
            encoded += String.fromCharCode(target.charCodeAt(i) ^ 42);
        }
        for (var i = 0; i < 10; i++) {
            var key = 'item_' + i;
            var val = domain + '_' + i;
        }
        var result = { domain: domain, token: config.token, encoded: encoded };
        JSON.stringify(result);
    "#
    .to_string();

    // Warm up.
    for _ in 0..3 {
        let _ = module.execute(&[script.clone()], bridge.clone(), &config);
    }

    let iterations = 100;
    let start = Instant::now();
    for _ in 0..iterations {
        let result = module
            .execute(&[script.clone()], bridge.clone(), &config)
            .unwrap();
        assert_eq!(result.scripts_executed, 1);
        assert!(result.errors.is_empty());
    }
    let elapsed = start.elapsed();

    let per_detonation_us = elapsed.as_micros() / iterations;
    let detonations_per_second = 1_000_000 / per_detonation_us.max(1);

    eprintln!();
    eprintln!("=== Throughput: Phishing Scripts ===");
    eprintln!("  Iterations:    {iterations}");
    eprintln!("  Total:         {:.1}ms", elapsed.as_millis());
    eprintln!("  Per detonation: {}us", per_detonation_us);
    eprintln!("  Rate:          {}/sec", detonations_per_second);
    eprintln!(
        "  Projected 1M:  {:.1} minutes",
        1_000_000.0 / detonations_per_second as f64 / 60.0
    );
    eprintln!();

    assert!(
        detonations_per_second >= 50,
        "throughput too low: {detonations_per_second}/sec"
    );
}

/// Module compilation benchmark — how fast is startup?
#[test]
fn benchmark_module_compilation() {
    let start = Instant::now();
    let module = CompiledModule::new().unwrap();
    let compilation_ms = start.elapsed().as_millis();

    // Serialize and deserialize.
    let serialized = module.serialize().unwrap();
    let serialize_ms = start.elapsed().as_millis() - compilation_ms;

    let deser_start = Instant::now();
    let _cached = CompiledModule::load_cached(&serialized).unwrap();
    let deserialize_ms = deser_start.elapsed().as_millis();

    eprintln!();
    eprintln!("=== Module Compilation ===");
    eprintln!("  Fresh compile:   {}ms", compilation_ms);
    eprintln!("  Serialize:       {}ms", serialize_ms);
    eprintln!("  Load cached:     {}ms", deserialize_ms);
    eprintln!(
        "  Speedup:         {:.0}x",
        compilation_ms as f64 / deserialize_ms.max(1) as f64
    );
    eprintln!("  Serialized size: {} KB", serialized.len() / 1024);
    eprintln!();
}

/// Memory usage — how much does one instance cost?
#[test]
fn benchmark_memory_per_instance() {
    let module = CompiledModule::new().unwrap();
    let bridge = Arc::new(EmptyBridge);
    let config = SandboxConfig::detonation();

    // Execute a script to force memory allocation.
    let result = module
        .execute(
            &[
                "var x = 'a'.repeat(10000); var arr = []; for(var i=0;i<100;i++) arr.push(x+i);"
                    .into(),
            ],
            bridge,
            &config,
        )
        .unwrap();

    assert!(result.errors.is_empty());
    eprintln!();
    eprintln!("=== Memory Per Instance ===");
    eprintln!("  Execution completed in {}us", result.duration_us);
    eprintln!("  Observations: {}", result.observations.len());
    eprintln!();
}