Ir al contenido
Código en GitHub (se abre en una pestaña nueva)

Documentación · Referencia

La compilación incremental

Cómo se rehace solo lo que un cambio afecta, y cuánta memoria pide.

Versión 0.16

ehundu compilar construye siempre el sitio entero. ehundu servir, en cambio, recuerda la última compilación y, cuando cambia algo, rehace solo las páginas a las que afecta ese cambio. El resultado tiene que ser exactamente el de una compilación completa: los mismos ficheros, con el mismo contenido, y los mismos avisos. Si no se puede asegurar, se rehace todo.

Con ehundu servir --completo la previsualización rehace todo en cada cambio, como antes; sirve para descartar la compilación incremental cuando algo no cuadra.

Qué se recuerda

Mientras construye una página, el motor apunta todo lo que usa: qué plantillas y parciales carga (también los que busca y no existen), qué ficheros de publico/ mira, incrusta o comprueba, qué colecciones recorre, de qué otras páginas muestra el contenido y de qué páginas mira las traducciones. Apunta también el CSS y el JS que declara y los avisos que da. El PDF de una página (pdf: sí, formato §10.3) se construye aparte, pero su registro se suma al de la página: se rehace todo junto cuando cambia algo de lo que usaron la página o su PDF. Del PDF se anota también lo que dompdf lee de publico/ por su cuenta (imágenes, tipografías, CSS que carga), y lo que el HTML pide aunque no exista todavía, para que un fichero que aparece rehaga el PDF. Lo mismo hace con el cuerpo de cada página, que se convierte una vez y se reutiliza donde se muestre. Lo que usa un cuerpo lo usa también la página que lo muestra, así que cada registro está completo por sí solo.

Las variantes de una imagen (formato §2 y §7.3) se apuntan como cualquier otro fichero de publico/: variantes(), el atajo imagen y las figuras del Markdown anotan cada fichero <tronco>-<ancho>.<formato> que buscan, exista o no, así que la página se rehace cuando aparece, desaparece o cambia uno. La sección imagenes es de sitio.yml, y su cambio rehace todo.

Además de esos registros, se recuerda lo que se leyó del proyecto y una huella de cada fichero de plantillas/, parciales/ y publico/: su tamaño, su fecha y, si pesa menos de un mega, un resumen de su contenido. La fecha sola no basta, porque PHP la da en segundos y dos cambios del mismo tamaño dentro del mismo segundo la dejarían igual. Para no leer cada vez todos los ficheros, el resumen se reutiliza mientras no cambien el tamaño ni la fecha, salvo si el fichero se ha tocado en los dos últimos segundos. El vigilante de la previsualización usa las mismas huellas.

Qué se rehace

En cada compilación se vuelve a leer el proyecto entero, que es rápido, y se compara con lo anterior. Si han cambiado sitio.yml o algo de datos/, que llegan a todas las plantillas, se rehace todo. También si la compilación anterior falló, si se activan o desactivan los borradores, o si no hay una compilación anterior.

Si no, se rehace una página cuando cambia su fichero, cuando aparece o pasa a publicarse, o cuando ha cambiado algo de lo que apuntó: una plantilla o un parcial, un fichero de publico/, una colección o el contenido de otra página. Una colección cambia cuando entra o sale una página con su etiqueta, o cuando una de sus páginas cambia de campos o de URL; un cambio solo en el cuerpo de una página no cambia la colección, pero sí rehace las páginas que muestran ese cuerpo. Las traducciones de una página cambian igual: cuando aparece, desaparece o se publica una de sus versiones, o cuando cambia de campos o de URL. Los datos de cada idioma (menus.eu.yml) son parte de datos/, y su cambio rehace todo; los _datos.eu.yml y los campos comunes de una página cambian los campos de las páginas que los heredan, como cualquier _datos.yml. Si aparece o desaparece un atajo del sitio, cambian todos los cuerpos en Markdown, porque cambia cómo se lee el texto. El sitemap y el feed se generan siempre de nuevo, y la lista de ficheros que se copian también.

Las páginas que genera una página con agrupar (formato §6.3) no tienen nada de especial para todo esto. Se deciden en cada compilación, justo después de leer el proyecto y antes de nada más, y lo que se recuerda es la lectura ya con ellas: la página que define los grupos no está y en su lugar están las generadas, cada una con su ruta (etiqueta.md#ideas) y las páginas de su grupo. Por eso una etiqueta nueva es una página nueva y la última página de una etiqueta, una página que desaparece, y quien lista las generadas a través de una colección se entera como con cualquier otra. Una página generada se rehace cuando cambia lo que la define (el fichero de la plantilla de grupos, que ellas copian) o su grupo (Pagina::$miembros: que entre, salga o cambie de sitio una página). Que cambien los campos de una de las páginas del grupo se anota aparte, página por página (Registro::$campos): la generada se rehace si cambia el título de uno de sus artículos, pero no si solo cambia su cuerpo. El grupo cuenta también como un campo de la página generada para las colecciones: si gana o pierde una página, se rehace quien lee el recuento a través de coleccion().

Las listas que parte paginar (formato §6.4) funcionan igual. La serie se decide en cada compilación, justo después de las de agrupar: la primera página es la propia página, con sus rutas de trozo, y las demás son páginas generadas (blog/index.md#2), sin traducciones y sin colecciones. El trozo de cada una (Pagina::$miembros) y su paginacion (que es un campo más) cuentan como cambios de la página: entrar, salir o cambiar de sitio un artículo rehace las páginas de la serie a las que toca, y si cambia cuántas hay, o sus direcciones, las de toda la serie. Los campos de cada artículo se anotan uno a uno, como con agrupar, así que un título cambiado rehace solo la página en la que sale, y un cuerpo cambiado, ninguna. Una página de la serie que ya no existe deja de escribirse.

Lo que no se rehace se aprovecha tal cual, y sus avisos se repiten, para que la terminal diga lo mismo que diría una compilación completa. Los avisos de agrupar (un valor en blanco, una url sin grupo) no son de una página sino de la lectura, y salen siempre.

Las comparaciones de rutas no distinguen mayúsculas ni cuentan las barras repetidas, porque Windows tampoco lo hace. Lo que una plantilla pida fuera de publico/ con .. no tiene huella, y la página que lo pide se rehace siempre.

Lo que queda fuera

Tres cosas no se vigilan. La primera, la hora: una plantilla que escriba la fecha actual con "now"|date no se rehace al cambiar de día. La segunda, lo que elige random(): una página que no se rehace conserva lo que eligió. Las páginas con publicar sí entran solas cuando llega su fecha, porque eso se comprueba en cada compilación. La tercera, un fichero de publico/ que cambie sin cambiar ni de tamaño ni de fecha después de llevar un rato sin tocarse; solo pasa si un programa restaura a propósito la fecha antigua.

Cómo se comprueba

Las pruebas hacen sobre un sitio pequeño, preparado para que las páginas dependan unas de otras de todas las maneras posibles, una lista de cambios de cada tipo, y comprueban cuántas páginas se rehacen con cada uno. Después hacen una secuencia de cambios al azar, con una semilla fija. Tras cada cambio comparan la compilación incremental con una completa, fichero a fichero, aviso a aviso, y también cuando la compilación falla.

Para quien incruste el motor

Lo que se recuerda entre compilaciones son datos simples (Memoria), sin recursos ni conexiones, así que se pueden serializar y guardar fuera del proceso. La previsualización no guarda nada en disco: al arrancar, hace una compilación completa. Quien incruste el motor sí puede guardarlo: CompiladorIncrementalEnProceso (biblioteca.md, formato §10.5) lo escribe en .ehundu/memoria.bin después de cada compilación en la que se rehace alguna página, y la siguiente, aunque sea de otro proceso, lo aprovecha. En el sitio más grande de los de prueba (1.204 páginas), cambiar el cuerpo de una página rehace una y tarda unos 3 s en total (leer el proyecto, cargar la memoria, comparar salida/ y guardarla), frente a los 35–45 s de una compilación completa. La memoria pesa del orden de salida/ más lo leído (97 MB en ese sitio) y se carga en unos 0,06 s. Son medidas de un sitio y de una máquina, no una promesa del formato.

La memoria de PHP. Se guarda y se lee registro a registro (decisión 141), sin construir ni leer el fichero entero de una vez. En el mismo sitio, el pico de memoria de PHP (la reservada, la que cuenta el memory_limit) es de 170 MB en la primera compilación, 190 al cambiar el cuerpo de una página y 252 al cambiar una plantilla, que rehace todas; con la memoria guardada en una sola cadena eran 332, 352 y 416. En otros dos sitios de 27 y 29 MB de memoria guardada, de 770 y 295 páginas, el peor caso es de 80 y 82 MB. Lo que cuenta es el peso del HTML, no el número de páginas, y la regla que sale es unas 2,5 veces lo que mide .ehundu/memoria.bin más unos 10 MB en el peor caso: lo medido cabe con 128 MB hasta una memoria de unos 45 MB, con 256 MB hasta unos 95, y con 512 MB hasta unos 200. El motor es más conservador, porque no sabe qué se va a rehacer: antes de cargarla pide tres veces su tamaño más 16 MB, y descuenta lo que ya ocupa el proceso, así que acepta memorias de hasta unos 37 MB con 128 MB, 80 con 256 y 170 con 512. Si no cabe en el memory_limit, compilar compila entero y avisa, y avanzar falla con el límite que hace falta. La primera compilación, sin fichero que medir, no se comprueba. Una compilación completa sin memoria (CompiladorEnProceso) pide menos: unos 136 MB en ese sitio.

Cuándo no vale lo guardado. Si el fichero falta, está cortado o dañado, lo escribió otra versión de la memoria o del motor (se compara una huella del contenido de su código, de las versiones de sus dependencias y de la versión menor de PHP), o es de otro proyecto (otra ruta), se ignora y se compila entero: nunca es un error. Solo se guarda cuando una compilación sale bien, y si no se ha rehecho ninguna página no se vuelve a escribir. Las huellas de plantillas/, parciales/ y publico/ que tomó el proceso anterior también se aprovechan: no se vuelve a leer el contenido de los ficheros que ya llevaban un rato sin tocarse.

Repartida en llamadas. avanzar corta la compilación cuando se agota el presupuesto y guarda lo hecho. No hace falta ningún estado aparte: una memoria con la mitad de las páginas es una memoria a la que le faltan páginas, y el plan rehace las que faltan y aprovecha las que valen. Por eso, si el proyecto cambia entre dos llamadas, no se empieza de nuevo (a diferencia de los lotes de más abajo): solo se rehace lo que el cambio afecta.

Compilar decide primero qué se aprovecha (Plan) y después rehace el resto.

Compilar por lotes (interno)

Un proceso web puede no tener tiempo de compilar un sitio grande de una vez. CompiladorPorLotes (formato §10.4) reparte la compilación completa en tareas (cada página, cada fichero que se copia, el sitemap con el feed, la limpieza final) y las hace en varias llamadas, cada una con un presupuesto de tiempo. No es la compilación incremental: no recuerda qué se rehizo la vez anterior, y cada lote lo rehace todo. Para un sitio que se compila una y otra vez, CompiladorIncrementalEnProceso (más arriba) hace lo mismo con memoria; los lotes quedan para quien quiere una compilación completa repartida sin guardar nada del sitio.

No se promete en la 1.0 (decisión 128): CompiladorPorLotes y CompiladorPorLotesEnProceso son @internal, y para repartir una compilación en llamadas se usa CompiladorIncrementalEnProceso::avanzar. Lo que sigue cuenta cómo funcionan hoy, y puede cambiar.

Cada llamada vuelve a leer el proyecto entero (con agrupar y paginar expandidos) y construye de nuevo el maquetador; lo único que viaja de una a otra es .ehundu/lote.json, que cuenta por dónde va el lote y no lleva nada del sitio. Leer cuesta poco frente a hacer las páginas: en el sitio más grande de los de prueba, 1.200 páginas, leerlo y expandirlo son menos de 1 s y hacerlas, de 40 a 50. Lo mismo vale para los avisos: se juntan de una llamada a la siguiente sin repetirse, como en una compilación completa, y el informe final los da en el mismo orden.

Si algún fichero de sitio.yml, contenido/, datos/, plantillas/, parciales/ o publico/ cambia de tamaño o de fecha entre dos llamadas, el lote empieza de nuevo (la huella no lee el contenido, porque en publico/ puede haber gigas). El momento con el que se decide qué está publicado se fija al empezar y no cambia en todo el lote: una página que se publica por fecha a mitad de un lote no cambia las tareas que quedan.

Lo que no se puede garantizar es la coherencia de salida/ mientras dura: cada página se escribe al hacerla. Por eso un lote dice si está completo y la subida no debería empezar hasta entonces.

Ver este documento en GitHub (se abre en una pestaña nueva)