mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
136 lines
4.3 KiB
Rust
136 lines
4.3 KiB
Rust
//! Phase 23 / Track O.1 micro-benchmark for the Node build pool.
|
|
//!
|
|
//! Asserts the warm-cache hot path (a `prepare_node` cache hit fronted by the
|
|
//! shared npm download cache) stays ≤ 200ms, the interpreted-language budget.
|
|
//! Skips when `npm` is not runnable so a toolchain-less CI image keeps the gate
|
|
//! green.
|
|
|
|
#![cfg(feature = "dynamic")]
|
|
|
|
use std::path::Path;
|
|
use std::sync::{Mutex, MutexGuard};
|
|
use std::time::{Duration, Instant};
|
|
|
|
use nyx_scanner::dynamic::build_sandbox::prepare_node;
|
|
use nyx_scanner::dynamic::spec::{
|
|
EntryKind, HarnessSpec, JavaToolchain, PayloadSlot, SpecDerivationStrategy,
|
|
};
|
|
use nyx_scanner::labels::Cap;
|
|
use nyx_scanner::symbol::Lang;
|
|
|
|
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
|
|
|
/// Isolates `NYX_BUILD_CACHE` + `NYX_BUILD_POOL_DIR` to private tempdirs so the
|
|
/// benchmark never reads or writes the user-level build cache.
|
|
struct CacheGuard {
|
|
_lock: MutexGuard<'static, ()>,
|
|
prior_cache: Option<String>,
|
|
prior_pool: Option<String>,
|
|
_cache: tempfile::TempDir,
|
|
_pool: tempfile::TempDir,
|
|
}
|
|
|
|
impl CacheGuard {
|
|
fn isolated() -> Self {
|
|
let lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
let cache = tempfile::TempDir::new().unwrap();
|
|
let pool = tempfile::TempDir::new().unwrap();
|
|
let prior_cache = std::env::var("NYX_BUILD_CACHE").ok();
|
|
let prior_pool = std::env::var("NYX_BUILD_POOL_DIR").ok();
|
|
unsafe {
|
|
std::env::set_var("NYX_BUILD_CACHE", cache.path());
|
|
std::env::set_var("NYX_BUILD_POOL_DIR", pool.path());
|
|
}
|
|
Self {
|
|
_lock: lock,
|
|
prior_cache,
|
|
prior_pool,
|
|
_cache: cache,
|
|
_pool: pool,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for CacheGuard {
|
|
fn drop(&mut self) {
|
|
restore("NYX_BUILD_CACHE", self.prior_cache.take());
|
|
restore("NYX_BUILD_POOL_DIR", self.prior_pool.take());
|
|
}
|
|
}
|
|
|
|
fn restore(key: &str, prior: Option<String>) {
|
|
match prior {
|
|
Some(v) => unsafe { std::env::set_var(key, v) },
|
|
None => unsafe { std::env::remove_var(key) },
|
|
}
|
|
}
|
|
|
|
fn median(mut ds: Vec<Duration>) -> Duration {
|
|
ds.sort();
|
|
ds[ds.len() / 2]
|
|
}
|
|
|
|
fn mk_spec() -> HarnessSpec {
|
|
HarnessSpec {
|
|
finding_id: "bench".to_owned(),
|
|
entry_file: "entry".to_owned(),
|
|
entry_name: "main".to_owned(),
|
|
entry_kind: EntryKind::Function,
|
|
lang: Lang::JavaScript,
|
|
toolchain_id: "bench-node".to_owned(),
|
|
payload_slot: PayloadSlot::Param(0),
|
|
expected_cap: Cap::CODE_EXEC,
|
|
constraint_hints: vec![],
|
|
sink_file: "sink".to_owned(),
|
|
sink_line: 1,
|
|
spec_hash: "0000000000000000".to_owned(),
|
|
derivation: SpecDerivationStrategy::FromFlowSteps,
|
|
stubs_required: vec![],
|
|
framework: None,
|
|
java_toolchain: JavaToolchain::default(),
|
|
}
|
|
}
|
|
|
|
fn write_project(workdir: &Path) {
|
|
// Dependency-free manifest: `npm install` succeeds offline and the warm
|
|
// cache marker lets every later call short-circuit.
|
|
std::fs::write(
|
|
workdir.join("package.json"),
|
|
"{\"name\":\"nyxbench\",\"version\":\"1.0.0\",\"private\":true}\n",
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "real-toolchain perf bench: spawns `npm install`. Opt-in so the default suite stays hermetic + fast. Run: cargo nextest run --features dynamic --run-ignored ignored-only -E 'binary(~build_pool) | binary(~compile_pool)'"]
|
|
fn warm_prepare_p50_under_200ms() {
|
|
let _guard = CacheGuard::isolated();
|
|
let spec = mk_spec();
|
|
let work = tempfile::TempDir::new().unwrap();
|
|
write_project(work.path());
|
|
|
|
// Cold prep warms the cache; not measured. A toolchain-less host returns
|
|
// Err here, so skip rather than fail.
|
|
match prepare_node(&spec, work.path()) {
|
|
Ok(_) => {}
|
|
Err(e) => {
|
|
eprintln!("skipping node build-pool bench: {e:?}");
|
|
return;
|
|
}
|
|
}
|
|
|
|
let mut hot = Vec::new();
|
|
for _ in 0..5 {
|
|
let start = Instant::now();
|
|
let r = prepare_node(&spec, work.path()).expect("warm prepare must succeed");
|
|
hot.push(start.elapsed());
|
|
assert!(r.cache_hit, "warm prepare_node must be a cache hit");
|
|
}
|
|
|
|
let p50 = median(hot);
|
|
eprintln!("node build-pool warm P50: {p50:?}");
|
|
assert!(
|
|
p50 <= Duration::from_millis(200),
|
|
"node warm-prepare P50 {p50:?} exceeds the 200ms interpreted budget",
|
|
);
|
|
}
|