use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::Instant;
use crate::commands::up::StubProvider;
const BENCH_RUNS: usize = 5;
const BOLD: &str = "\x1b[1m";
const GREEN: &str = "\x1b[32m";
const YELLOW: &str = "\x1b[33m";
const DIM: &str = "\x1b[2m";
const RESET: &str = "\x1b[0m";
fn bold(s: &str) -> String { format!("{}{}{}", BOLD, s, RESET) }
fn green(s: &str) -> String { format!("{}{}{}", GREEN, s, RESET) }
fn yellow(s: &str) -> String { format!("{}{}{}", YELLOW, s, RESET) }
fn dim(s: &str) -> String { format!("{}{}{}", DIM, s, RESET) }
pub fn run(path: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let project_dir = match path {
Some(p) => std::fs::canonicalize(p)?,
None => find_demo_project()?,
};
println!();
println!("{}", bold("=== Hyperdocker Demo & Benchmark ==="));
println!("Project: {}", project_dir.display());
println!();
verify_prerequisites(&project_dir)?;
println!("{}", bold("Phase 1: Docker cold build (--no-cache)…"));
let docker_cold_ms = docker_build_timed(&project_dir, true)?;
println!(" Done in {:.0} ms\n", docker_cold_ms);
println!("{}", bold("Phase 2: Hyperdocker cold build…"));
let (hd_cold_ms, _) = hd_build_timed(&project_dir)?;
println!(" Done in {:.1} ms\n", hd_cold_ms);
println!("{}", bold("Phase 3: File Change + Diff Preview"));
show_diff_preview(&project_dir)?;
println!();
println!("{}", bold("Phase 4: Rebuild Comparison"));
println!(" Running {} iterations each (median reported)…\n", BENCH_RUNS);
let app_py = project_dir.join("app.py");
let original = std::fs::read_to_string(&app_py)?;
let modified = original.replace(
"Hello from Hyperdocker!",
"Hello from Hyperdocker! (modified)",
);
let mut docker_warm_times: Vec<f64> = Vec::with_capacity(BENCH_RUNS);
let mut hd_warm_times: Vec<f64> = Vec::with_capacity(BENCH_RUNS);
for i in 0..BENCH_RUNS {
print!(" Run {}/{}…", i + 1, BENCH_RUNS);
std::fs::write(&app_py, &original)?;
let _ = docker_build_timed(&project_dir, false)?;
let _ = hd_build_timed(&project_dir)?;
std::fs::write(&app_py, &modified)?;
let d_ms = docker_build_timed(&project_dir, false)?;
docker_warm_times.push(d_ms);
let (h_ms, _) = hd_build_timed(&project_dir)?;
hd_warm_times.push(h_ms);
println!(" Docker {:.0} ms | Hyperdocker {:.1} ms", d_ms, h_ms);
}
std::fs::write(&app_py, &original)?;
let docker_median = median(&docker_warm_times);
let hd_median = median(&hd_warm_times);
let speedup = docker_median / hd_median.max(0.001);
println!();
println!("{}", bold("Phase 5: Results"));
println!("{}", bold("----------------------------------------------------------"));
println!("{:<40} {:>12}", bold("Metric"), bold("Time"));
println!("{}", bold("----------------------------------------------------------"));
println!("{:<40} {:>12}", "Docker cold build", format!("{:.0} ms", docker_cold_ms));
println!("{:<40} {:>12}", "Hyperdocker cold build", format!("{:.1} ms", hd_cold_ms));
println!("{:<40} {:>12}", "Docker warm rebuild (median)", format!("{:.0} ms", docker_median));
println!("{:<40} {:>12}", "Hyperdocker warm rebuild (median)", format!("{:.1} ms", hd_median));
println!("{}", bold("----------------------------------------------------------"));
println!();
println!("{}", green(&format!("Hyperdocker is {:.0}x faster on warm rebuilds", speedup)));
println!();
println!("{}", dim("Note: Docker executes all Dockerfile build steps when any file changes."));
println!("{}", dim(" Hyperdocker tracks changes at the file level via content-addressed"));
println!("{}", dim(" storage (CAS). Unchanged files are never re-hashed or re-uploaded,"));
println!("{}", dim(" so incremental rebuilds only pay for what actually changed."));
println!();
Ok(())
}
fn find_demo_project() -> Result<PathBuf, Box<dyn std::error::Error>> {
let cwd = std::env::current_dir()?;
let candidate = cwd.join("examples").join("flask-demo");
if is_demo_project(&candidate) {
return Ok(candidate);
}
if let Some(parent) = cwd.parent() {
let candidate = parent.join("examples").join("flask-demo");
if is_demo_project(&candidate) {
return Ok(candidate);
}
}
if let Some(home) = dirs::home_dir() {
let candidate = home.join(".hd").join("demo").join("flask-demo");
if is_demo_project(&candidate) {
return Ok(candidate);
}
}
Err(concat!(
"Could not find the flask-demo project.\n",
"Provide a path with: hd demo <path>\n",
"Or run from the hyperdocker repo root so 'examples/flask-demo' is found automatically."
).into())
}
fn is_demo_project(dir: &Path) -> bool {
dir.is_dir()
&& dir.join("Dockerfile").exists()
&& dir.join("hd.toml").exists()
}
fn verify_prerequisites(project_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
let docker_ok = Command::new("docker")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if !docker_ok {
return Err("Docker is not installed or not on PATH. Install Docker to run this demo.".into());
}
if !project_dir.join("Dockerfile").exists() {
return Err(format!("No Dockerfile found in {}", project_dir.display()).into());
}
if !project_dir.join("hd.toml").exists() {
return Err(format!("No hd.toml found in {}", project_dir.display()).into());
}
println!("Prerequisites: {}", green("OK"));
println!();
Ok(())
}
fn hd_build_timed(dir: &Path) -> Result<(f64, hd_cas::ContentHash), Box<dyn std::error::Error>> {
let start = Instant::now();
let spec = hd_spec::EnvSpec::from_file(&dir.join("hd.toml"))?;
let cas_path = dirs::home_dir()
.unwrap_or_default()
.join(".hd")
.join("cas");
let store = hd_cas::ContentStore::open(&cas_path)?;
let file_store = hd_cas::ContentStore::open(&cas_path)?;
let mut registry = hd_spec::ProviderRegistry::new();
for provider_name in spec.dependencies.keys() {
registry.register(Box::new(StubProvider {
provider_name: provider_name.clone(),
}));
}
let mut dag = hd_engine::Dag::new(store);
let root_hash = hd_spec::compile_with_files(&spec, ®istry, &mut dag, dir, &file_store)?;
let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
Ok((elapsed_ms, root_hash))
}
fn docker_build_timed(dir: &Path, no_cache: bool) -> Result<f64, Box<dyn std::error::Error>> {
let mut cmd = Command::new("docker");
cmd.arg("build").arg("-t").arg("hd-demo-bench").arg(".");
if no_cache {
cmd.arg("--no-cache");
}
cmd.current_dir(dir);
cmd.stdout(Stdio::null()).stderr(Stdio::piped());
let start = Instant::now();
let output = cmd.output()?;
let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
if !output.status.success() {
return Err(format!(
"Docker build failed: {}",
String::from_utf8_lossy(&output.stderr)
)
.into());
}
Ok(elapsed_ms)
}
fn median(values: &[f64]) -> f64 {
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
sorted[sorted.len() / 2]
}
fn ingest_file_hashes(
project_dir: &Path,
spec: &hd_spec::EnvSpec,
) -> Result<BTreeMap<String, String>, Box<dyn std::error::Error>> {
let cas_path = dirs::home_dir()
.unwrap_or_default()
.join(".hd")
.join("cas");
let store = hd_cas::ContentStore::open(&cas_path)?;
let mut dag = hd_engine::Dag::new(hd_cas::ContentStore::open(&cas_path)?);
let result = hd_engine::ingest_tree(
project_dir,
&spec.files.include,
&spec.files.exclude,
&store,
&mut dag,
)?;
Ok(result
.file_hashes
.into_iter()
.map(|(k, v)| (k, v.to_hex()))
.collect())
}
fn show_diff_preview(project_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
let spec = hd_spec::EnvSpec::from_file(&project_dir.join("hd.toml"))?;
let app_py = project_dir.join("app.py");
let before = ingest_file_hashes(project_dir, &spec)?;
let original = std::fs::read_to_string(&app_py)?;
let modified = original.replace(
"Hello from Hyperdocker!",
"Hello from Hyperdocker! (modified)",
);
std::fs::write(&app_py, &modified)?;
let after = ingest_file_hashes(project_dir, &spec)?;
std::fs::write(&app_py, &original)?;
println!(" Visual DAG diff (after modifying app.py):");
let mut all_keys: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
for k in before.keys() {
all_keys.insert(k.as_str());
}
for k in after.keys() {
all_keys.insert(k.as_str());
}
for path in &all_keys {
match (before.get(*path), after.get(*path)) {
(Some(b), Some(a)) if b != a => {
println!(" {} (content changed)", yellow(path));
}
(Some(_), Some(_)) => {
println!(" {} (unchanged)", dim(path));
}
(None, Some(_)) => {
println!(" {} (added)", green(path));
}
(Some(_), None) => {
println!(" {} (removed)", yellow(path));
}
(None, None) => {}
}
}
Ok(())
}