use std::collections::BTreeMap;
use std::sync::Arc;
use serde::Serialize;
use sui_cache::storage::StorageBackend;
use sui_dockerfile_wrapper::cache::MockCacheBackend;
use sui_dockerfile_wrapper::command::{
CommandOutcome, CommandRunError, CommandRunner, DockerBuildInvocation, RealCommandRunner,
};
use sui_dockerfile_wrapper::{run_wrapper, FilesystemDockerfileEnvironment, WrapperConfig, WrapperOutcome};
use sui_spec::dockerfile::{self, DockerfileArgs, MockDockerfileEnvironment};
#[derive(Debug, Serialize)]
struct WarmthReport {
cold_duration_ms: u64,
warm_duration_ms: u64,
speedup_ratio: f64,
partial_invalidation_node_count: usize,
total_node_count: usize,
}
struct RealDockerHarnessRunner {
inner: RealCommandRunner,
}
impl CommandRunner for RealDockerHarnessRunner {
fn run(&self, invocation: &DockerBuildInvocation) -> Result<CommandOutcome, CommandRunError> {
match invocation.args.first().map(String::as_str) {
Some("pull") => {
let image_ref = invocation.args.last().cloned().unwrap_or_default();
let tag_invocation = DockerBuildInvocation::tag(&image_ref, &image_ref);
self.inner.run(&tag_invocation)
}
Some("build") => {
let mut with_load = invocation.clone();
with_load.args.insert(1, "--load".to_string());
self.inner.run(&with_load)
}
_ => self.inner.run(invocation),
}
}
}
fn docker_available() -> bool {
std::process::Command::new("docker").arg("info").output().map(|o| o.status.success()).unwrap_or(false)
}
fn buildable_gateway_dockerfile() -> String {
"FROM debian:bookworm-slim\n\
ARG TARGETARCH\n\
ARG FIPS=false\n\
RUN apt-get update && apt-get install -y ca-certificates curl\n\
RUN --mount=type=bind,source=.,target=/build echo installing FIPS for $TARGETARCH if requested\n\
ENV GATEWAY_HOME=/opt/gateway\n\
WORKDIR /opt/gateway\n\
COPY gateway.txt /opt/gateway/gateway\n\
RUN adduser --disabled-password --gecos '' --home /opt/gateway gateway && chown gateway:gateway /opt/gateway/gateway\n\
ENTRYPOINT [\"/opt/gateway/gateway\"]\n\
CMD [\"--config\", \"/opt/gateway/config.yaml\"]\n"
.to_string()
}
#[tokio::test]
#[ignore = "needs a real docker daemon and takes real wall-clock time; run via `cargo test -p sui-dockerfile-wrapper --test warmth_benchmark -- --ignored --nocapture`"]
async fn warmth_benchmark_cold_vs_warm_vs_partial_invalidation() {
if !docker_available() {
eprintln!("skipping warmth_benchmark_cold_vs_warm_vs_partial_invalidation: no docker daemon reachable");
return;
}
let dir = tempfile::tempdir().unwrap();
let dockerfile_path = dir.path().join("Dockerfile.nonroot_gateway");
std::fs::write(&dockerfile_path, buildable_gateway_dockerfile()).unwrap();
std::fs::write(dir.path().join("gateway.txt"), b"pretend-gateway-binary").unwrap();
let mut build_args = BTreeMap::new();
build_args.insert("TARGETARCH".to_string(), "amd64".to_string());
let env = FilesystemDockerfileEnvironment { build_args: build_args.clone() };
let cache: Arc<dyn StorageBackend> = Arc::new(MockCacheBackend::new());
let cfg = WrapperConfig {
dockerfile_path: dockerfile_path.clone(),
context_dir: dir.path().to_path_buf(),
build_args,
image_tag: "sui-warmth-bench:v1".to_string(),
daemon_socket_path: None,
};
let real_runner = RealDockerHarnessRunner { inner: RealCommandRunner };
let cold_receipt = run_wrapper(&cfg, &env, &cache, &real_runner).await.unwrap();
assert!(
matches!(cold_receipt.outcome, WrapperOutcome::CacheMiss { .. }),
"cold run must be a cache miss, got {:?}",
cold_receipt.outcome
);
let cold_duration_ms = cold_receipt.total_wall_clock_ms;
let warm_runner = RealDockerHarnessRunner { inner: RealCommandRunner };
let warm_receipt = run_wrapper(&cfg, &env, &cache, &warm_runner).await.unwrap();
assert!(
matches!(warm_receipt.outcome, WrapperOutcome::CacheHit { .. }),
"warm run must be a full cache hit, got {:?}",
warm_receipt.outcome
);
let warm_duration_ms = warm_receipt.total_wall_clock_ms.max(1);
let modified_text = buildable_gateway_dockerfile()
.replace("apt-get install -y ca-certificates curl", "apt-get install -y ca-certificates curl jq");
let modified_env =
MockDockerfileEnvironment::default().with_dockerfile("modified", &modified_text).with_build_arg("TARGETARCH", "amd64");
let modified_graph = dockerfile::apply(&DockerfileArgs { path: "modified".to_string() }, &modified_env).unwrap();
let total_node_count = modified_graph.nodes.len();
let mut partial_invalidation_node_count = 0usize;
for node in &modified_graph.nodes {
if cache.get_narinfo(&node.content_hash).await.unwrap().is_none() {
partial_invalidation_node_count += 1;
}
}
#[allow(clippy::cast_precision_loss)]
let speedup_ratio = cold_duration_ms as f64 / warm_duration_ms as f64;
let report = WarmthReport {
cold_duration_ms,
warm_duration_ms,
speedup_ratio,
partial_invalidation_node_count,
total_node_count,
};
eprintln!("{}", serde_json::to_string_pretty(&report).unwrap());
assert!(
warm_duration_ms < cold_duration_ms / 2,
"warm run must be at least 2x faster than cold: cold={cold_duration_ms}ms warm={warm_duration_ms}ms"
);
assert!(
partial_invalidation_node_count > 0 && partial_invalidation_node_count < total_node_count,
"changing one instruction must invalidate a strict, non-empty subset of nodes: {partial_invalidation_node_count}/{total_node_count}"
);
let _ = std::process::Command::new("docker").args(["rmi", "-f", "sui-warmth-bench:v1"]).output();
}