#!/bin/bash # cazar-cuelgue-bajo-presion.sh — segunda caza del cuelgue de firefox headless, ahora BAJO PRESIÓN # DE MEMORIA, que es la única condición que las cuatro cazas anteriores (202 corridas, 0 cuelgues) # no tenían y que los dos bancos con cuelgues sí. # # ══ LA HIPÓTESIS, Y POR QUÉ ES FALSABLE ════════════════════════════════════════════════════════ # El cuelgue es TODO O NADA: en 202 corridas ninguna pasó de 45 s, o terminan en ~20 s o se bloquean # hasta el timeout. Eso no parece lentitud por paginación —que daría una cola de tiempos largos— # sino un BLOQUEO. La forma más simple de que firefox se bloquee sólo bajo presión es que # **el proceso padre espere a un hijo que el OOM killer se llevó**: `--screenshot` espera a que el # proceso de contenido dibuje, y si el kernel lo mata, la respuesta no llega nunca. # # Eso es falsable con un contador que el kernel ya lleva: `oom_kill` en /proc/vmstat. Se muestrea # antes y después de CADA corrida. Si los cuelgues coinciden con incrementos, es prueba mecánica, # no correlación; si hay cuelgues SIN incremento, la hipótesis se cae y queda acotada por otro lado. # # ══ QUÉ SE MIDE, Y POR QUÉ ESTAS COSAS ═════════════════════════════════════════════════════════ # · MemAvailable y PSI (/proc/pressure/memory) al lanzar ⇒ la presión queda como NÚMERO. Sin esto # «corrí bajo presión» es una afirmación sobre mi intención, no sobre el experimento. # · delta de oom_kill ⇒ el testigo mecánico. # · código de salida ⇒ distingue COLGADA (bloqueada hasta el umbral) de MUERTA (el kernel se la # llevó a los 3 s). Son fenómenos distintos y confundirlos arruinaría el recuento. # · nivel 0 SIN hog ⇒ el control. Sin él, un cuelgue no se puede atribuir a la presión. # # El generador de presión (presion-memoria.py) se marca oom_score_adj=1000: si el kernel tiene que # matar a alguien es a él, nunca a un build de la granja. Y toda la caza corre con el flock de la # granja tomado, para que no arranque una compilación de 4 h mientras se estrangula la RAM. # # Uso: N=20 flock -o work/.farm-build.lock scripts/diagnostico/cazar-cuelgue-bajo-presion.sh # El `-o` NO es opcional: sin él, un firefox que se cuelgue hereda el fd del lock y lo # retiene DESPUÉS de que el arnés termine (regla 1 de CLAUDE.md). Pasó dos veces. set -u RAIZ=/mnt/vvv/takana; R=$RAIZ/store/.rootfs/sway-v2 AQUI=$(cd "$(dirname "$0")" && pwd) N=${N:-20}; UMBRAL=${UMBRAL:-60} SAL=$(mktemp -d); trap 'rm -rf "$SAL"' EXIT FF=($(ls -d $RAIZ/store/8116bdec*-firefox $RAIZ/store/3d199174*-firefox)) PAGS=(fuera-del-corpus.html del-corpus.html) # LAS DEL BANCO: donde ocurrieron las 2 colgadas disp(){ awk '/MemAvailable/{print int($2/1024)}' /proc/meminfo; } psi(){ awk -F'avg10=' '/^some/{print substr($2,1,5)}' /proc/pressure/memory; } ooms(){ awk '/^oom_kill /{print $2}' /proc/vmstat; } HOG="" para_hog(){ [ -n "$HOG" ] && kill "$HOG" 2>/dev/null; HOG=""; sleep 3; } trap 'para_hog; rm -rf "$SAL"' EXIT for NIVEL in 0 2000 1200 700; do if [ "$NIVEL" = 0 ]; then echo "── nivel CONTROL: sin presión" else echo "── nivel: apuntando a $NIVEL MiB disponibles" python3 "$AQUI/presion-memoria.py" "$NIVEL" 1800 >"$SAL/hog.log" 2>&1 & HOG=$! for _ in $(seq 1 40); do sleep 2; [ "$(disp)" -le $((NIVEL+300)) ] && break; done fi echo " arranca con disponible=$(disp) MiB psi=$(psi)" colg=0; muer=0; ok=0 for i in $(seq 1 $N); do d0=$(disp); p0=$(psi); o0=$(ooms) # ⚠ `setsid` + matar el GRUPO, no el bwrap (medido 2026-09-08, DOS veces el mismo día). # `kill $BW` mata el bwrap EXTERIOR, que no es el init del namespace de PID: el firefox de # dentro sobrevive. Y si el arnés corría bajo `flock`, ese superviviente HEREDA el fd del lock # y deja a la granja sin poder compilar hasta que alguien lo note — pasó hora y media, en # silencio. Medido: matando sólo el bwrap queda 1 firefox vivo; matando el grupo, 0. setsid bwrap --ro-bind "$R" / --ro-bind /usr/lib/musl/lib/libc.so /lib/ld-musl-x86_64.so.1 \ --ro-bind "${FF[$(( i % 2 ))]}/usr/lib/firefox" /usr/lib/firefox \ --dev /dev --proc /proc --tmpfs /run --tmpfs /tmp --unshare-pid \ --ro-bind "$AQUI" /bench --bind "$SAL" /salida --unshare-user --uid 0 --gid 0 \ --setenv PATH /usr/bin:/bin \ /bin/sh -c 'export HOME=/tmp/h MOZ_HEADLESS=1 MOZ_DISABLE_CONTENT_SANDBOX=1 LIBGL_ALWAYS_SOFTWARE=1 LANG=C.UTF-8; mkdir -p $HOME LD_LIBRARY_PATH=/usr/lib/firefox /usr/lib/firefox/firefox --headless --screenshot /salida/x.png "file:///bench/'"${PAGS[$(( i % 2 ))]}"'"' \ >"$SAL/nav-$i.log" 2>&1 & BW=$!; t=0 while kill -0 $BW 2>/dev/null && [ $t -lt $UMBRAL ]; do sleep 1; t=$((t+1)); done if kill -0 $BW 2>/dev/null; then colg=$((colg+1)); D=$RAIZ/work/cuelgue-$NIVEL-$i; mkdir -p "$D" { echo "nivel=$NIVEL corrida=$i COLGADA a ${t}s" echo "al lanzar: disponible=$d0 MiB psi_some_avg10=$p0 oom_kill=$o0" echo "ahora: disponible=$(disp) MiB psi_some_avg10=$(psi) oom_kill=$(ooms)" echo "--- ¿mató el kernel a alguien DURANTE esta corrida? delta=$(( $(ooms) - o0 ))"; } > "$D/resumen.txt" ps -eo pid,ppid,stat,rss,wchan:24,comm | grep -iE 'bwrap|firefox|Web Con|Isolated|PID' > "$D/ps.txt" for p in $(pgrep -f 'usr/lib/firefox/firefox' | head -10); do { echo "--- pid $p comm=$(cat /proc/$p/comm 2>/dev/null)" echo " state=$(awk '/^State:/{print $2,$3}' /proc/$p/status 2>/dev/null)" echo " wchan=$(cat /proc/$p/wchan 2>/dev/null)" echo " syscall=$(cut -c1-70 /proc/$p/syscall 2>/dev/null)" echo " rss=$(awk '/^VmRSS:/{print $2,$3}' /proc/$p/status 2>/dev/null)"; } >>"$D/procesos.txt" 2>/dev/null done cp "$SAL/nav-$i.log" "$D/navegador.log" 2>/dev/null kill -TERM -$BW 2>/dev/null; sleep 3; kill -KILL -$BW 2>/dev/null # el GRUPO; printf 'X' else wait $BW; rc=$? od=$(( $(ooms) - o0 )) if [ $rc -ne 0 ]; then muer=$((muer+1)); printf 'M' echo "nivel=$NIVEL i=$i MUERTA rc=$rc t=${t}s disp=$d0 psi=$p0 oom_delta=$od" >> "$RAIZ/work/caza-presion.txt" else ok=$((ok+1)); printf '.'; fi [ $od -gt 0 ] && printf '(oom+%d)' $od fi done echo ""; echo " nivel $NIVEL: ok=$ok COLGADAS=$colg muertas=$muer (disponible final $(disp) MiB, psi $(psi))" para_hog done