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