← bitácora

Lo que el navegador se guarda

La memoria es una feature, y olvidar también

Cuatro de los cambios de este lote van de qué retiene el motor: una caché que lo guardaba todo y lo perdía todo al salir, cookies que existían solo si cerrabas con educación, un servidor de devtools grabando una sesión que nadie miraba, y una página que cuenta para qué es su memoria. Y uno es el problema opuesto — algo retenido exactamente un frame de más.

La caché tenía un tier y el límite equivocado

La caché HTTP guardaba todas las respuestas en memoria, contadas por entradas y no por tamaño, y lo perdía todo al salir. Contar entradas significa que mil favicons y mil segmentos de vídeo cuestan lo mismo, que es justo lo que un límite por tamaño debería impedir.

Ahora tiene dos tiers. El tier de memoria está acotado por los bytes de los cuerpos que guarda, configurado con network_http_cache_memory_size. El tier de disco conserva respuestas completas entre reinicios bajo Opts::cache_dir, hasta network_http_cache_disk_size, y solo lo tiene la sesión pública — una sesión temporal con caché en disco no sería temporal.

Cada fichero se escribe con un nombre temporal y se renombra a su sitio. Un crash a media escritura deja por tanto el fichero anterior intacto en vez de uno truncado, lo que importa más de lo que suena para una caché: una entrada medio escrita no es una respuesta que falta, es una respuesta equivocada.

Cookies, escritas en marcha

El tarro de cookies público, la lista HSTS y la caché de autenticación se escribían en config_dir solo cuando el resource thread recibía Exit. Un embedder que petaba, o al que mataban, perdía todas las cookies de la sesión — y a un embedder en desarrollo lo matas constantemente.

Un hilo de fondo las escribe ahora cada 30 segundos cuando han cambiado, por la misma ruta de fichero temporal y rename. Como el fichero en disco puede ahora contener cookies de sesión, lo que se escribe ha tenido que decidirse en vez de darse por supuesto.

Un servidor de devtools sin público

El servidor de devtools guardaba todos los eventos de red, incluido el cuerpo de cada respuesta, y una copia del texto de cada fuente que veía, scripts inline incluidos, mientras estuviera corriendo. Sin ningún cliente conectado a quien mostrárselos, eso era todas las páginas cargadas, en memoria, durante la vida del proceso.

Sin cliente, los eventos de red se descartan ahora. Los actores de fuentes se siguen creando, así que una fuente aparece listada en el momento en que un cliente se conecta, pero no guardan texto hasta que hay alguien a quien mandárselo.

about:memory, en palabras

La página listaba las rutas crudas de los informes de memoria, que son los datos correctos y la presentación equivocada: js-main-runtime/compartments/system te dice de dónde sale un número, no si es un problema.

Ahora nombra cada parte en palabras llanas, dice si esa memoria se va y cuándo, y agrupa los informes por página. También puede volver a medir, pedir a todas las páginas que recojan basura, y abrir un conjunto de sitios pesados para cargar el navegador — las tres cosas que haces de verdad cuando persigues una fuga, en vez de leer un número una vez.

Una nota de build que es fácil pasar por alto: jemalloc tiene que estar construido con sus estadísticas, o stats.allocated y el resto no existen y el informe no tiene ninguna cifra del heap. El texto de la página está en español, como el resto del chrome.

SAME BUFFER ID, THREE FRAMES fence import no fence reuse the import fence import again the producer hands out a fence once per write, which is the only signal that the contents moved the old rule: keep the import while the buffer id is unchanged a write under a kept import shows the previous contents — a black page after a resize on NVIDIA The buffer id says which buffer it is. It never said whether anything was written to it.

El import que sobrevivió a una escritura

La otra dirección del mismo error. GPUI mantenía su import de un dmabuf mientras el id del buffer no cambiara, que suena bien: el id dice qué buffer es, y el buffer no se ha cambiado.

El id dice qué buffer es. Nunca dijo si se había escrito algo en él. Un import mantenido a través de una escritura del productor sigue presentando lo que el buffer tenía antes — que en NVIDIA aparecía como una página en negro después de un resize, el síntoma más confuso posible para un bug de caché, porque nada en él apunta a una caché.

El productor entrega una fence una vez por escritura, y esa es la única señal de que el contenido se ha movido. Un frame que recibe una fence vuelve a importar el buffer; los frames que no traen nada nuevo reutilizan el import.

Dos más, mientras contamos cosas mal retenidas

document.visibilityState era hidden para todos los documentos hasta que se reactivaban desde el historial de sesión, y después no cambiaba nunca. Una página que espera a ser visible antes de cargar o reproducir algo no lo hacía por tanto nunca. Un documento que carga en un webview que no está throttled empieza ahora visible; hacer throttle a un webview, que es como dice un embedder que ha dejado de mostrarlo, oculta el documento y dispara visibilitychange, y quitarlo lo revierte.

Y un receiver de crossbeam cerrado está siempre listo, así que en cuanto desaparecía un sender de un conjunto de receivers, cada select posterior volvía de inmediato con el mismo ChannelClosed y los receivers que seguían abiertos no se leían más. El receiver cerrado se reemplaza por uno que nunca está listo, lo que mantiene los ids de los demás sin cambiar — el detalle que hace que el arreglo sea un arreglo y no una renumeración.

Lo que sigue sin acotar

La caché de disco tiene un límite de tamaño y ninguna política de desalojo que merezca el nombre más allá de él. about:memory informa del heap solo cuando jemalloc se construyó para contarlo, lo que significa que las cifras faltan justo en los builds que usaría alguien.

Y la contención del servidor de devtools depende de que no haya ningún cliente conectado. Engánchale uno a una sesión larga y la acumulación vuelve, porque los datos que un cliente podría pedir siguen siendo todo lo que ha visto.

BEFORE: EVERY RESPONSE IN MEMORY, COUNTED BY ENTRIES, LOST AT EXIT memory tier bounded by the bytes of its bodies network_http_cache_memory_size disk tier complete responses under cache_dir network_http_cache_disk_size survives a restart · public session only temporary name, then rename a crash mid-write keeps the previous file The cookie jar, the HSTS list and the auth cache take the same route, written every 30 seconds when changed, not only at exit.
Acotada por bytes y no por un recuento de entradas, y escrita con un rename para que un crash a media escritura conserve el fichero anterior.