prvng_platform/crates/buildkit-launcher/src/sizing.rs

93 lines
2.9 KiB
Rust
Raw Normal View History

use anyhow::Result;
use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct RunnerSize {
pub cpu: u32,
pub memory_gb: u32,
pub disk_gb: u32,
pub time_budget_min: u32,
}
#[derive(Deserialize, Default)]
struct BuildSpec {
cpu: Option<u32>,
memory_gb: Option<u32>,
disk_gb: Option<u32>,
time_budget_min: Option<u32>,
language: Option<String>,
}
impl RunnerSize {
fn language_default(language: &str) -> Self {
match language {
"rust" => Self { cpu: 4, memory_gb: 8, disk_gb: 50, time_budget_min: 60 },
"go" => Self { cpu: 2, memory_gb: 4, disk_gb: 30, time_budget_min: 30 },
"java" | "kotlin" | "scala" => Self { cpu: 4, memory_gb: 8, disk_gb: 40, time_budget_min: 45 },
_ => Self { cpu: 2, memory_gb: 4, disk_gb: 30, time_budget_min: 30 },
}
}
pub fn from_p95(cpu_p95: f64, memory_mb_p95: f64) -> Self {
let cpu = ((cpu_p95 * 1.2).ceil() as u32).max(2);
let memory_gb = (((memory_mb_p95 * 1.2) / 1024.0).ceil() as u32).max(4);
Self { cpu, memory_gb, disk_gb: 30, time_budget_min: 60 }
}
}
fn parse_build_spec(context_path: &Path) -> Option<BuildSpec> {
let spec_path = context_path.join(".build-spec.ncl");
if !spec_path.exists() {
return None;
}
// Extract JSON from nickel export — buildkit-launcher shells out to nickel
let output = std::process::Command::new("nickel")
.args(["export", "--format", "json"])
.arg(&spec_path)
.output()
.ok()?;
if !output.status.success() {
return None;
}
serde_json::from_slice(&output.stdout).ok()
}
pub fn resolve(
context_path: &Path,
p95_cpu: Option<f64>,
p95_mem_mb: Option<f64>,
language_hint: Option<&str>,
) -> Result<RunnerSize> {
let spec = parse_build_spec(context_path).unwrap_or_default();
// Tier 1: explicit declaration in .build-spec.ncl
if spec.cpu.is_some() || spec.memory_gb.is_some() {
let base = language_hint
.or(spec.language.as_deref())
.map(RunnerSize::language_default)
.unwrap_or_else(|| RunnerSize::language_default("default"));
return Ok(RunnerSize {
cpu: spec.cpu.unwrap_or(base.cpu),
memory_gb: spec.memory_gb.unwrap_or(base.memory_gb),
disk_gb: spec.disk_gb.unwrap_or(base.disk_gb),
time_budget_min: spec.time_budget_min.unwrap_or(base.time_budget_min),
});
}
// Tier 2: P95 historical × 1.2
if let (Some(cpu_p95), Some(mem_p95)) = (p95_cpu, p95_mem_mb) {
let mut size = RunnerSize::from_p95(cpu_p95, mem_p95);
if let Some(budget) = spec.time_budget_min {
size.time_budget_min = budget;
}
return Ok(size);
}
// Tier 3: language defaults
let lang = language_hint
.or(spec.language.as_deref())
.unwrap_or("default");
Ok(RunnerSize::language_default(lang))
}