# SDD 26 — `atuq`: el envoltorio Gecko de la distro Escrito 2026-09-05, a partir de la pregunta del usuario: *«ya habiendo compilado firefox y waterfox, qué tan complicado vs provechoso suena crear nuestro propio envoltorio, en vez de zen?»*, y de su aclaración de por qué la pregunta existe: **`puriy` quedó muy verde y es difícil sacarlo de ahí**. `atuq` es *zorro* en quechua. El nombre estaba libre —verificado con `grep -ri atuq` sobre `/mnt/vvv/takana` y `/mnt/vvv/tawasuyu`, cero coincidencias— y dice exactamente lo que la cosa es: otro fox, con marca propia y sin usar la de Mozilla (ver el §branding de `recipes/firefox.toml`). **Este documento no reemplaza a `puriy` ni lo cancela.** `puriy` sigue siendo el motor soberano; el §9 explica por qué `atuq` es su andamio y no su desvío. --- ## 1. La corrección que ordena todo: no hay envoltorio fuera del chrome La intuición natural —una carcasa Llimphi con nuestros widgets, y Gecko adentro pintando la página— **no es posible**. Gecko no tiene API de embebido en escritorio desde que murió XULRunner; GeckoView existe y es de Android. No hay forma soportada de meter el motor en una ventana de `mirada`. ⇒ un envoltorio de Gecko es, necesariamente, **chrome-level**: JS/CSS/XHTML *dentro* del propio Firefox, más extensiones, más procesos externos que hablen con él. Y eso es exactamente lo que Zen es. Zen no toca el motor: su diferencia entera —workspaces, split view, glance, compact mode— vive en el chrome, más una herramienta (`surfer`) que reaplica sus parches sobre el árbol de Firefox y recompila. **Su costo no es el motor: es la cinta de correr del rebase cada cuatro semanas.** Copiar su método sería heredar su costo sin heredar su equipo. ## 2. La decisión de arquitectura: artefacto DERIVADO, no fork de fuente Si `atuq` parchea el árbol, **cada iteración de UI cuesta un build de Gecko de cuatro horas**. Eso mata el proyecto en la primera semana de diseño, porque el diseño de chrome es iterativo por naturaleza. Pero nosotros no distribuimos un binario: sellamos artefactos. Entonces `atuq` es una receta cuyo `[deps]` incluye `firefox`, y cuyas fases copian el árbol instalado e inyectan encima: | Qué se inyecta | Mecanismo de Firefox | Sin recompilar | |---|---|---| | Marca, iconos, nombre | `distribution/`, `application.ini` | ✅ | | Prefs de fábrica | `defaults/pref/*.js` + autoconfig (`.cfg`) | ✅ | | Política de empresa | `distribution/policies.json` | ✅ | | Chrome propio (dientes, layout) | `userChrome.css` + re-empaque de `omni.ja` | ✅ | | Extensiones nuestras | `distribution/extensions/` | ✅ | **Ventajas que esto tiene sobre el método de Zen, y que son estructurales:** 1. **Iteración en segundos, no en horas.** El chrome se toca sin volver a compilar C++. 2. **No mueve el `ArtifactHash` de `firefox`.** El corpus no se invalida; `waterfox` y cualquier otro fork siguen compartiendo el mismo artefacto base. 3. **Las CVE se heredan gratis.** Un fork de fuente te vuelve dueño del reloj de seguridad del binario más atacado de la máquina — es lo que hundió a casi todos los forks de Firefox, y el reproche histórico a Waterfox. Un derivado se reconstruye solo cuando sube `firefox`. **Los dos gotchas reales del camino, que son trabajo pero chico:** - **`omni.ja` es un zip ⇒ hay que re-empacarlo determinista** (mtimes fijos, orden estable) o el artefacto deja de reproducir bit a bit. Es la misma disciplina que ya exige la imagen del lab. - **El `jarlog` del PGO ordena el `omni.ja` para el arranque** (§3.bis). Re-empacarlo a lo bruto tira esa optimización a la basura sin que nadie lo note. El re-empaque preserva el orden. - **Hay que invalidar el startup cache** tras tocarlo (fichero `.purgecaches` junto al binario, o bump del BuildID). Si no, los cambios no se ven y **parece que el overlay no agarró**: un falso negativo de los caros, de la misma familia que el cache-hit que congela regresiones. ### 2.bis Cuándo SÍ hay que parchear el árbol Cuando algo necesite tocar C++ del motor. Hoy conocemos un caso: `sct` en su forma fuerte (§6.1). La regla es: **nada baja al árbol antes de que exista la toolchain del §3**, porque hasta entonces cada intento cuesta cuatro horas. Y cuando baje, el vigía de parches (`scripts/vigia-parches.py`, commit `aa200a4`) es lo que convierte esas cuatro horas en un minuto para saber si el parche agarra. ## 3. La puerta de las optimizaciones: no es un flag, es una toolchain El usuario decidió habilitar las optimizaciones. Lo que hay que saber antes de intentarlo: `recipes/firefox.toml` va con `compiler = "gcc"` por el **muro 3** (zig enlaza `libc++` estática para musl y el `configure` de Mozilla exige encontrar un `NEEDED …libc++`; es negativa de upstream, no una perilla). El problema es que **PGO, LTO y BOLT son cadena de clang en Gecko**: - `--enable-lto=cross` quiere clang + lld. El LTO de GCC sobre Gecko no está soportado upstream. - `--enable-profile-use` espera `-fprofile-instr-use` (clang), no `-fprofile-use` (gcc). - BOLT necesita `llvm-bolt`. Y **el corpus no tiene clang usable como compilador**. Verificado en las recetas, no supuesto: - `recipes/clang18.toml` compila `ninja -C build libclang.so clang-resource-headers` y su fase `install` copia **sólo** `build/lib/libclang.so*` y las cabeceras `clang-c`. Está ahí porque `bindgen` carga `libclang.so` en runtime. **No hay driver `clang`, ni `lld`, ni `libc++`.** - `llvm18` se selló con `-DLLVM_ENABLE_PROJECTS=""` — LLVM a secas. ⇒ la primera versión de este documento concluyó que hacía falta **una receta `llvm-toolchain` (clang+lld+libc++ desde fuente)**. **Era caro de más, y se corrigió el mismo día mirando el lab en vez de suponerlo:** - `.dev-fs/alpine` **ya trae clang22 + llvm22 22.1.8** — la misma major que usa el APKBUILD de Alpine para este mismo Firefox (`_llvmver=22`). - **`Compiler::Clang` ya existe en takana**, cableado de punta a punta (`parse_compiler` lo acepta; `takana-build` pone `CC=clang`, `CXX=clang++`, `AR=llvm-ar`). Ninguna receta lo usaba. - Faltaba **sólo `ld.lld`**: `apk add lld` ⇒ dos paquetes, cero upgrades. Y los tres muros caen sin perder lo que gcc daba: el sondeo de linker se satisface con `--enable-linker=lld`, el `ar` lo pone takana solo, y el `NEEDED` de la stdlib de C++ existe porque clang++ de Alpine usa la `libstdc++` **compartida** — la prueba no es teórica: Alpine construye este Firefox con clang22 y **sin** `libcxx` en sus makedepends. **La huella del lab no se movió, y se midió antes de tocar nada.** `lld` no casa ningún prefijo de `TOOLCHAIN_PREFIXES` (`takana-core/src/lab.rs`), así que los 43 paquetes que entran en `hash_inputs` salieron idénticos ⇒ **los 837 artefactos sellados quedan intactos**. Eso abarata el cambio hoy y a la vez deja un agujero escrito: la versión de `lld` no es parte de la identidad del artefacto, y sólo expone a las recetas `compiler="clang"` — hoy, una. Cerrarlo cuesta re-hashear el corpus entero. **Hecho el 2026-09-05** (commit `cb3ecd5`): `firefox` va con `compiler = "clang"`, `--enable-linker=lld`, `--enable-lto=cross`, `--enable-packed-relative-relocs` y `--with-unsigned-addon-scopes=app,system`. Falta construirlo. **Riesgos de PGO, escritos antes de empezar:** - El PGO de Mozilla **corre el navegador** para juntar el perfil (`profileserver.py`, headless + marionette). Dentro de un sandbox hermético, sin red y sin X11, eso hay que hacerlo andar. Headless no necesita display, pero sí necesita que el árbol de perfil se genere adentro. - **El perfil resultante es un input del artefacto.** O entra en `hash_inputs`, o `atuq` deja de ser determinista y no lo vamos a notar: sería otra forma del lab que no está en `hash_inputs`. - Ganancia esperable, para calibrar expectativas: PGO+LTO en Gecko son del orden de 10-25% en carga de página y JS. **Hoy estamos ATRÁS de Zen en este eje, no adelante** — Zen hereda la configuración de Mozilla tal cual. Esto no es una ventaja nuestra: es una deuda que se salda. ### 3.bis El diff contra Alpine — y no es sólo PGO Medido el 2026-09-05 trayendo el `APKBUILD` y el `mozconfig` de `community/firefox` de aports y el `PKGBUILD` de Arch, y comparándolos línea a línea con el mozconfig de `recipes/firefox.toml`. **Alpine es nuestro propio upstream** —de ahí salen los once parches de musl— así que la comparación no es contra una distro lejana: es contra la que ya construye este mismo código. | | `recipes/firefox.toml` | Alpine `community/firefox` | |---|---|---| | LTO | **no** | `--enable-lto=cross` | | PGO | **no** | `--enable-profile-use=cross` + `merged.profdata` + `jarlog` | | Linker | GNU ld (via gcc) | `--enable-linker=lld` (lld 22) | | RELR | **no** | `--enable-packed-relative-relocs` | | Sandbox RLBox | **APAGADO** (`--without-wasm-sandboxed-libraries`) | `--with-wasi-sysroot` | | Extensiones sin firmar | **no declarado** | `--with-unsigned-addon-scopes=app,system` | | Allocator | `--disable-jemalloc` | `--disable-jemalloc` — **igual, no es desviación nuestra** | | Librerías | todo bundleado | `--with-system-{icu,nss,nspr,av1,vpx,webp,png,jpeg,zlib,ffi,libevent,pixman,pipewire}` | Arch hace lo mismo en el eje de velocidad («Do 3-tier PGO», `--enable-lto=cross`, `--enable-profile-use=cross`). **Ni Alpine ni Arch usan BOLT** (cero menciones en los dos ficheros) ⇒ BOLT no es la desventaja, es el extra. Lo que nos separa del resto del mundo es **PGO + LTO**, del orden de 10-20% en carga de página y JS. **Tres lecturas que hay que sacar de esa tabla:** 1. **RLBox apagado es una desventaja de SEGURIDAD, no de velocidad, y probablemente pesa más que el PGO.** Es la jaula wasm alrededor de los parsers de fuentes y medios —graphite, ogg, expat, woff2—, o sea exactamente el código que come entrada no confiable. Encenderlo pide `wasi-sdk` + `wasi-compiler-rt` en el corpus: receta propia, no un flag. 2. **`--with-unsigned-addon-scopes=app,system` nos falta y `atuq` lo NECESITA** para shipear sus propias extensiones desde `distribution/extensions/` (§6.1, §7). Es un flag de `configure` ⇒ **una capacidad de `atuq` que no se resuelve en el overlay: exige tocar la base.** 3. **Las librerías del sistema son diferencia a propósito, no deuda.** Alpine usa las suyas; nuestro corpus *es* la fuente y bundlear es lo que mantiene la clausura cerrada. **Corolario que ordena el plan: todo esto entra en UN SOLO rebuild de `firefox`.** Cada rebuild son cuatro horas y re-sella la cola Gecko entera (waterfox incluido), así que LTO, PGO, RELR, lld, RLBox y el scope de addons se juntan en una sola pasada, no en seis. ### 3.ter Los dos muros del PGO que las distros no tienen **El perfil se junta corriendo el navegador, y las dos distros lo hacen bajo `xvfb-run`** (Alpine: `xvfb-run -a -s "-screen 0 1920x1080x24" ./mach python build/pgo/profileserver.py`; Arch, idéntico). **Nosotros no tenemos X11 en el corpus** —es la decisión Wayland-only de toda la distro— así que ese camino no existe acá. La salida takana-nativa es correrlo bajo un **sway headless** (`WLR_BACKENDS=headless`), que ya tenemos del frente wlr/sway y arranca en segundos. **✅ EL MURO DEL DISPLAY ESTÁ DESPEJADO (2026-09-06).** No es una previsión: se corrió. `sway` headless con `WLR_BACKENDS=headless` y `WLR_RENDERER=pixman` arranca en **gioser**, que es un LXC **sin `/dev/dri`** —o sea sin GPU ninguna, todo software— y dibuja píxeles de verdad: `docs/evidencia/sway-headless-gioser-sin-gpu-2026-09-06.png` (sway 1.10 / wlroots 0.18.2 / foot 1.27.0, los tres binarios nuestros, salida `HEADLESS-1` a 1280x720). El ciclo entero es de ~2 min con `scripts/wlr/sway-headless.sh`. Dos cosas que hubo que resolver y conviene tener escritas antes de repetirlo: · **Hidratar bajo el montaje del store.** `hydrate-profile.py` ENLAZA, y un hardlink no cruza montajes: con `--into work/...` sale `EXDEV` porque el store vive en otro disco. Con `--into store/.rootfs/sway` son 193/193 nodos, 41.677 ficheros y **4,9 G que no ocupan** (son hardlinks al store). · **Inyectar el loader de musl.** `sway` salió DINÁMICO y pide `/lib/ld-musl-x86_64.so.1`, que el cierre no trae; el error es `exec: /usr/bin/sway: not found`, que se lee como «falta el binario» y lo que falta es su intérprete. Se resuelve con `--ro-bind /usr/lib/musl/lib/libc.so /lib/ld-musl-x86_64.so.1` (en musl el `libc.so` ES el loader). Es la misma familia que el resto de la noche: el mensaje nombra lo que buscó, no lo que falta. **Y el perfil no es determinista.** Los contadores dependen del timing de la corrida, así que dos generaciones del profdata no dan los mismos bytes — a las distros no les importa porque no persiguen bit-repro; a nosotros nos rompe el invariante. La salida: **generar el perfil UNA vez y sellarlo como artefacto propio, consumido por hash** desde `firefox`. El build optimizado vuelve a ser determinista aunque su insumo no lo sea, y el perfil se regenera a propósito, no por accidente. ### 3.quinquies El PGO, y los tres muros que el plan no tenía escritos (2026-09-07) El §3.ter daba dos muros: el display (X11 que no tenemos) y el no-determinismo del perfil. Los dos eran ciertos. Aparecieron **tres más**, y ninguno se ve sin construir: 1. **`libclang_rt.profile.a` no existía.** El lab trae clang 22.1.8 pero **ninguna runtime de compiler-rt** — su `lib/` del resource dir ni existe, exactamente como ya documentaba `wasi-compiler-rt` para el caso wasm. El build instrumentado murió en el minuto 38:56 con `ld.lld: cannot open …/libclang_rt.profile.a`. De ahí sale `recipes/compiler-rt-profile.toml`. 2. **Los sandboxes de Firefox matan la corrida de perfilado.** Con ellos puestos los procesos hijo mueren con **signal 11**, el que renderiza no llega a nacer, el servidor local registra **0 GET** y el único `.profraw` que queda es el del padre arrancando — un perfil de nada, con todo en verde. Es lo mismo que hace el `profileserver.py` de Mozilla. Sólo aplica a la corrida de entrenamiento: el firefox que se distribuye lleva sus sandboxes intactos. 3. **El worker no puede bajar del mirror de fuentes.** Tiene `scripts/fuentes/mirror-env.sh` desde el 2026-09-01 pero no la clave del Storage Box, así que una receta pineada al mirror —y el perfil es la primera— no se construye allá. Se sembró el artefacto sellado, que es content-addressed: consigue lo mismo sin mover una credencial de máquina. **Cómo se verificó que el PGO llegó, y por qué NO como RLBox.** Con RLBox el artefacto delata la jaula (símbolos `w2c_*`, cero → 634). Acá el discriminante análogo serían las secciones `.text.hot`/`.text.unlikely` que clang emite al particionar por temperatura — y **no sirve**: con lld y ThinLTO el enlazador las fusiona, así que dan cero en el binario con PGO y sin él. La evidencia que sí vale está un nivel por debajo del `configure`: **189 invocaciones del compilador llevan `-fprofile-use`**, el configure encontró `llvm-profdata`, y no hay ni un aviso de perfil que no cuadre. `libxul.so` pasa de 226.078.752 a 227.802.816 bytes, consistente con inlining de caminos calientes. Es evidencia de build, no de artefacto, y conviene decirlo así. ⚠⚠ **LEER PRIMERO: LOS PORCENTAJES DE LAS TRES TABLAS QUE SIGUEN ESTÁN DILUIDOS Y NO SON LO QUE PARECEN.** Miden el ciclo completo del proceso, del que **~16 s son arranque**. La corrección, con el control que faltaba, está más abajo en «EL CONTROL QUE FALTABA». Se dejan tal cual se publicaron, sin retocar, porque el error de método es el que enseña. **LA GANANCIA, MEDIDA (2026-09-07).** Mismo rootfs, mismo script, corridas INTERCALADAS A/B/A/B para que la deriva de carga afecte a las dos, mediana de 7, calentamiento descartado. Lo único que cambia entre variantes es un `--ro-bind` de `/usr/lib/firefox`. | página | sin PGO | con PGO | | |---|---|---|---| | DOM+layout+strings, **fuera** del corpus de entrenamiento | 23.540 ms | 21.327 ms | **−9,4 %** | | SunSpider `3d-raytrace`, **dentro** del corpus | 16.034 ms | 15.878 ms | −1,0 % | Los rangos no se solapan en ninguna de las dos (23.185–23.654 vs 21.030–21.650), así que las dos diferencias son reales y no ruido. ⚠ **Y el resultado sale al revés de lo esperable, que es lo interesante.** La intuición dice que medir sobre el conjunto de ENTRENAMIENTO infla la ganancia; acá el corpus da el número MÁS BAJO. La razón es que `3d-raytrace` es aritmética pura en un bucle caliente, y ese bucle **no lo ejecuta el C++ de SpiderMonkey: lo ejecuta código máquina que el JIT genera en tiempo de ejecución**. El PGO optimiza el intérprete, el GC y el propio compilador JIT — no el código que el JIT emite. Un benchmark JIT-bound es casi ciego al PGO por construcción. Donde sí se ve es en el camino DOM/layout/arranque, que es C++ de principio a fin. Y eso es también lo que un usuario percibe: la medición cubre el ciclo COMPLETO del proceso (arrancar → renderizar → capturar → salir), no el rendimiento en régimen de una página ya cargada. De ahí que hasta la página de SunSpider tarde 16 s: casi todo es arranque. **Corolario para el corpus de entrenamiento — APLICADO Y MEDIDO (2026-09-08).** El corpus estaba sesgado hacia JS justo donde el PGO menos rinde, así que se le sumaron 10 páginas de maquetación propias (`scripts/pgo-corpus/`): flexbox, grid, tablas con los dos algoritmos de layout, texto en columnas, selectores contra 600 reglas, pintado, transforms, SVG, scroll pegajoso y layout thrashing. El perfil pasó de 5.885.254.799 ejecuciones registradas a **38.498.366.335 (6,5×)**. | | sin PGO | PGO v1 (36 pág.) | PGO v2 (46 pág.) | |---|---|---|---| | DOM/maquetación, fuera del corpus | 23.285 ms | 21.050 ms (−9,6 %) | **20.640 ms (−11,4 %)** | | SunSpider `3d-raytrace` | 16.003 ms | 15.846 ms (−1,0 %) | 15.857 ms (−0,9 %) | **La mejora en maquetación es real y no de medianas:** SEIS de las siete muestras de v1 son más lentas que TODAS las de v2; sólo una cae dentro del rango de v2. En SunSpider, en cambio, v1 y v2 son **indistinguibles** —los valores se entrelazan por completo— que es justo lo esperable: ese camino lo ejecuta el JIT y el PGO no lo toca. Un corpus mejor no puede mejorar lo que el PGO no alcanza. ⚠ Una de las siete corridas de v2 dio 120.029 ms contra ~20.600 de las otras seis: un atípico transitorio. **La mediana lo ignora por diseño**; una media lo habría convertido en «v2 es catastróficamente peor». Fue la razón de elegir mediana antes de ver un solo número. **EL JARLOG: PROBADO, MEDIDO, Y APARCADO PORQUE ROMPE `atuq` (2026-09-08).** Desde `d9833a58` el perfil trae también el `jarlog` y `firefox` lo consume con `--with-pgo-jarlog`. No hizo falta la extensión `Quitter` de Mozilla, que este documento daba por bloqueante: su `profileserver.py` sólo traduce `JARLOG_FILE` a `MOZ_JAR_LOG_FILE` en el entorno del navegador. | | sin PGO | v1 (36 pág.) | v2 (46 pág.) | v3 (= v2 + jarlog) | |---|---|---|---|---| | DOM/maquetación | 23.008 ms | 20.851 (−9,4 %) | 20.527 (−10,8 %) | 20.502 (−10,9 %) | | SunSpider | 16.006 ms | 15.859 (−0,9 %) | 15.869 (−0,9 %) | 15.838 (−1,0 %) | **El jarlog solo aporta −0,1 % y −0,2 %: indistinguible de cero.** Y el argumento no es que los rangos se solapen —que se solapan— sino que **la misma variante varía ~1 % entre corridas de días distintos** (v1 dio 20.851 hoy y 21.050 ayer). El suelo de ruido del banco es **diez veces** el efecto buscado. Eso NO es un fallo del jarlog: es que el banco corre con la **caché de página caliente**, y ahí reordenar `omni.ja` no ahorra ninguna lectura. Su terreno es el arranque EN FRÍO, que este instrumento no reproduce; medirlo pediría vaciar la caché entre corridas, que es una acción de sistema. **Lo que sí está probado es que la función existe**, y del lado del artefacto: `libxul` es IDÉNTICO byte a byte entre v2 y v3 y el `omni.ja` difiere en 26 bytes — el jarlog tocó exactamente lo que debía tocar y nada más. **Y sí costaba.** Al reconstruir `atuq` sobre ese firefox, murió: zipfile.BadZipFile: Bad magic number for central directory (rebrand.py) El jarlog convierte el `omni.ja` al **formato «jar optimizado» de Mozilla**, que mueve el directorio central al principio: sin jarlog: PK\003\004 ZIP estándar — `zipfile` lo abre, 5306 entradas con jarlog: \376\204#\0 `zipfile` lo rechaza Y `atuq` reempaqueta el `omni.ja` con `zipfile` para su branding. O sea que el jarlog **rompe el navegador propio de la distro**. **Aparcado, y la cuenta es asimétrica:** el coste está MEDIDO y el beneficio NO. Cambiar algo que funciona por una ganancia que no se pudo medir, rompiendo algo que sí funcionaba, es mal negocio. Se retoma cuando (1) se mida el arranque EN FRÍO, que es su terreno, y (2) `rebrand.py` sepa leer el jar optimizado o des-optimizarlo antes. El blob `92497cdd…` del mirror ya trae el jarlog, así que retomarlo es cambiar una línea y re-pinear el sha256 — no volver a perfilar. ⚠ **Y la lección de método, que es la que más vale:** el jarlog parecía GRATIS. «Es lo que hace upstream y no cuesta nada» — lo escribí yo, en este documento, hace unas horas. El coste no apareció midiendo el jarlog sino **construyendo lo que dependía de él**. Una función que se declara gratuita sin haber reconstruido a sus consumidores no es gratuita: es no medida. ⚠ **Dato que los datos delatan:** los dos valores atípicos del banco son 120.034 y 120.030 ms, o sea exactamente el `timeout 120` del arnés. No son ruido de carga: son corridas COLGADAS al arrancar en headless, 2 de 56 (~3,5 %). Sin perseguir, pero anotado. ⚠ Y lo que este banco NO puede afirmar: las 10 páginas nuevas ejercitan los mismos subsistemas que la página de medición, así que el parentesco con lo que ahora se entrena es mayor que antes. El −11,4 % es real para ESTA carga; generalizarlo a «cualquier página» sería justo el error que el propio corpus de Mozilla cometía al revés. ## EL CONTROL QUE FALTABA: el arranque eran 16 s, y sin restarlo ningún porcentaje significa nada (2026-09-08) Todo lo de arriba mide **el ciclo completo del proceso**: arrancar firefox, renderizar, capturar el PNG y salir. Nunca se midió cuánto de eso era la página. Faltaba el control más barato posible —una página VACÍA— y con él las cifras publicadas se leen distinto: | carga | total sin PGO | total con PGO | **trabajo** sin → con | mejora del TRABAJO | |---|---|---|---|---| | `vacia.html` (arranque puro) | 16.026 ms | 15.883 ms | — (ES la línea base) | **−0,9 %** | | DOM/maquetación | 23.307 ms | 20.627 ms | 7.281 → 4.744 ms | **−34,8 %** | | carga ajena (canvas/JSON/img) | 20.576 ms | 20.517 ms | 4.550 → 4.634 ms | **+1,8 % (nada)** | | SunSpider `3d-raytrace` | 15.992 ms | 15.877 ms | **−34 → −6 ms** | bajo el ruido | Las cuatro cargas y las dos variantes, **en una sola sesión intercalada**: restar el arranque de una corrida al total de otra no vale, porque la misma variante deriva ~1 % entre días. **Qué cambia, en concreto:** 1. **El «−10,9 %» que publiqué es correcto como número y engañoso como afirmación.** Es real para el ciclo completo, que es lo que un usuario espera al abrir el navegador. Pero se leía como «firefox es un 11 % más rápido», y sobre el trabajo de la página la mejora es **−34,8 %**: el PGO rinde TRES VECES más de lo que dije. El error subestimaba mi propio resultado. 2. **La explicación que di del SunSpider era aire.** Escribí que el −1,0 % se explicaba porque un benchmark JIT-bound es ciego al PGO. Mirá la fila: su trabajo mide **−34 ms**, negativo — la página con el benchmark tardó MENOS que la vacía. `3d-raytrace` corre en decenas de ms y su trabajo es más chico que la variación entre corridas. **Ahí no había nada que medir.** El −1,0 % era el −0,9 % del arranque, y yo le colgué encima una teoría sobre el JIT. La teoría puede ser cierta —el mecanismo es real y está bien descrito— pero **esta medición nunca la probó**, y la presenté como si sí. Un número real más un mecanismo real no hacen una explicación: hace falta que el número mida el mecanismo. 3. **La carga ajena confirma que el PGO no es magia.** +1,8 % sobre su trabajo, o sea cero: mejora lo que se le entrenó (maquetación, −34,8 %) y no lo que no (canvas/JSON, 0 %). Eso es exactamente lo que un PGO honesto debe hacer, y es mejor evidencia de que funciona que el número grande. 4. **El jarlog no se rehabilita.** Su −0,1 % diluido pasa a ~−0,5 % sin diluir; sigue muy por debajo del ~1 % que la misma variante deriva entre días. La conclusión de aparcarlo no se mueve. ⚠ **La lección, que es la misma de toda esta jornada con otro disfraz.** El instrumento funcionaba, las medianas eran correctas, las corridas estaban intercaladas, los rangos no se solapaban — todo el rigor estadístico estaba puesto **sobre una cantidad que no era la que yo creía estar midiendo**. Ninguna cantidad de repeticiones lo habría revelado: sólo lo revela un control, y el control costó siete corridas de una página vacía. **Antes de refinar la precisión de un número, hay que gastar una medición en averiguar qué mide.** Y el detalle que lo vuelve barato de recordar: el control salió NEGATIVO en una fila. Un valor imposible es la forma más limpia que tiene una medición de avisar que el modelo está mal, y por eso esa fila se deja publicada con su signo menos en lugar de redondearla a cero. **Y la pregunta que un insumo no determinista obliga a hacer, respondida: `firefox` SIGUE REPRODUCIENDO bit a bit** (`verificar-repro.sh`: REPRODUCEN 1, DERIVA 0, NO-DETERMINISMO 0). Era el riesgo real de esta unidad, no el rendimiento: si el `-fprofile-use` hubiera metido cualquier decisión dependiente del orden o del timing, habríamos cambiado velocidad por el invariante del proyecto. Congelar el perfil como fuente pineada era el argumento; esto es la medición. ### 3.quater La cadena wasm, y las tres cosas que costó (2026-09-06) `--without-wasm-sandboxed-libraries` se cambió por `--with-wasi-sysroot=/usr/share/wasi-sysroot`, que es literalmente el flag de Alpine. Detrás hay cinco recetas nuestras, en este orden obligatorio: ``` wasi-libc-headers → wasi-compiler-rt → wasi-libc → wasi-libcxx → wasi-sdk → firefox ``` `wasi-libc-headers` existe **sólo** para romper el ciclo del bootstrap y pinea un commit VIEJO a propósito (triple `wasm32-wasi`), mientras `wasi-libc` pinea el nuevo (`wasm32-wasip1`, el que clang 22.1 usa de verdad). Es lo mismo que hace Alpine y no es un descuido. **Lo que costó, que es la parte reusable:** 1. **`check-symbols` falla por sesgo de clang, no por un artefacto malo.** El lab trae clang 22.1.8 y el snapshot del árbol viene de uno anterior: sobra `#define __wasip1__ 1` y nada más. Se reconcilia el snapshot; **no** se salta con `make no-check-symbols`, que se lleva por delante la comparación de símbolos —la mitad que sí vale y que pasaba byte a byte. 2. **Faltaba `wasi-libcxx`, y el hueco era visible en el grafo sin construir nada.** El configure murió a los 6 s con `'cstring' file not found`, culpando a los headers de WASI, que estaban perfectos: `` es C++ y las librerías enjauladas no son todas C. El `wasi-sdk` de Alpine declara `wasi-libc wasi-libcxx wasi-compiler-rt` —TRES— y la cadena tenía dos. **El `depends` del paquete ajeno equivalente es una comprobación de completitud gratis**, antes de gastar CPU. 3. **Un directorio VACÍO que sí importa.** Con `wasi-libcxx` sellado el configure volvió a morir igual, y `cstring` estaba en el artefacto: clang no lo buscaba. Falta el `mkdir -p include/c++/v1` que Alpine tiene en su `package()` y que parece ruido de empaquetado — es la sonda por la que el driver decide que el sysroot tiene layout de libc++. Medido dentro del lab: sin ese directorio, `'cstring' file not found`; con él, exit 0. Es el **reverso exacto de la regla 3 del `CLAUDE.md`**: allá un directorio vacío es un artefacto mentiroso, acá es una declaración dirigida al compilador. De ahí sale el criterio de guardián que ahora usan estas recetas: **«existe» no es «se encuentra»**. `wasi-libcxx` corre hoy la misma prueba que hace el configure de firefox, contra un sysroot fusionado con el de su dep, así que un fallo aparece en el segundo 20 de una receta chica —con la lista de búsqueda de clang impresa— y no en el segundo 6 de un build de cuatro horas que además culpa a otro. Y `firefox` exige RLBox **en el binario**: sobre `b3:352d7880` (el último con RLBox apagado) los patrones `w2c_`, `rlbox` y `wasm2c` dan cero los tres en `libxul.so`, así que cualquiera apareciendo prueba que la jaula entró. Es la tercera repetición de la lección de `MOZ_REQUIRE_SIGNING` y `MOZ_BUILD_DATE`: una bandera que el configure acepta y el artefacto ignora. **Evidencia de reproducibilidad cruzada (2026-09-06).** Las cinco recetas se construyeron por separado en las DOS máquinas —el hub (gioser, 4c) y el worker (`dev.gioser.net`, LXC 6c)— y los cinco artefactos salen **byte a byte idénticos**, comparando el árbol completo de cada uno: | receta | sha256 del árbol | | |---|---|---| | `wasi-libc-headers` | `0d34f9cb27530606…` | ✓ | | `wasi-compiler-rt` | `0c4fc6fdf08ef908…` | ✓ | | `wasi-libc` | `c13acdf896f2b78c…` | ✓ | | `wasi-libcxx` | `b1d9aeec08499063…` | ✓ | | `wasi-sdk` | `b49f7a74a5caed3f…` | ✓ | No es un trámite: **el lab NO entra en `hash_inputs`**, así que dos labs distintos pueden sellar bytes distintos en la misma dirección del store y nada lo detecta. Esto dice que para esta cadena los dos labs coinciden de verdad, y no sólo que coinciden los `ArtifactHash`. ## 4. Lo que `atuq` NO promete Esta sección existe antes que la lista de features a propósito, con el mismo criterio que el modelo de adversario de `qullqa`: **lo que no se promete se escribe primero, porque una promesa falsa de privacidad es peor que no ofrecer nada.** **`atuq` no es Tor Browser y no va a tener «modo Tor».** Tor Browser no es «Firefox + SOCKS»: su valor son las contramedidas de huella (RFP, letterboxing, normalización de fuentes/canvas/timers) y sobre todo **un conjunto de anonimato donde todos se ven idénticos**. `atuq` es, por construcción, un binario único en el mundo: musl, Wayland-only, branding propio, extensiones propias. Alguien saliendo por Tor desde `atuq` es **más** identificable, no menos, y su conjunto de anonimato son diez personas. Para anonimato: Tor Browser, y lo decimos nosotros primero. Lo que sí se ofrece, y es honesto porque es otra cosa: **proxy por contenedor** (§6.8) — separación de tráfico, no anonimato. **`atuq` tampoco promete** anti-fingerprinting propio (es un programa de investigación con equipo full-time) ni bloqueo de anuncios propio (se shipea uBO y listo). ## 5. Contexto verificado al escribir (2026-09-05) De `docs/state/build-state.json`, no de memoria: | Dato | Valor | |---|---| | Recetas / nodos | 838 / 840 | | Selladas | 837 | | `firefox` 154.0 | **sealed**, cola `corpus` | | `waterfox` 6.7.1.1 | **never** — primer build en curso | | Plataforma compartida | `gtk3`, `atk`, `nodejs`, `clang18`, `cbindgen`: todas sealed | **`atuq` no arranca hasta que `waterfox` selle.** Waterfox es la prueba de la tesis del reúso —«las diferencias reales con `firefox.toml` son TRES: la fuente, el branding y la versión»—. Si esa tesis falla, el precio de todo este documento cambia y hay que releerlo. ## 6. Los diferenciadores, priorizados La columna «quién más lo tiene» es lo que evita que nos contemos un cuento. | # | Qué | Quién más lo tiene | Pieza que ya existe | Costo | |---|---|---|---|---| | 6.1 | **`sct` — transparencia de scripts** | **nadie** | `puriy-sct`, `puriy-sct-testigo` | medio | | 6.2 | **Descargas direccionadas por contenido** | nadie | store CAS de takana, `tejido` | bajo | | 6.3 | **Archivo personal + RAG local** | Rewind/Recall (nube, Windows); SingleFile (guarda, no busca) | `khipu`, `rag-motor`, `willay-rag` | medio | | 6.4 | **Historial y perfil sobre `qullqa`** | nadie | `qullqa-core`, `qullqa-pozo` | medio | | 6.5 | **Foco por `cortafuegos`, no por extensión** ✅ **cadena y navegador; falta quién lo aplica** | nadie (nadie es dueño del navegador *y* del sistema) | `cortafuegos`, `nftables` (nuevo en el corpus) | bajo | | 6.6 | **Medios por fuera del navegador** | extensiones sueltas; de fábrica no | `foreign-ytdlp`, `-platform`, `-dlna` | bajo | | 6.7 | **IA local en la barra lateral** ✅ **motor**; falta modelo y barra | Chrome/Edge son nube; Zen no tiene | `llama-cpp` (NUEVO en el corpus) — `rimay`/`iniy` NO servían, ver §6.7 | bajo | | 6.8 | **Proxy por contenedor** ✅ **v0.5** | nadie de fábrica | — (API de Firefox) | bajo | | 6.9 | **Torrent adentro** | **Vivaldi lo trae; Brave tuvo WebTorrent** | `shared/foreign-torrent` (SDD propio, sobre librqbit) | bajo | ### 6.1 `sct` es la joya BLAKE3 a todo lo que se ejecuta, y **negarse a correr código que ningún testigo vio nunca**. Es lo más cerca que hay de «Certificate Transparency, pero para el JavaScript que te corre en la cara», y no lo tiene nadie: ni Zen, ni Brave, ni Tor Browser. En tawasuyu ya está escrito y —dice su README— la lógica es **agnóstica del transporte** y está certificada sin red; `puriy-sct-testigo` es sólo el cable HTTP. - **v1 — HECHA 2026-09-10, y con una corrección de este propio párrafo.** La extensión intercepta el cuerpo de cada `