92 lines
2.9 KiB
Rust
92 lines
2.9 KiB
Rust
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))
|
||
}
|