build(test): benchmark safe concurrency ramps

req: test/004
This commit is contained in:
slhx agent
2026-05-11 10:31:19 +02:00
parent 7a2c5ecae0
commit 9ceea7de94
2 changed files with 88 additions and 18 deletions
+1 -1
View File
@@ -597,7 +597,7 @@ what a valid business email is.
003 Generated registries are validated by compile-time tests: missing handler implementations produce test failures with actionable messages. 003 Generated registries are validated by compile-time tests: missing handler implementations produce test failures with actionable messages.
### req: test/004 ### req: test/004
004 Repository-wide verification uses a resource-aware runner that caps Cargo build jobs and Rust test threads from available CPU and memory. Browser E2E runs as an isolated step and can be skipped explicitly when browser infrastructure is unavailable. 004 Repository-wide verification uses a resource-aware runner that caps Cargo build jobs and Rust test threads from available CPU and memory. User-requested concurrency cannot exceed the detected safe cap. Browser E2E runs as an isolated step and can be skipped explicitly when browser infrastructure is unavailable.
--- ---
+87 -17
View File
@@ -1,11 +1,13 @@
use std::env; use std::env;
use std::path::Path; use std::path::Path;
use std::process::{Command, ExitCode}; use std::process::{Command, ExitCode};
use std::time::Instant;
fn main() -> ExitCode { fn main() -> ExitCode {
let mut args = env::args().skip(1); let mut args = env::args().skip(1);
match args.next().as_deref() { match args.next().as_deref() {
Some("test") | None => run_test_plan(), Some("test") | None => run_test_plan(),
Some("bench") => run_bench_plan(),
Some("help") | Some("--help") | Some("-h") => { Some("help") | Some("--help") | Some("-h") => {
print_help(); print_help();
ExitCode::SUCCESS ExitCode::SUCCESS
@@ -20,24 +22,14 @@ fn main() -> ExitCode {
fn print_help() { fn print_help() {
println!( println!(
"slhx-ci — resource-aware project checks\n\n cargo run -p slhx-xtask -- test\n\nEnvironment overrides:\n SLHX_CI_JOBS=N compile jobs\n SLHX_CI_TEST_THREADS=N Rust test threads\n SLHX_CI_SKIP_BROWSER=1 skip browser E2E" "slhx-ci — resource-aware project checks\n\n cargo run -p slhx-xtask -- test\n cargo run -p slhx-xtask -- bench\n\nEnvironment overrides:\n SLHX_CI_JOBS=N compile jobs, capped by detected resources\n SLHX_CI_TEST_THREADS=N Rust test threads, capped by detected resources\n SLHX_CI_SKIP_BROWSER=1 skip browser E2E"
); );
} }
fn run_test_plan() -> ExitCode { fn run_test_plan() -> ExitCode {
// req: test/004 // req: test/004
let budget = Budget::detect(); let budget = Budget::detect();
eprintln!( budget.report();
"slhx-ci: cpus={} mem={}GiB jobs={} test_threads={} browser={}",
budget.cpus,
budget
.mem_gib
.map(|mem| mem.to_string())
.unwrap_or_else(|| "unknown".into()),
budget.jobs,
budget.test_threads,
if budget.skip_browser { "skip" } else { "run" }
);
let mut steps = vec![ let mut steps = vec![
Step::new( Step::new(
@@ -71,6 +63,40 @@ fn run_test_plan() -> ExitCode {
ExitCode::SUCCESS ExitCode::SUCCESS
} }
fn run_bench_plan() -> ExitCode {
// req: test/004
let budget = Budget::detect();
budget.report();
eprintln!(
"slhx-ci: benchmarking small safe test slices from 1 to {} job(s)",
budget.jobs
);
let steps = [
Step::new("bench-xtask", ["test", "-p", "slhx-xtask"]),
Step::new("bench-runtime", ["test", "-p", "slhx-js"]),
];
for jobs in bench_values(budget.jobs) {
let bench_budget = budget.with_jobs(jobs);
for step in &steps {
let started = Instant::now();
if let Err(code) = step.run(&bench_budget) {
return code;
}
println!(
"bench\t{}\tjobs={}\ttest_threads={}\telapsed_ms={}",
step.name,
bench_budget.jobs,
bench_budget.test_threads,
started.elapsed().as_millis()
);
}
}
ExitCode::SUCCESS
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
struct Budget { struct Budget {
cpus: usize, cpus: usize,
@@ -108,8 +134,11 @@ impl Budget {
let cpus = cpus.max(1); let cpus = cpus.max(1);
let mem_jobs = mem_gib.map_or(cpus, |gib| (gib / 2).max(1)); let mem_jobs = mem_gib.map_or(cpus, |gib| (gib / 2).max(1));
let auto_jobs = cpus.min(mem_jobs).clamp(1, 6); let auto_jobs = cpus.min(mem_jobs).clamp(1, 6);
let jobs = jobs_override.unwrap_or(auto_jobs).max(1); let jobs = jobs_override.unwrap_or(auto_jobs).clamp(1, auto_jobs);
let test_threads = test_threads_override.unwrap_or(jobs.min(4)).max(1); let max_test_threads = jobs.min(4);
let test_threads = test_threads_override
.unwrap_or(max_test_threads)
.clamp(1, max_test_threads);
Self { Self {
cpus, cpus,
mem_gib, mem_gib,
@@ -118,6 +147,41 @@ impl Budget {
skip_browser, skip_browser,
} }
} }
fn report(&self) {
eprintln!(
"slhx-ci: cpus={} mem={}GiB jobs={} test_threads={} browser={}",
self.cpus,
self.mem_gib
.map(|mem| mem.to_string())
.unwrap_or_else(|| "unknown".into()),
self.jobs,
self.test_threads,
if self.skip_browser { "skip" } else { "run" }
);
}
fn with_jobs(self, jobs: usize) -> Self {
let jobs = jobs.clamp(1, self.jobs);
Self {
jobs,
test_threads: self.test_threads.min(jobs.min(4)).max(1),
..self
}
}
}
fn bench_values(limit: usize) -> Vec<usize> {
let limit = limit.max(1);
let mut values = Vec::new();
let mut value = 1;
while value < limit {
values.push(value);
value *= 2;
}
values.push(limit);
values.dedup();
values
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -269,10 +333,16 @@ mod tests {
} }
#[test] #[test]
fn budget_allows_explicit_overrides() { fn budget_clamps_explicit_overrides_to_resource_budget() {
let budget = Budget::from_resources(2, Some(2), Some(8), Some(7), true); let budget = Budget::from_resources(2, Some(2), Some(8), Some(7), true);
assert_eq!(budget.jobs, 8); assert_eq!(budget.jobs, 1);
assert_eq!(budget.test_threads, 7); assert_eq!(budget.test_threads, 1);
}
#[test]
fn bench_values_grow_gradually_and_include_limit() {
assert_eq!(super::bench_values(1), vec![1]);
assert_eq!(super::bench_values(6), vec![1, 2, 4, 6]);
} }
} }