//! Host-RSS validation for the shared-golden-base memory dedup (measure-first).
//!
//! The page-overlap probe (`_mem_dedup_probe`) showed a same-image sibling
//! stores ~14% of its pages as a diff. This probe answers the question that
//! actually justifies the lifecycle work: when you RESTORE a golden + N diff
//! snapshots CONCURRENTLY, does host resident memory grow SUBLINEARLY (golden
//! resident once + ~diff-sized per sibling), because `restore_diff_from_file`
//! CoW-maps the shared golden inode? Or does each VM pay full freight?
//!
//! Method (embedded VMM → guest RAM lives in THIS process):
//!   1. Boot the image, snapshot a FULL golden (`vm.snap`).
//!   2. Boot N more, snapshot each full, then `rewrite_full_as_diff` vs golden.
//!   3. Restore golden + all N diffs, KEEPING every VM alive, recording
//!      /proc/self/status RssAnon after each restore.
//!   4. Report per-VM marginal RssAnon. Shared-golden → the first restore pays
//!      the base, later diff restores add only their changed pages.
//!
//! Usage: `_mem_dedup_rss_probe COLD_SNAP_DIR [N]`   (default N=3)

#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
fn main() {
    eprintln!("_mem_dedup_rss_probe: Linux/x86_64 (KVM) only");
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    use std::time::Duration;
    use supermachine::{Image, Vm, VmConfig};

    let mut args = std::env::args().skip(1);
    let Some(cold) = args.next() else {
        eprintln!("usage: _mem_dedup_rss_probe COLD_SNAP_DIR [N]");
        std::process::exit(2);
    };
    let n: usize = args.next().and_then(|s| s.parse().ok()).unwrap_or(3);

    // RssAnon (private/anonymous, in KiB) from /proc/self/status — this is the
    // memory NOT shared via the page cache. CoW-mapped golden pages that are
    // untouched stay in shared file-backed cache and do NOT count here; only
    // pages a VM actually wrote (diff overlay + dirtied-on-run) count.
    let rss_anon_kib = || -> u64 {
        std::fs::read_to_string("/proc/self/status")
            .ok()
            .and_then(|s| {
                s.lines()
                    .find(|l| l.starts_with("RssAnon:"))
                    .and_then(|l| l.split_whitespace().nth(1))
                    .and_then(|v| v.parse().ok())
            })
            .unwrap_or(0)
    };

    let base = Image::from_snapshot(&cold)?;
    let workdir = std::env::temp_dir().join(format!("sm-rss-dedup-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&workdir);
    std::fs::create_dir_all(&workdir)?;

    // Produce a golden full snapshot + N sibling diffs against it.
    let snap_full = |dest: &std::path::Path| -> Result<(), Box<dyn std::error::Error>> {
        let vm = base.start(&VmConfig::new())?;
        std::thread::sleep(Duration::from_secs(4));
        let _ = vm.snapshot(dest)?;
        Ok(())
    };

    let golden_dir = workdir.join("golden");
    eprintln!("snapshotting golden ...");
    snap_full(&golden_dir)?;
    let golden_snap = golden_dir.join("vm.snap");
    let full_bytes = std::fs::metadata(&golden_snap)?.len();

    let mut diff_dirs = Vec::new();
    for i in 0..n {
        let d = workdir.join(format!("sib{i}"));
        eprintln!("snapshotting sibling {i} ...");
        snap_full(&d)?;
        let diff_bytes =
            supermachine::kvm::run::rewrite_full_as_diff(&d.join("vm.snap"), &golden_snap)?;
        eprintln!(
            "  sib{i} diff = {:.1} MiB ({:.1}% of full)",
            diff_bytes as f64 / 1048576.0,
            100.0 * diff_bytes as f64 / full_bytes as f64
        );
        diff_dirs.push(d);
    }

    // Restore golden + all diffs concurrently, holding every VM alive.
    let mut live: Vec<Vm> = Vec::new();
    let mib = |kib: u64| kib as f64 / 1024.0;

    let before = rss_anon_kib();
    eprintln!(
        "\n=== concurrent restore (RssAnon; golden={:.0} MiB full on disk) ===",
        full_bytes as f64 / 1048576.0
    );
    eprintln!("baseline RssAnon: {:.1} MiB", mib(before));

    let g = Image::from_snapshot(&golden_dir)?.start(&VmConfig::new())?;
    live.push(g);
    let after_golden = rss_anon_kib();
    eprintln!(
        "after golden restore : RssAnon {:.1} MiB  (+{:.1} MiB)",
        mib(after_golden),
        mib(after_golden.saturating_sub(before))
    );

    let mut prev = after_golden;
    let mut diff_marginals = Vec::new();
    for (i, d) in diff_dirs.iter().enumerate() {
        let vm = Image::from_snapshot(d)?.start(&VmConfig::new())?;
        live.push(vm);
        let now = rss_anon_kib();
        let marginal = now.saturating_sub(prev);
        diff_marginals.push(marginal);
        eprintln!(
            "after diff #{i} restore: RssAnon {:.1} MiB  (+{:.1} MiB marginal)",
            mib(now),
            mib(marginal)
        );
        prev = now;
    }

    let total = prev.saturating_sub(before);
    let avg_diff = if diff_marginals.is_empty() {
        0
    } else {
        diff_marginals.iter().sum::<u64>() / diff_marginals.len() as u64
    };
    eprintln!(
        "\n{} VMs live (1 golden + {} diffs).\n\
         total RssAnon growth: {:.1} MiB\n\
         avg marginal per diff VM: {:.1} MiB  (vs {:.0} MiB full guest)\n\
         => each extra same-image VM costs ~{:.0}% of a full guest's private RAM;\n\
            the golden's {:.0} MiB base is CoW-shared (page cache, not counted here).",
        live.len(),
        n,
        mib(total),
        mib(avg_diff),
        full_bytes as f64 / 1048576.0,
        100.0 * avg_diff as f64 / (full_bytes as f64 / 1024.0),
        full_bytes as f64 / 1048576.0,
    );

    drop(live);
    let _ = std::fs::remove_dir_all(&workdir);
    Ok(())
}
