diff --git a/crates/hammer-build/tests/sin_pre_exec.rs b/crates/hammer-build/tests/sin_pre_exec.rs new file mode 100644 index 00000000..e376ba62 --- /dev/null +++ b/crates/hammer-build/tests/sin_pre_exec.rs @@ -0,0 +1,73 @@ +//! Guardián de SDD 25 §7-H4: **ningún camino de spawn de hammer usa `pre_exec`**. +//! +//! POR QUÉ. `std::process::Command` de Rust usa `posix_spawn` cuando puede, y `posix_spawn` es +//! PLANO respecto al tamaño del padre: a 256 MB tocados, `fork+exec` costó 3 949 µs contra 231 µs +//! (SDD 25 T1, **17×**). En cuanto se le pone un `pre_exec`, Rust apaga ese camino y cae a +//! `fork+exec`, que copia el espacio de direcciones del padre. hammer lanza builds desde un +//! proceso que puede tener cientos de MB mapeados. +//! +//! Hoy `hammer-build::sandbox` lanza `bwrap` sin `pre_exec` — y hasta ahora eso era SUERTE, no una +//! regla: nadie lo comprobaba. Esto lo vuelve una regla. +//! +//! CUÁNDO SE PUEDE APAGAR. Si algún día hace falta de verdad (un `setsid`, un `unshare` que bwrap +//! no cubra), la respuesta no es borrar el test: es escribir acá por qué ese caso paga los 17× y +//! ponerlo en la lista de excepciones, para que el precio quede a la vista de quien lo lea. + +use std::path::{Path, PathBuf}; + +fn raiz_del_workspace() -> PathBuf { + // CARGO_MANIFEST_DIR = crates/hammer-build + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("crates/ tiene dos ancestros") + .to_path_buf() +} + +fn rs_de(dir: &Path, out: &mut Vec) { + let Ok(rd) = std::fs::read_dir(dir) else { return }; + for e in rd.flatten() { + let p = e.path(); + if p.is_dir() { + rs_de(&p, out); + } else if p.extension().is_some_and(|x| x == "rs") { + out.push(p); + } + } +} + +#[test] +fn ningun_spawn_de_hammer_usa_pre_exec() { + let raiz = raiz_del_workspace(); + let crates = raiz.join("crates"); + assert!(crates.is_dir(), "no encontré {} — ¿se movió el workspace?", crates.display()); + + let mut fuentes = Vec::new(); + rs_de(&crates, &mut fuentes); + // Un barrido que no encuentra fuentes aprueba por vacío, que es el fallo de CLAUDE.md §3. + assert!(fuentes.len() > 20, "sólo {} fuentes: el barrido no miró nada", fuentes.len()); + + let mut culpables = Vec::new(); + for f in &fuentes { + if f == Path::new(file!()) || f.ends_with("tests/sin_pre_exec.rs") { + continue; + } + let Ok(txt) = std::fs::read_to_string(f) else { continue }; + for (i, l) in txt.lines().enumerate() { + let limpia = l.trim_start(); + if limpia.starts_with("//") || limpia.starts_with("*") { + continue; // documentarlo no es usarlo + } + if l.contains("pre_exec") { + culpables.push(format!("{}:{}: {}", f.display(), i + 1, l.trim())); + } + } + } + + assert!( + culpables.is_empty(), + "SDD 25 §7-H4: `pre_exec` apaga el camino `posix_spawn` de Rust y devuelve el spawn a \ + `fork+exec` (17× más caro con 256 MB en el padre, SDD 25 T1). Aparece en:\n {}", + culpables.join("\n ") + ); +} diff --git a/docs/25-tasas-del-kernel.md b/docs/25-tasas-del-kernel.md index 417632d6..5cb52330 100644 --- a/docs/25-tasas-del-kernel.md +++ b/docs/25-tasas-del-kernel.md @@ -403,9 +403,13 @@ Su precio, concreto: parcheado en la receta. Queda escrito para no rediscutirlo. - **H3 · `harkaq` usa `openat2 RESOLVE_BENEATH|RESOLVE_NO_SYMLINKS`** donde hoy valida rutas a mano (T9, ~gratis). -- **H4 · Auditar los `Command` de hammer que ponen `pre_exec`**: Rust apaga el camino - `posix_spawn` en cuanto hay `pre_exec`, y cae a `fork+exec` (T1). `hammer-build/sandbox.rs` - lanza `bwrap` sin `pre_exec` ⇒ está bien hoy; el punto es que sea una regla, no una suerte. +- **H4 · Ningún `Command` de hammer pone `pre_exec`** — Rust apaga el camino `posix_spawn` en + cuanto hay uno, y cae a `fork+exec` (T1: 17× a 256 MB tocados). `hammer-build/sandbox.rs` lanza + `bwrap` sin `pre_exec`, o sea que estaba bien **por suerte**: nadie lo comprobaba. **HECHO**: el + test `crates/hammer-build/tests/sin_pre_exec.rs` barre las fuentes del workspace y falla si + aparece uno (comprobado en los dos sentidos: inyectando un `pre_exec` el test se pone rojo, y un + barrido que no encuentra fuentes también, para que no apruebe por vacío). Si algún día hace falta + de verdad, la salida es escribir en el test por qué ese caso paga los 17×, no borrarlo. - **H5 · La materialización de dependencias se queda en hardlink** (T10) y el experimento de reflink (§9) alimenta SDD 18 (wawafs), donde el store como FS vivo cambiaría el cociente. - **H6 · No `madvise(MADV_HUGEPAGE)` en el camino de build** mientras `defrag=madvise` y la