> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
tasas: T19 — el sandbox apila una capa overlay por dep, y lo caro no es lo que parece
Nadie había medido qué cuesta el apilado de capas de `bwrap_args`. El 95% de las
recetas (1 108 de 1 171) construye con las deps APILADAS, hasta ~27.
Un lookup que FALLA cuesta +1 925 ns por capa (a 64 capas, 124 µs = 1 580 syscalls
nulas), pero se paga UNA VEZ POR RUTA: la dentry fusionada queda cacheada y el
montaje dura toda la fase. Un `cc -O2 -c` toca 381 rutas distintas que fallan ⇒
2,9 ms por fase a 24 capas, la misma décima de por ciento que la jaula de T17/T18.
⇒ `merge_deps_layer` no es una optimización de velocidad: sólo se pagaría sola a
partir de ~35 200 primeros fallos en una misma fase. Es el rodeo de un muro, y el
muro quedó bisecado al byte: `mount(2)` monta 41 capas (4 059 B de opciones) y
falla en 42 con un `ENOENT` que miente. El `mount(8)` de util-linux se rinde entre
8 y 12, así que bisecarlo desde la shell da un muro FALSO.
Consecuencias anotadas en sandbox.rs: no bajar el umbral «para que vaya más
rápido», y podar `.dmerge` es gratis en rendimiento. Y hay puerta medida: la API
de montaje nueva (`fsconfig lowerdir+`) monta las 64 capas que `mount(2)` rechaza.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
diff --git a/crates/hammer-build/src/sandbox.rs b/crates/hammer-build/src/sandbox.rs
index b9bca334..60a551fa 100644
--- a/crates/hammer-build/src/sandbox.rs
+++ b/crates/hammer-build/src/sandbox.rs
@@ -356,6 +356,23 @@ impl Sandbox {
// La única forma de escalar a muchas deps es FUNDIRLAS en UN solo directorio real (hardlinks,
// `cp -al`, last-wins = misma semántica que el apilado overlay) y pasar esa ÚNICA capa ⇒
// lowerdir = rootfs + 1 merge, constante sin importar cuántas deps haya.
+ //
+ // MEDIDO (SDD 25 T19, `docs/evidencia/tasas-kernel-2026-08-29/overlay.txt`), y conviene
+ // leerlo antes de tocar este umbral:
+ // · El muro es de UNA PÁGINA EXACTA: `mount(2)` acepta 41 capas del store (4 059 B de
+ // opciones) y falla en 42 (4 158 B) con `ENOENT` — el kernel no dice «demasiado largo».
+ // (El `mount(8)` de util-linux se rinde mucho antes; bisecarlo desde la shell miente.)
+ // · Este umbral dispara a las ~27 deps ⇒ factor 1,5 de margen, y es estructural: el muro
+ // va como ~4002/(L+1) y el umbral como 3000/(L+16), cociente ≥1,33 para cualquier L.
+ // No hay que recalibrarlo si el store se muda.
+ // · Fundir NO es una optimización de velocidad. El overlay cobra por PRIMER toque de cada
+ // ruta (+1 925 ns por capa en un lookup que falla; ~660 ns planos si la ruta ya se
+ // tocó), y el montaje dura toda la fase: un compilado toca 381 rutas distintas que
+ // fallan ⇒ 2,9 ms por fase a 24 capas. Fundir 24 deps reales cuesta 264 ms, o sea que
+ // sólo se pagaría sola a partir de ~35 200 primeros fallos en una misma fase.
+ // ⇒ NO bajar el umbral «para que vaya más rápido»: no gana nada y engorda `.dmerge`, que
+ // retiene por hardlink los artefactos que borres del store. Y por lo mismo, PODAR
+ // `.dmerge` es gratis en rendimiento.
let total_len: usize = self.deps.iter().map(|d| d.as_os_str().len() + 16).sum();
if total_len > 3000 && self.deps.len() > 1 {
if let Some(merged) = self.merge_deps_layer() {
diff --git a/docs/25-tasas-del-kernel.md b/docs/25-tasas-del-kernel.md
index 1570b685..94091ead 100644
--- a/docs/25-tasas-del-kernel.md
+++ b/docs/25-tasas-del-kernel.md
@@ -43,6 +43,15 @@ El espejo de este documento para el lado tawasuyu es
una clausura fichero a fichero. Sobre un compilado entero son **0,23–0,31%**: una décima parte
del presupuesto de <5% de SDD 16. Lo caro de enjaular sigue siendo el ~1,09 ms por proceso de
los namespaces (T14).
+7. **El sandbox apila una capa overlay por dep, y eso NO es lo caro** (T19, medido 2026-09-01):
+ un lookup que FALLA cuesta +1 925 ns por capa —a 64 capas, 124 µs, mil quinientas syscalls nulas—
+ pero **se paga una sola vez por ruta**: la dentry fusionada queda cacheada y el montaje dura toda
+ la fase. Un compilado toca 381 rutas distintas que fallan ⇒ **2,9 ms por fase**, la misma décima
+ de por ciento que la jaula. ⇒ **fundir las deps en una capa (`.dmerge`) no es una optimización,
+ es el rodeo de un muro**: `mount(2)` monta 41 capas (4 059 B de opciones) y falla en 42, con un
+ `ENOENT` que miente. La API de montaje nueva (`fsconfig lowerdir+`) monta las 64 y **quitaría
+ `.dmerge` de en medio** (§7-H11).
+
6. **Cuando `/proc` sí se sondea entero, hay algo cerca de 7× más barato** (§9.5, medido
2026-08-31): un iterador BPF `task` saca la misma foto **al precio de listar el directorio**
—1,75× el piso de `readdir` contra 12,1× de `/proc`—. Su portón es `DEBUG_INFO_BTF`, que sigue
@@ -377,7 +386,113 @@ lecturas, el grano fino de harkaq gasta menos de la décima parte del presupuest
y lo caro de enjaular sigue siendo lo que ya decía T14: el ~1,09 ms por proceso de los namespaces,
que es diez veces todo esto junto.
-## 3. Las seis sorpresas
+### T19 · El sandbox apila una capa overlay por dep — y lo caro no es lo que parece
+
+T17/T18 midieron lo que harkaq pone DENTRO del sandbox. Esto es lo que pone el sandbox mismo:
+`crates/hammer-build/src/sandbox.rs` monta el rootfs Alpine como capa overlay de arriba y **una capa
+más por cada dependencia de build** debajo. Sólo cuando la suma de las rutas se acerca al tope las
+funde en una sola capa de hardlinks (`merge_deps_layer`, el `.dmerge` del store). Con el store en
+`/mnt/vvv/hammer/store` ese umbral cae en las **~27 deps**, así que **1 108 de las 1 171 recetas del
+corpus (95%) construyen con las capas APILADAS**, hasta 27 de ellas. Nadie había medido qué cuesta
+esa N. Medido con `bench_overlay.c` → `overlay.txt` (2026-09-01, mismo `momento`, 7 rondas,
+mínimos; los cocientes son todos de una misma corrida).
+
+El banco monta y desmonta el overlay en **cada ronda**, así que las dentries del overlay están
+frías en cada medida y las del filesystem de abajo, calientes: lo que se aísla es el paseo del
+overlay por sus capas.
+
+| N capas | acierto en la capa 0 | acierto en la capa N-1 | **FALLO** | montar |
+|---|---|---|---|---|
+| 1 | 4 068 ns | — | 3 200 ns | 152 µs |
+| 2 | 4 723 | 4 630 | 4 919 | 199 µs |
+| 8 | 4 205 | 6 209 | 15 867 | 195 µs |
+| 16 | 4 970 | 9 330 | 30 205 | 219 µs |
+| **24** | 4 056 | **10 893** | **44 716** | 245 µs |
+| 64 | 4 717 | 22 939 | **124 448** | 365 µs |
+
+**Tres leyes salen de ahí:**
+
+1. **El acierto en la capa de arriba es plano** (~4,2 µs sin importar N): se encuentra a la primera
+ y no se mira nada más.
+2. **El acierto en la capa del fondo crece +295 ns por capa** — el overlay va preguntando hacia
+ abajo hasta encontrarlo.
+3. **El que paga es el FALLO: +1 925 ns por capa**, porque para decir «no está» hay que preguntar
+ a **todas**. A 64 capas un `stat` que falla cuesta **124 µs**, o sea **1 580 syscalls nulas**.
+
+**Y la ley que lo desactiva todo: se paga UNA VEZ POR RUTA, no por acceso.**
+
+| | ns |
+|---|---|
+| la misma ruta repetida (dentry ya creada) | **664–693, plano en N** |
+| `open` de una ruta recién `stat`-eada | 1 727–2 394, plano en N |
+
+El kernel cachea la dentry fusionada del overlay: el segundo acceso a la misma ruta ya no pasea
+nada. El montaje dura **toda la fase de build**, así que el impuesto de una fase es
+«rutas DISTINTAS tocadas × capas», no «lookups × capas».
+
+**La alternativa —fundirlas en una capa de hardlinks— sale plana en todo**, que es lo esperable
+(vuelve a ser un solo directorio):
+
+| N | acierto | FALLO | fundirlas |
+|---|---|---|---|
+| 8 | 4 061 ns | 3 835 ns | 55,8 ms |
+| 24 | 4 442 | 4 118 | 128,5 ms |
+| 64 | 3 983 | 3 673 | 376,4 ms |
+
+≈ **23 µs por fichero hardlinkeado** (20 µs con artefactos reales del store). Como piso, el mismo
+árbol sin overlay: acierto 878 ns, fallo 2 588 ns.
+
+**Con artefactos REALES del store** (64 con `usr/include`, de 100 a 3 087 ficheros) la forma se
+repite y las constantes bajan —árboles más profundos, dentries de abajo más calientes—: el fallo
+sube **+738 ns por capa** con nombres nunca vistos y **+363 ns** cuando el nombre ya se buscó antes
+(la dentry negativa de ext4 está puesta y sólo se paga el paseo del overlay). Fundir 24 deps reales
+= 16 112 ficheros = **264 ms**.
+
+**La cuenta que decide, y sale al revés de la intuición.** Un `cc -O2 -c` de 12 KB de fuente toca
+**381 rutas distintas que fallan** (352 bajo `/usr`, o sea dentro del overlay) y 260 que aciertan
+—`strace` del propio store, §5 de `overlay.txt`—. A 24 capas reales, cada primer fallo cuesta
+**+7,5 µs** contra la capa fundida ⇒ 381 × 7,5 µs = **2,9 ms**, y se paga **una vez por fase**, no
+por unidad de traducción: la segunda unidad que busque las mismas cabeceras ya las encuentra
+cacheadas. Contra una fase de compilado de segundos a minutos, es **la misma décima de por ciento
+que la jaula de T17/T18**.
+
+Al revés: para que fundir las capas se pagara sola en lookups ahorrados harían falta
+**264 ms / 7,5 µs ≈ 35 200 primeros fallos** en una sola fase — dos órdenes de magnitud más de lo
+que gasta un compilado. ⇒ **`merge_deps_layer` NO es una optimización de velocidad. Es un rodeo de
+un muro**, y conviene que quede escrito porque la caché que deja (`.dmerge`) tiene un precio
+conocido en disco: hardlinkea los artefactos y **retiene lo que borrás del store**.
+
+**El muro, medido exactamente.** El comentario de `sandbox.rs` decía «~1 página (4096 B)» y acierta
+al byte. Bisecado con `mount(2)` directo (`mnt2.c`; **el `mount(8)` de util-linux se rinde mucho
+antes —entre 8 y 12 capas acá— así que bisecarlo desde la shell da un muro FALSO**):
+
+| capas | opciones | |
+|---|---|---|
+| 41 | 4 059 B | monta |
+| **42** | **4 158 B** | **falla, `ENOENT`** |
+
+Una página exacta, y el error es el engañoso «No such file or directory» que ya estaba anotado:
+**el kernel no dice «demasiado largo», dice «no existe»**. Con eso el umbral de `sandbox.rs` queda
+calibrado y no por casualidad: dispara a las ~27 deps y el muro está en 41, o sea **un factor 1,5
+de margen**. Y el margen es estructural, no del store de esta máquina: con rutas de longitud L el
+muro está en ~4002/(L+1) capas y el umbral en 3000/(L+16), cuyo cociente es **≥1,33 sea cual sea
+L**. No hay que recalibrarlo si el store se muda.
+
+**Y el muro tiene puerta, medida.** El límite es del `mount(2)` clásico, que pasa todas las opciones
+en un único buffer de una página. La API de montaje nueva las pasa de a una y overlayfs acepta
+`lowerdir+` repetido; el mismo banco monta así **las 64 capas reales que `mount(2)` no puede**, con
+el mismo coste por capa y montando en 406 µs:
+
+| N (API nueva) | acierto capa N-1 | FALLO | montar |
+|---|---|---|---|
+| 24 | 6 996 ns | 23 070 ns | 302 µs |
+| 48 | 25 388 | 39 832 | 362 µs |
+| **64** | 37 536 | 52 042 | **406 µs** |
+
+Sin muro, `merge_deps_layer` —y con él `.dmerge` entero— dejaría de hacer falta. El precio está en
+§7-H11: **bwrap no usa esa API**, y el overlay se monta dentro de su namespace.
+
+## 3. Las siete sorpresas
Las anoto aparte porque contradicen el consejo de manual:
@@ -399,6 +514,12 @@ Las anoto aparte porque contradicen el consejo de manual:
Lo que sí cuesta es **mirar un argumento**: eso saca a esa syscall del bitmap y le devuelve la
cadena entera. La variable no es el largo, es si el veredicto depende de los args.
+7. **Apilar 24 capas overlay no encarece el build** (T19) — el consejo de manual (y el propio
+ comentario de `sandbox.rs`) invita a leer la fusión de deps como una optimización de velocidad;
+ medida, no lo es: el overlay cobra por PRIMER toque de cada ruta, no por acceso, y fundir 24
+ deps sólo se pagaría sola a partir de 35 200 primeros fallos en una misma fase. Lo que sí
+ justifica fundirlas es el muro de una página del `mount(2)`, que está en 42 capas.
+
## 4. El hallazgo cruzado: los kernels de hammer no tienen `MEMCG`
No lo buscaba; salió de verificar la recomendación antes de escribirla.
@@ -724,6 +845,26 @@ Su precio, concreto:
retener lo borrado— al mismo precio. Es un argumento para wawafs, medido; no un cambio de
filesystem decidido acá.
+- **H11 · El umbral de fusión de capas se queda, y ahora se sabe qué es.** T19 lo midió: no
+ compra velocidad (2,9 ms por fase de compilado a 24 capas), compra **no chocar contra el muro de
+ una página de `mount(2)`**, que está en 42 capas / 4 158 B de opciones. Tres consecuencias:
+
+ 1. **No bajar el umbral «para que vaya más rápido».** No hay nada que ganar y sí que perder: cada
+ fusión cuesta ~20 µs por fichero (264 ms para 24 deps reales) y deja un árbol de hardlinks en
+ `.dmerge` que **retiene los artefactos que borres del store**. Es la deuda de disco conocida.
+ 2. **Y por lo mismo, podar `.dmerge` es GRATIS en rendimiento.** Lo único que se pierde al
+ borrarla es rehacer el `cp -al` de la próxima receta grande: cientos de milisegundos. Eso
+ cambia el cálculo de la poda, que hasta ahora se hacía sin saber qué se estaba tirando.
+ 3. **El umbral no hay que recalibrarlo si el store se muda**: el muro va como ~4002/(L+1) capas y
+ el umbral como 3000/(L+16), así que el margen es ≥1,33 para cualquier longitud de ruta L.
+
+ **Lo que queda abierto, con su precio:** montar el overlay con la API nueva (`fsopen`+`fsconfig`
+ con `lowerdir+` repetido) borra el muro —el banco monta así las 64 capas reales que `mount(2)`
+ rechaza— y con él la necesidad de `.dmerge`. El precio no es el código de hammer: **es bwrap**,
+ que monta el overlay dentro de su namespace con la API vieja. O se parchea bwrap, o el overlay se
+ monta fuera y bwrap lo entra con `--bind`, lo que mueve el montaje al lado privilegiado. No se
+ decide acá; se deja medido que la puerta existe.
+
## 8. Propuestas — lado tawasuyu
Detalle en el handoff espejo. En una línea cada una:
@@ -953,6 +1094,19 @@ jaula no quedó puesta**: comprueba que un número de la denylist da `EPERM` y q
fuera del ruleset da `EACCES`, porque medir una jaula apagada da números preciosos y falsos
(CLAUDE.md §3, la misma regla que el exit 2 de §9.5).
+**Sexta corrida (T19, el overlay por capa, 2026-09-01)**: `bench_overlay.c` → `overlay.txt`. Se
+compila con `cc -O2 -o bench_overlay bench_overlay.c` y **pide root** (hace `mount(2)` de overlay).
+Tres modos: sin argumentos extra construye su propio árbol sintético (secciones A/B/C);
+`./bench_overlay <rondas> <dir-base> <fichero-con-capas>` apila las rutas de ese fichero, una por
+línea, la primera arriba (D1/D2); y con un cuarto argumento cualquiera usa la API de montaje nueva
+(E). **El merge de hardlinks cae en el filesystem de las capas, no en el del banco** —el store de
+`momento` es un bind de `/dev/sdb` y el árbol del banco vive en `/dev/sdc`, así que `cp -al` sería
+cross-device: el mismo cálculo que hace `merge_deps_layer`, y el canario lo destapó—. Deja árboles
+grandes (hasta 16 384 ficheros sintéticos y un merge de los artefactos reales) que hay que borrar a
+mano. **Aborta con exit 2 si la medición no está puesta**: comprueba que todos los ficheros de la
+capa más profunda se ven a través del montaje y que un inexistente no. `overlay.txt` trae además la
+bisección del muro con `mount(2)` directo y el recuento de rutas distintas de un compilado.
+
**El pago de H1** (2026-08-30): `contrato-h1-pagado.txt` — el barrido entero con los cuatro kernels
reconstruidos (5 de 5 vigentes en verde, 10 superados listados) y el `diff-back` del plan de gioser
contra su `.config` nuevo. Es la única salida de esta carpeta que sí cambia si se vuelve a correr:
diff --git a/docs/evidencia/tasas-kernel-2026-08-29/bench_overlay.c b/docs/evidencia/tasas-kernel-2026-08-29/bench_overlay.c
new file mode 100644
index 00000000..bbf251a2
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/bench_overlay.c
@@ -0,0 +1,570 @@
+// bench_overlay.c — SDD 25 / T19: qué cobra overlayfs POR CAPA en un lookup.
+//
+// hammer apila una capa overlay por dependencia bajo el rootfs (crates/hammer-build/src/sandbox.rs,
+// `bwrap_args`) y sólo las funde en una sola capa de hardlinks cuando la suma de rutas pasa de
+// 3000 B. Con el store en /mnt/vvv/hammer/store eso son ~26 deps ⇒ el 95% del corpus construye con
+// N capas apiladas. Este banco mide qué cuesta esa N.
+//
+// Compilar: cc -O2 -o bench_overlay bench_overlay.c
+// Correr : sudo ./bench_overlay [rondas] [dir-base] (root: hace mount(2) de overlay)
+//
+// Método (mismo que el resto de la carpeta): 7 rondas por defecto, se reporta MÍNIMO y mediana.
+// Las capas se construyen UNA vez; el overlay se monta y desmonta en CADA ronda, así que las
+// dentries del overlay están frías en cada medida y las del filesystem de abajo, calientes: lo que
+// se aísla es el paseo del overlay por sus capas, no el coste de ext4. Los FALLOS usan nombres
+// nuevos en cada ronda (`nope_r<ronda>_<i>`) para que tampoco haya dentry negativa cacheada abajo.
+//
+// MODO D (tercer argumento): `sudo ./bench_overlay <rondas> <dir-base> <fichero-con-capas-reales>`
+// — en vez del árbol sintético, apila DEPS REALES del store de hammer (una ruta por línea, la
+// primera es la de más arriba, como el rootfs en `bwrap_args`). Es la comprobación de que la ley
+// que sale de A/B no depende de que las capas sean de juguete: los artefactos reales tienen
+// árboles profundos y miles de ficheros.
+//
+// Sale con 2 si la jaula de la medición no está puesta: que el overlay esté montado y que un
+// fichero de la capa MÁS PROFUNDA se vea a través del punto de montaje. Medir un overlay que no
+// montó da números preciosos y falsos (CLAUDE.md §3).
+
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#define K_FILES 256 // ficheros por capa
+static const int NS[] = {1, 2, 4, 8, 16, 24, 32, 48, 64};
+#define N_NS ((int)(sizeof(NS) / sizeof(NS[0])))
+#define MAXN 64
+
+static char BASE[512];
+static double g_mount_ns, g_umount_ns; // lo que cuesta ARMAR la jaula de capas, por fase de build
+
+static double now_ns(void) {
+ struct timespec t;
+ clock_gettime(CLOCK_MONOTONIC, &t);
+ return (double)t.tv_sec * 1e9 + (double)t.tv_nsec;
+}
+
+static int cmp_d(const void *a, const void *b) {
+ double x = *(const double *)a, y = *(const double *)b;
+ return x < y ? -1 : x > y ? 1 : 0;
+}
+
+static void sh(const char *fmt, ...) {
+ char cmd[4096];
+ va_list ap;
+ va_start(ap, fmt);
+ vsnprintf(cmd, sizeof cmd, fmt, ap);
+ va_end(ap);
+ int r = system(cmd);
+ (void)r;
+}
+
+static void mkpath(const char *p) {
+ char buf[1024];
+ snprintf(buf, sizeof buf, "%s", p);
+ for (char *q = buf + 1; *q; q++) {
+ if (*q == '/') { *q = 0; mkdir(buf, 0755); *q = '/'; }
+ }
+ mkdir(buf, 0755);
+}
+
+// ── construcción del árbol ────────────────────────────────────────────────────────────────────
+// Cada capa i trae `usr/include/` (compartido: fuerza la fusión de directorios, que es lo que hace
+// un overlay real) y K ficheros PROPIOS `usr/include/l<i>_<j>.h`. Un fichero de la capa i sólo
+// existe ahí ⇒ encontrarlo cuesta exactamente i+1 lookups de capa.
+static void construir_capas(int nmax) {
+ char p[1024];
+ for (int i = 0; i < nmax; i++) {
+ snprintf(p, sizeof p, "%s/lower/%d/usr/include", BASE, i);
+ mkpath(p);
+ for (int j = 0; j < K_FILES; j++) {
+ char f[1200];
+ snprintf(f, sizeof f, "%s/l%d_%d.h", p, i, j);
+ int fd = open(f, O_CREAT | O_WRONLY | O_TRUNC, 0644);
+ if (fd >= 0) { (void)!write(fd, "/* x */\n", 8); close(fd); }
+ }
+ }
+ snprintf(p, sizeof p, "%s/upper", BASE); mkpath(p);
+ snprintf(p, sizeof p, "%s/work", BASE); mkpath(p);
+ snprintf(p, sizeof p, "%s/mnt", BASE); mkpath(p);
+}
+
+// lowerdir=l0:l1:...:l(n-1) — el PRIMERO es el de arriba (mayor prioridad), igual que bwrap apila
+// el rootfs primero y las deps debajo.
+static int montar(int n, const char *lower_override) {
+ char opts[8192];
+ int off = snprintf(opts, sizeof opts, "lowerdir=");
+ if (lower_override) {
+ off += snprintf(opts + off, sizeof opts - off, "%s", lower_override);
+ } else {
+ for (int i = 0; i < n; i++)
+ off += snprintf(opts + off, sizeof opts - off, "%s%s/lower/%d", i ? ":" : "", BASE, i);
+ }
+ snprintf(opts + off, sizeof opts - off, ",upperdir=%s/upper,workdir=%s/work", BASE, BASE);
+ char mnt[600];
+ snprintf(mnt, sizeof mnt, "%s/mnt", BASE);
+ double t0 = now_ns();
+ int r = mount("overlay", mnt, "overlay", 0, opts);
+ g_mount_ns = now_ns() - t0;
+ if (r != 0) {
+ fprintf(stderr, "mount overlay (n=%d): %m\n opts=%s\n", n, opts);
+ return -1;
+ }
+ return 0;
+}
+
+static void desmontar(void) {
+ char mnt[600];
+ snprintf(mnt, sizeof mnt, "%s/mnt", BASE);
+ double t0 = now_ns();
+ if (umount(mnt) != 0) fprintf(stderr, "umount: %m\n");
+ g_umount_ns = now_ns() - t0;
+ // El upper es descartable (como el --tmp-overlay de bwrap): vaciarlo entre montajes.
+ sh("rm -rf '%s/upper' '%s/work' && mkdir -p '%s/upper' '%s/work'", BASE, BASE, BASE, BASE);
+}
+
+// ── medidas ──────────────────────────────────────────────────────────────────────────────────
+// stat de K rutas DISTINTAS, cada una por primera vez en este montaje ⇒ cada una paga el paseo.
+static double stat_frio(int capa, int esperado_ok) {
+ char f[1200];
+ struct stat st;
+ int ok = 0;
+ double t0 = now_ns();
+ for (int j = 0; j < K_FILES; j++) {
+ snprintf(f, sizeof f, "%s/mnt/usr/include/l%d_%d.h", BASE, capa, j);
+ if (stat(f, &st) == 0) ok++;
+ }
+ double dt = now_ns() - t0;
+ if (esperado_ok && ok != K_FILES) {
+ fprintf(stderr, "[canario] capa %d: %d de %d ficheros VISIBLES — la medida no vale\n",
+ capa, ok, K_FILES);
+ exit(2);
+ }
+ return dt / K_FILES;
+}
+
+static double stat_fallo(int ronda) {
+ char f[1200];
+ struct stat st;
+ int ok = 0;
+ double t0 = now_ns();
+ for (int j = 0; j < K_FILES; j++) {
+ snprintf(f, sizeof f, "%s/mnt/usr/include/nope_r%d_%d.h", BASE, ronda, j);
+ if (stat(f, &st) == 0) ok++;
+ }
+ double dt = now_ns() - t0;
+ if (ok != 0) { fprintf(stderr, "[canario] un fichero inexistente EXISTE — la medida no vale\n"); exit(2); }
+ return dt / K_FILES;
+}
+
+static double open_frio(int capa) {
+ char f[1200];
+ int ok = 0;
+ double t0 = now_ns();
+ for (int j = 0; j < K_FILES; j++) {
+ snprintf(f, sizeof f, "%s/mnt/usr/include/l%d_%d.h", BASE, capa, j);
+ int fd = open(f, O_RDONLY);
+ if (fd >= 0) { ok++; close(fd); }
+ }
+ double dt = now_ns() - t0;
+ if (ok != K_FILES) { fprintf(stderr, "[canario] open: %d de %d\n", ok, K_FILES); exit(2); }
+ return dt / K_FILES;
+}
+
+// La MISMA ruta muchas veces: la dentry del overlay ya está, no se pasea nada.
+static double stat_caliente(int capa) {
+ char f[1200];
+ struct stat st;
+ snprintf(f, sizeof f, "%s/mnt/usr/include/l%d_0.h", BASE, capa);
+ if (stat(f, &st) != 0) { fprintf(stderr, "[canario] caliente: %m\n"); exit(2); }
+ const int R = 20000;
+ double t0 = now_ns();
+ for (int j = 0; j < R; j++) stat(f, &st);
+ return (now_ns() - t0) / R;
+}
+
+// ── la alternativa real: fundir las N capas en UNA por hardlinks (lo que hace merge_deps_layer) ──
+static double construir_merge(int n) {
+ char m[600];
+ snprintf(m, sizeof m, "%s/merged", BASE);
+ sh("rm -rf '%s'", m);
+ mkpath(m);
+ double t0 = now_ns();
+ for (int i = 0; i < n; i++)
+ sh("cp -alf '%s/lower/%d/.' '%s/' 2>/dev/null", BASE, i, m);
+ return now_ns() - t0; // ns totales
+}
+
+
+// ── MODO D · capas REALES del store ──────────────────────────────────────────────────────────
+#define MAXR 64
+static char CAPA[MAXR][512];
+static char REL[K_FILES][512]; // rutas relativas de ficheros que EXISTEN en la capa más profunda
+
+static int leer_capas(const char *lista) {
+ FILE *fp = fopen(lista, "r");
+ if (!fp) { fprintf(stderr, "no puedo abrir %s: %m\n", lista); exit(2); }
+ int n = 0;
+ char ln[512];
+ while (n < MAXR && fgets(ln, sizeof ln, fp)) {
+ size_t l = strlen(ln);
+ while (l && (ln[l - 1] == '\n' || ln[l - 1] == ' ')) ln[--l] = 0;
+ if (!l || ln[0] == '#') continue;
+ snprintf(CAPA[n++], 512, "%s", ln);
+ }
+ fclose(fp);
+ return n;
+}
+
+// Ficheros reales de la capa `dir`, en rutas relativas, para statearlos a través del montaje.
+static int recolectar(const char *dir, int *nrel) {
+ char cmd[1200];
+ snprintf(cmd, sizeof cmd,
+ "cd '%s' 2>/dev/null && find . -type f -printf '%%P\\n' 2>/dev/null | head -%d", dir, K_FILES);
+ FILE *fp = popen(cmd, "r");
+ if (!fp) return -1;
+ int k = 0;
+ char ln[512];
+ while (k < K_FILES && fgets(ln, sizeof ln, fp)) {
+ size_t l = strlen(ln);
+ while (l && ln[l - 1] == '\n') ln[--l] = 0;
+ if (l) snprintf(REL[k++], 512, "%s", ln);
+ }
+ pclose(fp);
+ *nrel = k;
+ return k;
+}
+
+static double stat_reales(int esperado_ok, int nrel) {
+ char f[1200]; struct stat st; int ok = 0;
+ double t0 = now_ns();
+ for (int j = 0; j < nrel; j++) {
+ snprintf(f, sizeof f, "%s/mnt/%s", BASE, REL[j]);
+ if (stat(f, &st) == 0) ok++;
+ }
+ double dt = now_ns() - t0;
+ if (esperado_ok && ok != nrel) {
+ fprintf(stderr, "[canario] sólo %d de %d ficheros REALES visibles — la medida no vale\n", ok, nrel);
+ exit(2);
+ }
+ return dt / nrel;
+}
+
+static int montar_lista(int n) {
+ char opts[8192];
+ int off = snprintf(opts, sizeof opts, "lowerdir=");
+ for (int i = 0; i < n; i++)
+ off += snprintf(opts + off, sizeof opts - off, "%s%s", i ? ":" : "", CAPA[i]);
+ if (off > 3800) fprintf(stderr, "# AVISO: lowerdir de %d B, cerca del límite de página\n", off);
+ snprintf(opts + off, sizeof opts - off, ",upperdir=%s/upper,workdir=%s/work", BASE, BASE);
+ char mnt[600]; snprintf(mnt, sizeof mnt, "%s/mnt", BASE);
+ double t0 = now_ns();
+ int rc = mount("overlay", mnt, "overlay", 0, opts);
+ g_mount_ns = now_ns() - t0;
+ if (rc != 0) {
+ fprintf(stderr, "mount overlay reales (n=%d): %m\n", n); return -1;
+ }
+ return 0;
+}
+
+static int modo_real(int rondas, const char *lista) {
+ int ncapas = leer_capas(lista);
+ printf("# MODO D — %d capas REALES del store, la primera es la de arriba\n", ncapas);
+ char p[1024];
+ snprintf(p, sizeof p, "%s/upper", BASE); mkpath(p);
+ snprintf(p, sizeof p, "%s/work", BASE); mkpath(p);
+ snprintf(p, sizeof p, "%s/mnt", BASE); mkpath(p);
+
+ int ns[] = {1, 2, 4, 8, 16, 24, 32, 48, 64};
+ printf("\n## D1 · apiladas: ns/ruta, lookup frío\n");
+ printf("%4s %8s %16s %16s %16s %10s\n", "N", "ficheros", "hit capa N-1", "FALLO",
+ "open capa N-1", "mount(us)");
+ for (int a = 0; a < 9; a++) {
+ int n = ns[a];
+ if (n > ncapas) break;
+ int nrel = 0;
+ if (recolectar(CAPA[n - 1], &nrel) <= 0 || nrel == 0) { printf("%4d (capa vacía)\n", n); continue; }
+ double t_hit[64], t_miss[64], t_open[64], t_mnt[64];
+ for (int r = 0; r < rondas; r++) {
+ if (montar_lista(n) != 0) {
+ // El lowerdir de mount(2) cabe en UNA página: con rutas reales del store
+ // (~97 B cada una) el apilado se acaba antes de N=64. Que se acabe ES el dato.
+ printf("%4d %8d — el lowerdir no cabe en una página (ver sandbox.rs)\n", n, nrel);
+ fflush(stdout);
+ goto fin_d1;
+ }
+ t_mnt[r] = g_mount_ns / 1000.0;
+ t_hit[r] = stat_reales(1, nrel);
+ t_miss[r] = stat_fallo(r + 7000 + a * 1000);
+ { // open de los mismos reales
+ char f[1200]; int ok = 0; double t0 = now_ns();
+ for (int j = 0; j < nrel; j++) {
+ snprintf(f, sizeof f, "%s/mnt/%s", BASE, REL[j]);
+ int fd = open(f, O_RDONLY);
+ if (fd >= 0) { ok++; close(fd); }
+ }
+ t_open[r] = (now_ns() - t0) / nrel;
+ if (ok != nrel) { fprintf(stderr, "[canario] open reales %d/%d\n", ok, nrel); return 2; }
+ }
+ desmontar();
+ }
+ qsort(t_hit, rondas, sizeof(double), cmp_d);
+ qsort(t_miss, rondas, sizeof(double), cmp_d);
+ qsort(t_open, rondas, sizeof(double), cmp_d);
+ qsort(t_mnt, rondas, sizeof(double), cmp_d);
+ printf("%4d %8d %8.1f/%6.1f %8.1f/%6.1f %8.1f/%6.1f %10.1f\n", n, nrel,
+ t_hit[0], t_hit[rondas / 2], t_miss[0], t_miss[rondas / 2],
+ t_open[0], t_open[rondas / 2], t_mnt[0]);
+ fflush(stdout);
+ continue;
+fin_d1:
+ break;
+ }
+
+ printf("\n## D2 · las mismas, FUNDIDAS en una capa de hardlinks\n");
+ printf("%4s %8s %16s %16s %16s %10s %10s\n", "N", "ficheros", "hit", "FALLO", "open", "fundir(ms)", "ficheros");
+ for (int a = 0; a < 9; a++) {
+ int n = ns[a];
+ if (n > ncapas) break;
+ int nrel = 0;
+ if (recolectar(CAPA[n - 1], &nrel) <= 0 || nrel == 0) continue;
+ // El merge por hardlinks TIENE que caer en el filesystem de las capas: el store de esta
+ // máquina es un bind de /dev/sdb y BASE está en /dev/sdc ⇒ `cp -al` cross-device falla.
+ // Es el mismo cálculo que hace `merge_deps_layer` en sandbox.rs, y por la misma razón.
+ char m[600];
+ { char dir[512]; snprintf(dir, sizeof dir, "%s", CAPA[0]);
+ char *sl = strrchr(dir, '/'); if (sl) *sl = 0;
+ snprintf(m, sizeof m, "%s/.bench-merge", dir); }
+ sh("rm -rf '%s'", m); mkpath(m);
+ double t0 = now_ns();
+ for (int i = 0; i < n; i++) sh("cp -alf '%s/.' '%s/' 2>/dev/null", CAPA[i], m);
+ double ms = (now_ns() - t0) / 1e6;
+ long nf = 0;
+ { char c[900]; snprintf(c, sizeof c, "find '%s' -type f 2>/dev/null | wc -l", m);
+ FILE *fp = popen(c, "r"); if (fp) { if (fscanf(fp, "%ld", &nf) != 1) nf = -1; pclose(fp); } }
+ snprintf(CAPA[MAXR - 1], 512, "%s", m);
+ char guardado[512]; snprintf(guardado, sizeof guardado, "%s", CAPA[0]);
+ snprintf(CAPA[0], 512, "%s", m);
+ double t_hit[64], t_miss[64], t_open[64];
+ for (int r = 0; r < rondas; r++) {
+ if (montar_lista(1) != 0) return 2;
+ t_hit[r] = stat_reales(1, nrel);
+ t_miss[r] = stat_fallo(r + 9000 + a * 1000);
+ { char f[1200]; int ok = 0; double tt = now_ns();
+ for (int j = 0; j < nrel; j++) {
+ snprintf(f, sizeof f, "%s/mnt/%s", BASE, REL[j]);
+ int fd = open(f, O_RDONLY); if (fd >= 0) { ok++; close(fd); } }
+ t_open[r] = (now_ns() - tt) / nrel; }
+ desmontar();
+ }
+ snprintf(CAPA[0], 512, "%s", guardado);
+ qsort(t_hit, rondas, sizeof(double), cmp_d);
+ qsort(t_miss, rondas, sizeof(double), cmp_d);
+ qsort(t_open, rondas, sizeof(double), cmp_d);
+ printf("%4d %8d %8.1f/%6.1f %8.1f/%6.1f %8.1f/%6.1f %10.1f %10ld\n", n, nrel,
+ t_hit[0], t_hit[rondas / 2], t_miss[0], t_miss[rondas / 2],
+ t_open[0], t_open[rondas / 2], ms, nf);
+ fflush(stdout);
+ }
+ printf("\n# el merge queda en %s/merged-real — borrar a mano\n", BASE);
+ return 0;
+}
+
+
+// ── MODO E · la API de montaje NUEVA (fsopen/fsconfig) ────────────────────────────────────────
+// El muro de D1 —el lowerdir no cabe en una página— es del `mount(2)` clásico, que pasa TODAS las
+// opciones en un único buffer de una página. La API nueva pasa cada opción por separado y overlayfs
+// acepta `lowerdir+` repetido (una capa por llamada). Si eso monta 64 capas reales, el merge de
+// hardlinks de `merge_deps_layer` deja de ser obligatorio — y con él la retención de `.dmerge`.
+#include <sys/syscall.h>
+#ifndef FSCONFIG_SET_STRING
+#define FSCONFIG_SET_STRING 1
+#define FSCONFIG_CMD_CREATE 6
+#endif
+#ifndef MOVE_MOUNT_F_EMPTY_PATH
+#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004
+#endif
+static int fsopen_(const char *fs, unsigned f) { return (int)syscall(__NR_fsopen, fs, f); }
+static int fsconfig_(int fd, unsigned cmd, const char *k, const void *v, int aux) {
+ return (int)syscall(__NR_fsconfig, fd, cmd, k, v, aux);
+}
+static int fsmount_(int fd, unsigned f, unsigned ms) { return (int)syscall(__NR_fsmount, fd, f, ms); }
+static int move_mount_(int ffd, const char *fp, int tfd, const char *tp, unsigned f) {
+ return (int)syscall(__NR_move_mount, ffd, fp, tfd, tp, f);
+}
+
+static int modo_api_nueva(int rondas, const char *lista) {
+ int ncapas = leer_capas(lista);
+ char p[1024];
+ snprintf(p, sizeof p, "%s/upper", BASE); mkpath(p);
+ snprintf(p, sizeof p, "%s/work", BASE); mkpath(p);
+ snprintf(p, sizeof p, "%s/mnt", BASE); mkpath(p);
+ char mnt[600]; snprintf(mnt, sizeof mnt, "%s/mnt", BASE);
+ char up[600], wk[600];
+ snprintf(up, sizeof up, "%s/upper", BASE);
+ snprintf(wk, sizeof wk, "%s/work", BASE);
+
+ printf("# MODO E — fsopen/fsconfig con `lowerdir+`, %d capas reales disponibles\n", ncapas);
+ printf("%4s %8s %16s %16s %12s\n", "N", "ficheros", "hit capa N-1", "FALLO", "mount(us)");
+ int ns[] = {8, 16, 24, 32, 48, 64};
+ for (int a = 0; a < 6; a++) {
+ int n = ns[a];
+ if (n > ncapas) break;
+ int nrel = 0;
+ if (recolectar(CAPA[n - 1], &nrel) <= 0) continue;
+ double t_hit[64], t_miss[64], t_mnt[64];
+ for (int r = 0; r < rondas; r++) {
+ double t0 = now_ns();
+ int fd = fsopen_("overlay", 0);
+ if (fd < 0) { fprintf(stderr, "fsopen: %m\n"); return 2; }
+ int mal = 0;
+ // Orden: la PRIMERA `lowerdir+` es la de más arriba, igual que en la lista separada por ':'.
+ for (int i = 0; i < n; i++)
+ if (fsconfig_(fd, FSCONFIG_SET_STRING, "lowerdir+", CAPA[i], 0) != 0) {
+ fprintf(stderr, "fsconfig lowerdir+ (capa %d): %m\n", i); mal = 1; break;
+ }
+ if (mal) { close(fd); return 2; }
+ if (fsconfig_(fd, FSCONFIG_SET_STRING, "upperdir", up, 0) != 0) { fprintf(stderr, "upperdir: %m\n"); return 2; }
+ if (fsconfig_(fd, FSCONFIG_SET_STRING, "workdir", wk, 0) != 0) { fprintf(stderr, "workdir: %m\n"); return 2; }
+ if (fsconfig_(fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) != 0) { fprintf(stderr, "create (n=%d): %m\n", n); return 2; }
+ int mfd = fsmount_(fd, 0, 0);
+ if (mfd < 0) { fprintf(stderr, "fsmount: %m\n"); return 2; }
+ if (move_mount_(mfd, "", AT_FDCWD, mnt, MOVE_MOUNT_F_EMPTY_PATH) != 0) { fprintf(stderr, "move_mount: %m\n"); return 2; }
+ t_mnt[r] = (now_ns() - t0) / 1000.0;
+ close(mfd); close(fd);
+ t_hit[r] = stat_reales(1, nrel);
+ t_miss[r] = stat_fallo(r + 20000 + a * 1000);
+ desmontar();
+ }
+ qsort(t_hit, rondas, sizeof(double), cmp_d);
+ qsort(t_miss, rondas, sizeof(double), cmp_d);
+ qsort(t_mnt, rondas, sizeof(double), cmp_d);
+ printf("%4d %8d %8.1f/%6.1f %8.1f/%6.1f %12.1f\n", n, nrel,
+ t_hit[0], t_hit[rondas / 2], t_miss[0], t_miss[rondas / 2], t_mnt[0]);
+ fflush(stdout);
+ }
+ return 0;
+}
+
+int main(int argc, char **argv) {
+ int rondas = argc > 1 ? atoi(argv[1]) : 7;
+ snprintf(BASE, sizeof BASE, "%s", argc > 2 ? argv[2] : "/mnt/vvv/.bench-overlay");
+
+ if (geteuid() != 0) { fprintf(stderr, "hace falta root (mount de overlay)\n"); return 2; }
+
+ if (argc > 4) { // 4º argumento cualquiera ⇒ modo E (API de montaje nueva)
+ snprintf(BASE, sizeof BASE, "%s", argv[2]);
+ printf("# bench_overlay — SDD 25 T19 (modo E: fsconfig lowerdir+)\n");
+ sh("rm -rf '%s/mnt' '%s/upper' '%s/work'", BASE, BASE, BASE);
+ mkpath(BASE);
+ return modo_api_nueva(rondas, argv[3]);
+ }
+ if (argc > 3) {
+ snprintf(BASE, sizeof BASE, "%s", argv[2]);
+ printf("# bench_overlay — SDD 25 T19 (modo D: capas reales)\n");
+ printf("# base=%s rondas=%d\n", BASE, rondas);
+ sh("rm -rf '%s/mnt' '%s/upper' '%s/work'", BASE, BASE, BASE);
+ mkpath(BASE);
+ return modo_real(rondas, argv[3]);
+ }
+
+ printf("# bench_overlay — SDD 25 T19\n");
+ printf("# base=%s rondas=%d ficheros/capa=%d\n", BASE, rondas, K_FILES);
+ fflush(stdout);
+
+ sh("rm -rf '%s'", BASE);
+ mkpath(BASE);
+ construir_capas(MAXN);
+
+ printf("\n## A · lookup FRÍO por primera vez, en ns/ruta (capas apiladas)\n");
+ printf("%4s %14s %14s %14s %14s %12s %10s %9s\n", "N", "hit capa 0", "hit capa N-1", "FALLO",
+ "open capa N-1", "hit caliente", "mount(us)", "umount");
+ for (int a = 0; a < N_NS; a++) {
+ int n = NS[a];
+ double t_top[64], t_bot[64], t_miss[64], t_open[64], t_warm[64], t_mnt[64], t_umnt[64];
+ for (int r = 0; r < rondas; r++) {
+ if (montar(n, NULL) != 0) return 2;
+ t_mnt[r] = g_mount_ns / 1000.0;
+ t_top[r] = stat_frio(0, 1);
+ t_bot[r] = stat_frio(n - 1, 1);
+ t_miss[r] = stat_fallo(r + a * 1000);
+ t_open[r] = open_frio(n - 1);
+ t_warm[r] = stat_caliente(n - 1);
+ desmontar();
+ t_umnt[r] = g_umount_ns / 1000.0;
+ }
+ qsort(t_mnt, rondas, sizeof(double), cmp_d);
+ qsort(t_umnt, rondas, sizeof(double), cmp_d);
+ qsort(t_top, rondas, sizeof(double), cmp_d);
+ qsort(t_bot, rondas, sizeof(double), cmp_d);
+ qsort(t_miss, rondas, sizeof(double), cmp_d);
+ qsort(t_open, rondas, sizeof(double), cmp_d);
+ qsort(t_warm, rondas, sizeof(double), cmp_d);
+ printf("%4d %7.1f/%6.1f %7.1f/%6.1f %7.1f/%6.1f %7.1f/%6.1f %6.1f/%5.1f %10.1f %9.1f\n",
+ n, t_top[0], t_top[rondas / 2], t_bot[0], t_bot[rondas / 2],
+ t_miss[0], t_miss[rondas / 2], t_open[0], t_open[rondas / 2],
+ t_warm[0], t_warm[rondas / 2], t_mnt[0], t_umnt[0]);
+ fflush(stdout);
+ }
+ printf("# (mínimo/mediana de %d rondas)\n", rondas);
+
+ printf("\n## B · las MISMAS N capas fundidas en UNA sola (hardlinks, como merge_deps_layer)\n");
+ printf("%4s %14s %14s %14s %12s %14s\n", "N", "hit", "FALLO", "open", "caliente", "fundirlas(ms)");
+ for (int a = 0; a < N_NS; a++) {
+ int n = NS[a];
+ double ms_merge = construir_merge(n) / 1e6;
+ char lower[600];
+ snprintf(lower, sizeof lower, "%s/merged", BASE);
+ double t_hit[64], t_miss[64], t_open[64], t_warm[64];
+ for (int r = 0; r < rondas; r++) {
+ if (montar(1, lower) != 0) return 2;
+ t_hit[r] = stat_frio(n - 1, 1); // el fichero que en A estaba en el FONDO
+ t_miss[r] = stat_fallo(r + 500 + a * 1000);
+ t_open[r] = open_frio(n - 1);
+ t_warm[r] = stat_caliente(n - 1);
+ desmontar();
+ }
+ qsort(t_hit, rondas, sizeof(double), cmp_d);
+ qsort(t_miss, rondas, sizeof(double), cmp_d);
+ qsort(t_open, rondas, sizeof(double), cmp_d);
+ qsort(t_warm, rondas, sizeof(double), cmp_d);
+ printf("%4d %7.1f/%6.1f %7.1f/%6.1f %7.1f/%6.1f %6.1f/%5.1f %14.1f\n",
+ n, t_hit[0], t_hit[rondas / 2], t_miss[0], t_miss[rondas / 2],
+ t_open[0], t_open[rondas / 2], t_warm[0], t_warm[rondas / 2], ms_merge);
+ fflush(stdout);
+ }
+
+ printf("\n## C · piso: el mismo árbol SIN overlay (ext4 directo)\n");
+ {
+ double t_hit[64], t_miss[64];
+ char f[1200]; struct stat st;
+ for (int r = 0; r < rondas; r++) {
+ double t0 = now_ns();
+ for (int j = 0; j < K_FILES; j++) {
+ snprintf(f, sizeof f, "%s/lower/%d/usr/include/l%d_%d.h", BASE, MAXN - 1, MAXN - 1, j);
+ stat(f, &st);
+ }
+ t_hit[r] = (now_ns() - t0) / K_FILES;
+ t0 = now_ns();
+ for (int j = 0; j < K_FILES; j++) {
+ snprintf(f, sizeof f, "%s/lower/%d/usr/include/nope_c%d_%d.h", BASE, MAXN - 1, r, j);
+ stat(f, &st);
+ }
+ t_miss[r] = (now_ns() - t0) / K_FILES;
+ }
+ qsort(t_hit, rondas, sizeof(double), cmp_d);
+ qsort(t_miss, rondas, sizeof(double), cmp_d);
+ printf("stat ext4 hit %.1f/%.1f FALLO %.1f/%.1f ns\n",
+ t_hit[0], t_hit[rondas / 2], t_miss[0], t_miss[rondas / 2]);
+ }
+
+ printf("\n# el árbol queda en %s — borrar a mano\n", BASE);
+ return 0;
+}
diff --git a/docs/evidencia/tasas-kernel-2026-08-29/mnt2.c b/docs/evidencia/tasas-kernel-2026-08-29/mnt2.c
new file mode 100644
index 00000000..b8b46a92
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/mnt2.c
@@ -0,0 +1,31 @@
+// mnt2.c — SDD 25 / T19: dónde está el muro de capas del `mount(2)` clásico.
+//
+// Existe porque bisecarlo con el `mount(8)` de util-linux da un muro FALSO: acá se rendía entre 8
+// y 12 capas (1 247 B de opciones) donde el kernel monta 41. bwrap usa `mount(2)`, así que el muro
+// que importa es éste.
+//
+// cc -O2 -o mnt2 mnt2.c -include errno.h
+// sudo ./mnt2 <n> <fichero-con-capas> <dir-base> (dir-base necesita mnt/, upper/ y work/)
+#define _GNU_SOURCE
+#include <sys/mount.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+int main(int argc,char**argv){
+ int n=atoi(argv[1]); const char*lista=argv[2]; const char*base=argv[3];
+ char capa[128][512]; int c=0; char ln[512];
+ FILE*fp=fopen(lista,"r");
+ while(c<128&&fgets(ln,sizeof ln,fp)){size_t l=strlen(ln);while(l&&ln[l-1]=='\n')ln[--l]=0;if(l)snprintf(capa[c++],512,"%s",ln);}
+ fclose(fp);
+ if(n>c){printf("sólo hay %d capas\n",c);return 1;}
+ char o[16384]; int off=snprintf(o,sizeof o,"lowerdir=");
+ for(int i=0;i<n;i++) off+=snprintf(o+off,sizeof o-off,"%s%s",i?":":"",capa[i]);
+ int lowlen=off-9;
+ off+=snprintf(o+off,sizeof o-off,",upperdir=%s/upper,workdir=%s/work",base,base);
+ char m[600]; snprintf(m,sizeof m,"%s/mnt",base);
+ int r=mount("overlay",m,"overlay",0,o);
+ printf("n=%-3d lowerdir=%-5d B opciones=%-5d B -> %s%s\n",n,lowlen,off,r==0?"OK":"FALLA: ",r==0?"":strerror(errno));
+ if(r==0) umount(m);
+ return r==0?0:1;
+}
diff --git a/docs/evidencia/tasas-kernel-2026-08-29/overlay.txt b/docs/evidencia/tasas-kernel-2026-08-29/overlay.txt
new file mode 100644
index 00000000..16ec19cc
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/overlay.txt
@@ -0,0 +1,115 @@
+# T19 · qué cobra overlayfs POR CAPA — SDD 25
+# Medido el 2026-09-01 en `momento` (KVM/EPYC-Rome, 4 vCPU, 7,7 GiB, Linux 7.1.4-artix1-1)
+# con bench_overlay.c. Máquina CARGADA (load ~3,6 sobre 4 vCPU, cosecha de la granja en vuelo)
+# => los absolutos son cotas superiores; la señal está en los COCIENTES de una misma corrida.
+# 7 rondas, minimo/mediana. El overlay se monta y desmonta en CADA ronda: las dentries del
+# overlay estan frias en cada medida y las de abajo, calientes. Los FALLOS usan nombres nuevos.
+# Capas sinteticas en /dev/sdc (ext4); capas reales = artefactos del store en /dev/sdb (bind).
+
+===============================================================================
+1 · CAPAS SINTETICAS (256 ficheros por capa, todas en el mismo filesystem)
+===============================================================================
+# bench_overlay — SDD 25 T19
+# base=/mnt/vvv/.bench-overlay rondas=7 ficheros/capa=256
+
+## A · lookup FRÍO por primera vez, en ns/ruta (capas apiladas)
+ N hit capa 0 hit capa N-1 FALLO open capa N-1 hit caliente mount(us) umount
+ 1 4067.8/4885.0 1065.2/1490.5 3199.9/3730.0 1727.5/2328.7 664.4/747.2 152.4 10637.3
+ 2 4723.2/5255.7 4629.8/5658.9 4918.5/5954.3 2226.6/2397.7 693.6/877.9 198.5 11269.4
+ 4 4082.9/4585.8 5147.0/6053.4 8630.6/9255.2 1804.7/2483.9 673.2/702.4 177.9 11851.6
+ 8 4204.9/5321.2 6208.8/8277.5 15866.8/20021.8 2114.3/2531.4 679.1/801.9 195.1 11825.4
+ 16 4969.9/5528.2 9330.1/10328.9 30205.1/38274.9 2302.5/2627.9 668.5/755.1 219.1 10975.4
+ 24 4055.5/4377.4 10893.2/11599.4 44716.2/45480.2 2072.6/2206.4 673.5/719.3 244.8 9252.3
+ 32 4353.0/5354.5 13938.4/14776.4 60869.2/67052.7 2315.6/2602.8 677.6/709.0 309.3 10052.3
+ 48 4543.7/5042.9 17377.3/19947.8 93970.6/102367.7 2317.1/2464.8 676.9/747.5 291.1 9166.2
+ 64 4716.9/5151.4 22938.9/24143.3 124448.1/128839.6 2393.9/2479.6 682.5/701.6 364.9 9735.5
+# (mínimo/mediana de 7 rondas)
+
+## B · las MISMAS N capas fundidas en UNA sola (hardlinks, como merge_deps_layer)
+ N hit FALLO open caliente fundirlas(ms)
+ 1 4370.9/5488.2 3387.8/4069.1 1815.2/2282.8 710.7/743.7 4.7
+ 2 3948.5/4865.0 3655.1/4402.2 1657.8/2237.5 692.0/752.5 11.0
+ 4 4207.6/4578.2 3578.0/3797.2 1927.8/2244.3 687.2/726.3 21.7
+ 8 4060.6/6188.4 3834.6/5120.7 2069.1/2766.4 690.0/717.4 55.8
+ 16 3851.7/4656.6 3390.6/4320.5 2001.1/2535.5 674.3/777.1 97.5
+ 24 4441.6/4756.3 4117.7/4676.9 2118.3/2265.7 681.0/795.1 128.5
+ 32 3617.2/6131.1 3398.0/4581.0 1722.4/2897.3 672.9/908.5 199.6
+ 48 4250.3/5046.1 3750.6/4600.1 1771.0/2519.3 702.3/851.0 282.4
+ 64 3982.9/4449.0 3673.2/4122.2 1861.3/2117.6 687.3/702.0 376.4
+
+## C · piso: el mismo árbol SIN overlay (ext4 directo)
+stat ext4 hit 877.9/1226.2 FALLO 2587.5/3165.6 ns
+
+# el árbol queda en /mnt/vvv/.bench-overlay — borrar a mano
+
+===============================================================================
+2 · CAPAS REALES DEL STORE (64 artefactos con usr/include, 100-3087 ficheros)
+===============================================================================
+# bench_overlay — SDD 25 T19 (modo D: capas reales)
+# base=/mnt/vvv/.bench-overlay rondas=7
+# MODO D — 64 capas REALES del store, la primera es la de arriba
+
+## D1 · apiladas: ns/ruta, lookup frío
+ N ficheros hit capa N-1 FALLO open capa N-1 mount(us)
+ 1 256 4828.5/5468.6 1783.6/2276.5 2237.5/3140.5 172.8
+ 2 256 5564.4/6697.8 2187.5/3126.2 2762.6/3724.4 173.7
+ 4 256 5481.4/5786.9 3356.8/3574.1 2439.5/2928.4 176.4
+ 8 256 4573.8/5585.9 4620.9/5342.9 2391.4/2935.1 215.6
+ 16 256 7239.4/8065.6 7896.4/8407.2 2923.6/3609.0 236.9
+ 24 256 5988.1/7217.6 9757.8/11375.6 3114.7/3255.1 252.8
+ 32 256 13520.6/15253.3 13039.9/14217.9 2643.8/3650.2 325.7
+# AVISO: lowerdir de 4663 B, cerca del límite de página
+mount overlay reales (n=48): No such file or directory
+ 48 193 — el lowerdir no cabe en una página (ver sandbox.rs)
+
+## D2 · las mismas, FUNDIDAS en una capa de hardlinks
+ N ficheros hit FALLO open fundir(ms) ficheros
+ 1 256 4626.9/5653.3 2294.5/2588.2 2063.5/2652.2 26.4 3087
+ 2 256 4206.8/5242.6 2247.2/2720.0 2209.8/2738.8 63.9 5508
+ 4 256 4959.6/5381.2 2223.1/2698.8 2515.5/3080.7 96.3 8674
+ 8 256 4640.7/5223.2 2634.9/2753.6 2186.6/2880.0 152.3 11378
+ 16 256 5341.5/6281.3 2540.1/2745.6 2584.2/3145.5 223.9 15354
+ 24 256 4415.5/5190.4 2233.8/2494.2 2070.2/2606.1 264.4 16112
+ 32 256 5923.7/7066.7 2480.6/3148.8 2481.5/3154.2 339.7 18228
+ 48 193 7073.0/7829.2 3166.6/4154.0 2095.4/2707.1 387.2 21232
+ 64 256 4407.3/4953.1 3549.5/3909.4 2135.3/2673.9 482.5 23803
+
+# el merge queda en /mnt/vvv/.bench-overlay/merged-real — borrar a mano
+
+===============================================================================
+3 · LA API DE MONTAJE NUEVA (fsopen/fsconfig con 'lowerdir+' repetido)
+ Monta lo que mount(2) no puede: 64 capas reales, sin muro de pagina.
+===============================================================================
+# bench_overlay — SDD 25 T19 (modo E: fsconfig lowerdir+)
+# MODO E — fsopen/fsconfig con `lowerdir+`, 64 capas reales disponibles
+ N ficheros hit capa N-1 FALLO mount(us)
+ 8 256 5475.8/5918.1 7089.7/9219.7 209.0
+ 16 256 7341.7/8460.4 12109.4/15078.8 267.5
+ 24 256 6996.2/10100.3 23070.0/29872.2 302.0
+ 32 256 12512.3/15712.0 25547.0/29229.4 268.3
+ 48 193 25388.3/33127.3 39832.3/46457.3 362.1
+ 64 153 37535.7/43370.4 52041.6/54458.3 406.2
+
+===============================================================================
+4 · DONDE ESTA EL MURO — bisecado con mount(2) DIRECTO, no con util-linux
+ (el mount(8) de util-linux se rinde mucho antes -entre 8 y 12 capas aca-, asi
+ que bisecar desde la shell da un muro FALSO; bwrap usa mount(2))
+===============================================================================
+n=8 lowerdir=776 B opciones=861 B -> OK
+n=32 lowerdir=3104 B opciones=3189 B -> OK
+n=40 lowerdir=3875 B opciones=3960 B -> OK
+n=41 lowerdir=3974 B opciones=4059 B -> OK
+n=42 lowerdir=4073 B opciones=4158 B -> FALLA: No such file or directory
+n=44 lowerdir=4266 B opciones=4351 B -> FALLA: No such file or directory
+n=48 lowerdir=4654 B opciones=4739 B -> FALLA: No such file or directory
+n=64 lowerdir=6223 B opciones=6308 B -> FALLA: No such file or directory
+
+===============================================================================
+5 · CUANTAS RUTAS DISTINTAS FALLA UN COMPILADO (strace del propio store)
+===============================================================================
+$ strace -f -e trace=file cc -O2 -c bench_connector.c (gcc 16 del host, 12 KB de fuente)
+llamadas de fichero trazadas : 2997
+llamadas que fallan ENOENT : 672
+RUTAS DISTINTAS que fallan : 381
+ de ellas bajo /usr /lib /opt: 352
+rutas distintas con acierto : 260