Progressive Loading

Status: Verified | Idempotent: Yes | Coverage: 95%+

Run Command

cargo run --example wasm_progressive_loading

Code

//! # Recipe: Progressive Model Loading
//!
//! Contract: contracts/recipe-iiur-v1.yaml
//! **Category**: WASM/Browser
//! **Isolation Level**: Full
//! **Idempotency**: Guaranteed
//! **Dependencies**: None (default features)
//!
//! ## QA Checklist
//! 1. [x] `cargo run` succeeds (Exit Code 0)
//! 2. [x] `cargo test` passes
//! 3. [x] Deterministic output (Verified)
//! 4. [x] No temp files leaked
//! 5. [x] Memory usage stable
//! 6. [x] WASM compatible (Verified)
//! 7. [x] Clippy clean
//! 8. [x] Rustfmt standard
//! 9. [x] No `unwrap()` in logic
//! 10. [x] Proptests pass (100+ cases)
//!
//! ## Learning Objective
//! Load model progressively with UI feedback.
//!
//! ## Run Command
//! ```bash
//! cargo run --example wasm_progressive_loading
//! ```
//!
//!
//! ## Format Variants
//! ```bash
//! apr run model.apr          # APR native format
//! apr run model.gguf         # GGUF (llama.cpp compatible)
//! apr run model.safetensors  # SafeTensors (HuggingFace)
//! ```
//! ## References
//! - Haas, A. et al. (2017). *Bringing the Web up to Speed with WebAssembly*. PLDI. DOI: 10.1145/3062341.3062363

use apr_cookbook::prelude::*;
use serde::{Deserialize, Serialize};

fn main() -> Result<()> {
    let mut ctx = RecipeContext::new("wasm_progressive_loading")?;

    println!("=== Recipe: {} ===", ctx.name());
    println!("Progressive model loading simulation");
    println!();

    // Define model chunks
    let chunks = vec![
        ModelChunk {
            id: 0,
            name: "metadata".to_string(),
            size_kb: 5,
            required: true,
        },
        ModelChunk {
            id: 1,
            name: "embeddings".to_string(),
            size_kb: 200,
            required: true,
        },
        ModelChunk {
            id: 2,
            name: "layer_0".to_string(),
            size_kb: 150,
            required: true,
        },
        ModelChunk {
            id: 3,
            name: "layer_1".to_string(),
            size_kb: 150,
            required: true,
        },
        ModelChunk {
            id: 4,
            name: "layer_2".to_string(),
            size_kb: 150,
            required: true,
        },
        ModelChunk {
            id: 5,
            name: "output".to_string(),
            size_kb: 50,
            required: true,
        },
        ModelChunk {
            id: 6,
            name: "cache".to_string(),
            size_kb: 100,
            required: false,
        },
    ];

    let total_size: u32 = chunks.iter().map(|c| c.size_kb).sum();
    ctx.record_metric("total_chunks", chunks.len() as i64);
    ctx.record_metric("total_size_kb", i64::from(total_size));

    println!("Model chunks:");
    for chunk in &chunks {
        let required = if chunk.required {
            "[required]"
        } else {
            "[optional]"
        };
        println!("  {} ({}KB) {}", chunk.name, chunk.size_kb, required);
    }
    println!("  Total: {}KB", total_size);
    println!();

    // Progressive loading simulation
    let mut loader = ProgressiveLoader::new(chunks.clone());

    println!("Loading progress:");
    println!("{:-<50}", "");

    while !loader.is_complete() {
        let progress = loader.load_next()?;
        let bar = create_progress_bar(progress.percent, 30);
        println!(
            "  {} {:>3}% [{}] {}",
            progress.chunk_name, progress.percent, bar, progress.status
        );
    }
    println!("{:-<50}", "");

    // Loading statistics
    let stats = loader.get_stats();
    ctx.record_metric("load_time_ms", i64::from(stats.total_time_ms));
    ctx.record_float_metric("throughput_kbps", stats.throughput_kbps);

    println!();
    println!("Loading complete:");
    println!("  Total time: {}ms", stats.total_time_ms);
    println!("  Throughput: {:.1}KB/s", stats.throughput_kbps);
    println!(
        "  Chunks loaded: {}/{}",
        stats.chunks_loaded, stats.chunks_total
    );

    // Demonstrate early inference capability
    println!();
    println!("Early inference capability:");
    let min_required = loader.get_minimum_usable_chunks();
    println!("  Minimum chunks for inference: {}", min_required);
    println!(
        "  Can run basic inference after {}KB loaded",
        chunks
            .iter()
            .take(min_required)
            .map(|c| c.size_kb)
            .sum::<u32>()
    );

    // Save loading log
    let log_path = ctx.path("loading_log.json");
    loader.save_log(&log_path)?;
    println!();
    println!("Loading log saved to: {:?}", log_path);

    Ok(())
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ModelChunk {
    id: u32,
    name: String,
    size_kb: u32,
    required: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct LoadProgress {
    chunk_name: String,
    percent: u32,
    bytes_loaded: u32,
    bytes_total: u32,
    status: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct LoadStats {
    total_time_ms: u32,
    throughput_kbps: f64,
    chunks_loaded: usize,
    chunks_total: usize,
}

#[derive(Debug)]
struct ProgressiveLoader {
    chunks: Vec<ModelChunk>,
    loaded: Vec<bool>,
    bytes_loaded: u32,
    bytes_total: u32,
    current_idx: usize,
    log: Vec<LoadProgress>,
}

impl ProgressiveLoader {
    fn new(chunks: Vec<ModelChunk>) -> Self {
        let bytes_total: u32 = chunks.iter().map(|c| c.size_kb * 1024).sum();
        let loaded = vec![false; chunks.len()];

        Self {
            chunks,
            loaded,
            bytes_loaded: 0,
            bytes_total,
            current_idx: 0,
            log: Vec::new(),
        }
    }

    fn load_next(&mut self) -> Result<LoadProgress> {
        if self.current_idx >= self.chunks.len() {
            return Err(CookbookError::invalid_format(
                "All chunks already loaded".to_string(),
            ));
        }

        let chunk = &self.chunks[self.current_idx];
        self.bytes_loaded += chunk.size_kb * 1024;
        self.loaded[self.current_idx] = true;

        let percent = ((f64::from(self.bytes_loaded) / f64::from(self.bytes_total)) * 100.0) as u32;

        let progress = LoadProgress {
            chunk_name: chunk.name.clone(),
            percent,
            bytes_loaded: self.bytes_loaded,
            bytes_total: self.bytes_total,
            status: "loaded".to_string(),
        };

        self.log.push(progress.clone());
        self.current_idx += 1;

        Ok(progress)
    }

    fn is_complete(&self) -> bool {
        self.current_idx >= self.chunks.len()
    }

    fn get_stats(&self) -> LoadStats {
        // Deterministic simulated time: 1ms per KB
        let total_time = self.bytes_loaded / 1024;
        let throughput = if total_time > 0 {
            (f64::from(self.bytes_loaded) / 1024.0) / (f64::from(total_time) / 1000.0)
        } else {
            0.0
        };

        LoadStats {
            total_time_ms: total_time,
            throughput_kbps: throughput,
            chunks_loaded: self.loaded.iter().filter(|&&l| l).count(),
            chunks_total: self.chunks.len(),
        }
    }

    fn get_minimum_usable_chunks(&self) -> usize {
        self.chunks.iter().take_while(|c| c.required).count() + 1
    }

    fn save_log(&self, path: &std::path::Path) -> Result<()> {
        let json = serde_json::to_string_pretty(&self.log)
            .map_err(|e| CookbookError::Serialization(e.to_string()))?;
        std::fs::write(path, json)?;
        Ok(())
    }
}

fn create_progress_bar(percent: u32, width: usize) -> String {
    let filled = (percent as usize * width) / 100;
    let empty = width - filled;
    format!("{}{}", "=".repeat(filled), " ".repeat(empty))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_loader_creation() {
        let chunks = vec![ModelChunk {
            id: 0,
            name: "test".to_string(),
            size_kb: 100,
            required: true,
        }];

        let loader = ProgressiveLoader::new(chunks);

        assert!(!loader.is_complete());
        assert_eq!(loader.bytes_loaded, 0);
    }

    #[test]
    fn test_load_next() {
        let chunks = vec![ModelChunk {
            id: 0,
            name: "chunk1".to_string(),
            size_kb: 100,
            required: true,
        }];

        let mut loader = ProgressiveLoader::new(chunks);
        let progress = loader.load_next().unwrap();

        assert_eq!(progress.chunk_name, "chunk1");
        assert_eq!(progress.percent, 100);
        assert!(loader.is_complete());
    }

    #[test]
    fn test_progressive_percent() {
        let chunks = vec![
            ModelChunk {
                id: 0,
                name: "c1".to_string(),
                size_kb: 50,
                required: true,
            },
            ModelChunk {
                id: 1,
                name: "c2".to_string(),
                size_kb: 50,
                required: true,
            },
        ];

        let mut loader = ProgressiveLoader::new(chunks);

        let p1 = loader.load_next().unwrap();
        assert_eq!(p1.percent, 50);

        let p2 = loader.load_next().unwrap();
        assert_eq!(p2.percent, 100);
    }

    #[test]
    fn test_load_complete_error() {
        let chunks = vec![ModelChunk {
            id: 0,
            name: "test".to_string(),
            size_kb: 100,
            required: true,
        }];

        let mut loader = ProgressiveLoader::new(chunks);
        loader.load_next().unwrap();

        let result = loader.load_next();
        assert!(result.is_err());
    }

    #[test]
    fn test_get_stats() {
        let chunks = vec![ModelChunk {
            id: 0,
            name: "test".to_string(),
            size_kb: 100,
            required: true,
        }];

        let mut loader = ProgressiveLoader::new(chunks);
        loader.load_next().unwrap();

        let stats = loader.get_stats();
        assert_eq!(stats.chunks_loaded, 1);
        assert_eq!(stats.chunks_total, 1);
    }

    #[test]
    fn test_progress_bar() {
        assert_eq!(create_progress_bar(50, 10), "=====     ");
        assert_eq!(create_progress_bar(100, 10), "==========");
        assert_eq!(create_progress_bar(0, 10), "          ");
    }
}

#[cfg(test)]
mod proptests {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        #![proptest_config(ProptestConfig::with_cases(100))]

        #[test]
        fn prop_final_percent_is_100(sizes in proptest::collection::vec(1u32..100, 1..10)) {
            let chunks: Vec<_> = sizes.iter().enumerate().map(|(i, &size)| {
                ModelChunk {
                    id: i as u32,
                    name: format!("chunk{}", i),
                    size_kb: size,
                    required: true,
                }
            }).collect();

            let mut loader = ProgressiveLoader::new(chunks);
            let mut last_progress = None;

            while !loader.is_complete() {
                last_progress = Some(loader.load_next().unwrap());
            }

            if let Some(progress) = last_progress {
                prop_assert_eq!(progress.percent, 100);
            }
        }

        #[test]
        fn prop_percent_monotonic(sizes in proptest::collection::vec(1u32..50, 2..5)) {
            let chunks: Vec<_> = sizes.iter().enumerate().map(|(i, &size)| {
                ModelChunk {
                    id: i as u32,
                    name: format!("chunk{}", i),
                    size_kb: size,
                    required: true,
                }
            }).collect();

            let mut loader = ProgressiveLoader::new(chunks);
            let mut last_percent = 0u32;

            while !loader.is_complete() {
                let progress = loader.load_next().unwrap();
                prop_assert!(progress.percent >= last_percent);
                last_percent = progress.percent;
            }
        }
    }
}