use criterion::{criterion_group, criterion_main, Criterion};
use nix::unistd::Uid;
use nucleus::container::{Container, ContainerConfig, TrustLevel};
use nucleus::filesystem::ContextMode;
use nucleus::isolation::NamespaceConfig;
use nucleus::resources::{Cgroup, ResourceLimits};
use std::io::Write;
use std::os::unix::process::ExitStatusExt;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus, Stdio};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tempfile::TempDir;
type BenchResult<T> = Result<T, Box<dyn std::error::Error>>;
const CPU_LOOP_SCRIPT: &str =
"i=0; acc=0; while [ \"$i\" -lt 200000 ]; do acc=$((acc + (i % 97))); i=$((i + 1)); done; test \"$acc\" -ge 0";
const CONTEXT_SCAN_SCRIPT_TEMPLATE: &str = "total=0; for f in {PATH}/*; do chunk=''; IFS= read -r chunk < \"$f\" || [ -n \"$chunk\" ]; total=$((total + ${#chunk})); done; test \"$total\" -gt 0";
#[derive(Clone, Copy)]
enum Runner {
HostDirect,
Containerized,
}
impl Runner {
fn label(self) -> &'static str {
match self {
Self::HostDirect => "host_direct",
Self::Containerized => "containerized",
}
}
}
#[derive(Clone, Copy)]
enum Workload {
Startup,
CpuLoop,
ContextScan,
}
impl Workload {
fn host_script(self, context_dir: Option<&Path>) -> String {
match self {
Self::Startup => ":".to_string(),
Self::CpuLoop => CPU_LOOP_SCRIPT.to_string(),
Self::ContextScan => {
let context_dir = context_dir.expect("context workload requires a fixture");
context_scan_script(&shell_quote(context_dir))
}
}
}
fn container_script(self) -> String {
match self {
Self::Startup => ":".to_string(),
Self::CpuLoop => CPU_LOOP_SCRIPT.to_string(),
Self::ContextScan => context_scan_script("/context"),
}
}
fn label(self) -> &'static str {
match self {
Self::Startup => "startup",
Self::CpuLoop => "cpu_loop",
Self::ContextScan => "context_scan",
}
}
}
#[derive(Clone, Copy)]
enum ContextVariant {
BindMount,
Copy,
}
impl ContextVariant {
fn label(self) -> &'static str {
match self {
Self::BindMount => "bind",
Self::Copy => "copy",
}
}
fn into_context_mode(self) -> ContextMode {
match self {
Self::BindMount => ContextMode::BindMount,
Self::Copy => ContextMode::Copy,
}
}
}
#[derive(Clone)]
struct LimitProfile {
label: &'static str,
limits: Option<ResourceLimits>,
}
#[derive(Clone)]
struct Scenario {
workload: Workload,
context_variant: Option<ContextVariant>,
limits: LimitProfile,
}
impl Scenario {
fn group_name(&self) -> String {
match self.context_variant {
Some(context_variant) => format!(
"container_runtime/{}/{}/{}",
self.workload.label(),
context_variant.label(),
self.limits.label
),
None => format!(
"container_runtime/{}/{}",
self.workload.label(),
self.limits.label
),
}
}
fn context_fixture(&self) -> Option<ContextFixture> {
self.context_variant
.map(|_| ContextFixture::new_flat(128, 64 * 1024))
}
fn measurement_time(&self) -> Duration {
match self.workload {
Workload::Startup => Duration::from_secs(4),
Workload::CpuLoop => Duration::from_secs(10),
Workload::ContextScan => Duration::from_secs(8),
}
}
}
struct ContextFixture {
_tempdir: TempDir,
path: PathBuf,
}
impl ContextFixture {
fn new_flat(file_count: usize, file_size_bytes: usize) -> Self {
let tempdir = TempDir::new().expect("failed to create context fixture");
let path = tempdir.path().to_path_buf();
let data = vec![0x5Au8; file_size_bytes];
for index in 0..file_count {
std::fs::write(path.join(format!("file_{index:03}.dat")), &data)
.expect("failed to write context fixture file");
}
Self {
_tempdir: tempdir,
path,
}
}
fn path(&self) -> &Path {
&self.path
}
}
fn shell_quote(path: &Path) -> String {
let rendered = path.to_string_lossy();
format!("'{}'", rendered.replace('\'', "'\"'\"'"))
}
fn context_scan_script(path: &str) -> String {
CONTEXT_SCAN_SCRIPT_TEMPLATE.replace("{PATH}", path)
}
fn unlimited_profile() -> LimitProfile {
LimitProfile {
label: "unlimited",
limits: None,
}
}
fn constrained_profile() -> LimitProfile {
let limits = ResourceLimits::unlimited()
.with_memory("128M")
.expect("valid memory limit")
.with_cpu_cores(0.5)
.expect("valid CPU limit")
.with_pids(64)
.expect("valid PID limit");
LimitProfile {
label: "half_core_128m",
limits: Some(limits),
}
}
fn benchmark_scenarios() -> Vec<Scenario> {
vec![
Scenario {
workload: Workload::Startup,
context_variant: None,
limits: unlimited_profile(),
},
Scenario {
workload: Workload::CpuLoop,
context_variant: None,
limits: unlimited_profile(),
},
Scenario {
workload: Workload::CpuLoop,
context_variant: None,
limits: constrained_profile(),
},
Scenario {
workload: Workload::ContextScan,
context_variant: Some(ContextVariant::BindMount),
limits: unlimited_profile(),
},
Scenario {
workload: Workload::ContextScan,
context_variant: Some(ContextVariant::Copy),
limits: unlimited_profile(),
},
Scenario {
workload: Workload::ContextScan,
context_variant: Some(ContextVariant::Copy),
limits: constrained_profile(),
},
]
}
fn measure_iterations<F>(iters: u64, mut run_once: F) -> Duration
where
F: FnMut() -> BenchResult<()>,
{
let mut total = Duration::ZERO;
for _ in 0..iters {
let start = Instant::now();
run_once().unwrap();
total += start.elapsed();
}
total
}
fn ensure_status_success(label: &str, status: ExitStatus) -> BenchResult<()> {
if status.success() {
return Ok(());
}
match (status.code(), status.signal()) {
(Some(code), _) => Err(format!("{label} exited with status {code}").into()),
(None, Some(signal)) => Err(format!("{label} terminated by signal {signal}").into()),
_ => Err(format!("{label} failed without an exit status").into()),
}
}
fn run_host_direct(scenario: &Scenario, context: Option<&ContextFixture>) -> BenchResult<()> {
let script = scenario
.workload
.host_script(context.map(ContextFixture::path));
match &scenario.limits.limits {
Some(limits) => run_host_direct_with_limits(&script, limits),
None => run_host_direct_unlimited(&script),
}
}
fn run_host_direct_unlimited(script: &str) -> BenchResult<()> {
let status = Command::new("/bin/sh")
.arg("-c")
.arg(script)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
ensure_status_success("host workload", status)
}
fn run_host_direct_with_limits(script: &str, limits: &ResourceLimits) -> BenchResult<()> {
let gated_script = format!("IFS= read -r _; {script}");
let mut child = Command::new("/bin/sh")
.arg("-c")
.arg(&gated_script)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let cgroup_name = format!("nucleus-bench-host-{}", child.id());
let mut cgroup = Cgroup::create(&cgroup_name)?;
cgroup.set_limits(limits)?;
cgroup.attach_process(child.id())?;
let mut stdin = child
.stdin
.take()
.ok_or("limited host workload missing stdin pipe")?;
stdin.write_all(b"start\n")?;
drop(stdin);
let status = child.wait()?;
ensure_status_success("host workload", status)?;
cgroup.cleanup()?;
Ok(())
}
fn run_containerized(scenario: &Scenario, context: Option<&ContextFixture>) -> BenchResult<()> {
let command = vec![
"/bin/sh".to_string(),
"-c".to_string(),
scenario.workload.container_script(),
];
let limits = scenario
.limits
.limits
.clone()
.unwrap_or_else(ResourceLimits::unlimited);
let mut config = ContainerConfig::try_new(None, command)?
.with_limits(limits)
.with_namespaces(NamespaceConfig::minimal())
.with_gvisor(false)
.with_trust_level(TrustLevel::Trusted)
.with_allow_degraded_security(true)
.with_allow_chroot_fallback(true);
if let Some(context_variant) = scenario.context_variant {
let context = context.expect("context workload requires a fixture");
config = config
.with_context(context.path().to_path_buf())
.with_context_mode(context_variant.into_context_mode());
}
let exit_code = Container::new(config).run()?;
if exit_code == 0 {
Ok(())
} else {
Err(format!("container workload exited with status {exit_code}").into())
}
}
fn run_scenario(
runner: Runner,
scenario: &Scenario,
context: Option<&ContextFixture>,
) -> BenchResult<()> {
match runner {
Runner::HostDirect => run_host_direct(scenario, context),
Runner::Containerized => run_containerized(scenario, context),
}
}
fn container_runtime(c: &mut Criterion) {
if !Uid::effective().is_root() {
eprintln!("Skipping container_runtime benchmark: requires root for namespaces and cgroups");
return;
}
for scenario in benchmark_scenarios() {
let group_name = scenario.group_name();
let context = scenario.context_fixture().map(Arc::new);
let mut group = c.benchmark_group(group_name);
group.sample_size(10);
group.warm_up_time(Duration::from_secs(1));
group.measurement_time(scenario.measurement_time());
for runner in [Runner::HostDirect, Runner::Containerized] {
let scenario = scenario.clone();
let context = context.clone();
group.bench_function(runner.label(), move |b| {
let context = context.clone();
b.iter_custom(|iters| {
measure_iterations(iters, || {
run_scenario(runner, &scenario, context.as_deref())
})
});
});
}
group.finish();
}
}
criterion_group!(benches, container_runtime);
criterion_main!(benches);