//! DAX step 4: fleet RAM density. Boot N VMs that all DAX-mount the SAME host
//! directory and read the same big file. With virtio-fs DAX the file's pages are
//! mapped read-only into each guest's DAX window straight from the host page
//! cache — ONE host copy shared across every VM — so the file is NOT paid
//! per-VM (contrast: the committed-squashfs cold-boot path decompresses into
//! private guest RAM, ~148-252 MiB/VM in kvm_density_scale).
//!
//! Metric: host MemAvailable consumed per VM (the file lives in shared,
//! reclaimable page cache, so it barely dents MemAvailable even once — that IS
//! the win). Needs the virtio-fs/DAX kernel via SUPERMACHINE_KVM_KERNEL.
//!
//! Usage:
//!   SUPERMACHINE_KVM_KERNEL=/root/linux-6.12.30/arch/x86/boot/bzImage \
//!     cargo run --example kvm_dax_density [N]

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn mem_avail_mib() -> i64 {
    let s = std::fs::read_to_string("/proc/meminfo").unwrap_or_default();
    for line in s.lines() {
        if let Some(r) = line.strip_prefix("MemAvailable:") {
            return r
                .split_whitespace()
                .next()
                .and_then(|v| v.parse::<i64>().ok())
                .unwrap_or(0)
                / 1024;
        }
    }
    0
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn main() {
    use std::time::Duration;
    use supermachine::vmm::resources::MountSpec;
    use supermachine::{Image, VmConfig};

    // The bundled kernel has virtio-fs/DAX built in; no override needed.
    let n: usize = std::env::args()
        .nth(1)
        .and_then(|s| s.parse().ok())
        .unwrap_or(4);
    let mib = 128u64;

    // Host dir with one big INCOMPRESSIBLE file shared by all VMs.
    let host_dir = std::env::temp_dir().join("sm-dax-dens-host");
    let _ = std::fs::remove_dir_all(&host_dir);
    std::fs::create_dir_all(&host_dir).expect("mkdir");
    {
        use std::io::Write;
        let mut f = std::fs::File::create(host_dir.join("blob")).expect("create blob");
        let chunk = {
            let mut b = vec![0u8; 1 << 20];
            for (i, x) in b.iter_mut().enumerate() {
                *x = (i as u8).wrapping_mul(31).wrapping_add(7);
            }
            b
        };
        for _ in 0..mib {
            f.write_all(&chunk).expect("write blob");
        }
    }

    // Bake once (experimental kernel via the env override); start N VMs from it.
    let dest = std::env::temp_dir().join("sm-dax-dens-img");
    let _ = std::fs::remove_dir_all(&dest);
    let image = Image::bake_kvm_auto("alpine", &dest).expect("bake");
    let cfg = VmConfig::new().with_virtiofs(MountSpec::new(
        host_dir.to_string_lossy().into_owned(),
        "smfs0",
        "/mnt/host",
    ));

    let base = mem_avail_mib();
    eprintln!("baseline MemAvailable = {base} MiB; file = {mib} MiB (shared via DAX)");
    let mut held = Vec::new();
    for i in 0..n {
        let vm = image.start(&cfg).expect("start");
        std::thread::sleep(Duration::from_millis(4000));
        // Read the whole file through the DAX mount, so its pages fault in.
        let o = vm
            .exec_builder()
            .argv([
                "/bin/sh",
                "-c",
                "mount | grep -q 'dax=always' && dd if=/mnt/host/blob of=/dev/null bs=1M 2>/dev/null && echo dax-read-ok",
            ])
            .output()
            .expect("read");
        let ok = String::from_utf8_lossy(&o.stdout).contains("dax-read-ok");
        held.push(vm);
        let used = base - mem_avail_mib();
        eprintln!(
            "VM #{i} (dax-read-ok={ok}): host used +{used} MiB total, {} MiB/VM avg",
            used / (i as i64 + 1)
        );
    }
    let used = base - mem_avail_mib();
    let per_vm = used / n as i64;
    eprintln!(
        "\n=== DAX FLEET DENSITY: {n} VMs each DAX-read a {mib} MiB file ===\n\
         host used +{used} MiB total, ~{per_vm} MiB/VM\n\
         the {mib} MiB file is NOT in the per-VM cost (shared host page cache via\n\
         DAX), vs ~148-252 MiB/VM for the non-DAX committed-squashfs path."
    );
    // A loose gate: per-VM must be well under the file size (file not paid per VM).
    eprintln!(
        "=== {} ===",
        if per_vm < mib as i64 {
            "PASS (file shared)"
        } else {
            "FAIL (file paid per-VM)"
        }
    );
    drop(held);
}

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