Skip to content

La evolución se puede repartir, y deja de retener 43 GB mientras corre - #999

Merged
github-actions[bot] merged 1 commit into
developfrom
fix/evolucion-reparto-y-temporales
Oct 9, 2026
Merged

github-actions[bot] merged 1 commit into
developfrom
fix/evolucion-reparto-y-temporales

Conversation

@frapercan

Copy link
Copy Markdown
Owner

Por qué

Pregunta de Francisco mientras analyze_annotation_evolution corría sobre las 74
releases de la campaña limpia: "Esto no podría hacerse utilizando más CPU?". La
respuesta medida es que no, no dentro de una corrida, y el motivo tiene
consecuencias de diseño.

Postgres no paraleliza nada de esto, y no por configuración. En el fuente de
la 18.6 exacta, set_rel_consider_parallel (allpaths.c:623) vuelve en cuanto ve
RELPERSISTENCE_TEMP:

if (get_rel_persistence(rte->relid) == RELPERSISTENCE_TEMP)
    return;          /* deja consider_parallel en false */

Los caminos parciales no se descartan por coste: no se generan.
plan_create_index_workers (planner.c:7272) se niega igual para los dos
CREATE INDEX. Y la documentación lista "Scans of temporary tables" como siempre
parallel restricted. El control pareado lo cierra: la misma consulta de
_estratos, palabra por palabra, saca 5 subplanes paralelos sobre tablas
permanentes y cero sobre temporales, con la persistencia como única variable.

Medido sobre dos ciclos completos de release: el backend ocupa 96% de UN
núcleo
con 3,9% de muestras fuera de CPU, y una ráfaga de 12.000 muestras a 2 ms
da R=99,98%. Mientras tanto, 12 de los 16 núcleos ociosos. La vía para gastar
más de un núcleo es correr varios rangos a la vez.

Y repartir por rangos no era exacto

composicion es una fila por release; estratos y detalle son una fila por
transición. Un rango no puede servir para las dos cosas, y la línea 361 emitía
composicion incondicionalmente, antes del guardia if ant is not None::

  • con solape, cada frontera describe dos veces una release: +3·(k−1) filas.
    Con 4 shards, 9 filas de más.
  • sin solape, se pierden k−1 transiciones completas, y resultan ser
    precisamente (192,194), la que cruza la 193 borrada, y (205,211), la que
    cruza el hueco 206-210.

Es la clase de defecto que el proyecto ya tiene anotada para la 192 y la 193: una
serie contando un intervalo dos veces.

seed_from_previous separa las dos cosas. La operación resuelve el predecesor
ella misma, sobre las releases que existen, que es el punto: antes de la 194
está la 192 y antes de la 211 está la 205. Quien llamara restando uno se
equivocaría en los dos sitios y en silencio.

Dos cosas más que aparecieron midiendo

Una transacción para toda la serie retiene todo. Los ficheros de una temporal
borrada no se desenlazan hasta el COMMIT: 577 MB por release medidos, 43 GB
proyectados sobre las 74, y un xmin clavado que detiene el vacuum de la base
entera durante horas. Ahora se comitea por release; las temporales sobreviven
porque son de sesión y preservan filas.

Eso regala la garantía de leer la serie en un solo snapshot, así que la garantía
deja de ser gratis y pasa a comprobarse. _comprobar_serie compara la serie como
se leyó contra la serie al terminar, y si se movió falla en lugar de publicar
dos artefactos que describen corpus distintos.

temp_buffers era el número más desatendido del lote. Las temporales se leen
por esos buffers y nunca por shared_buffers, y estaban en los 8 MB de fábrica
contra ~1 GB de conjunto vivo por ciclo. De ahí que el 37% de las muestras marque
DataFileRead mientras el proceso está al 99,5% de un núcleo: son pread servidos
por la caché de páginas, cobrados como stime. Ahora es un campo del payload,
None deja el servidor como está, y se fija antes de la primera temporal porque
después el servidor ignora el cambio sin decir nada.

Qué cubren los tests

Los existentes leen el SQL que un método construye. Los nuevos preguntan lo
contrario: qué sentencias decide emitir el bucle, y para qué releases. Ahí
estaba el defecto, y no se ve desde ningún método suelto. _SesionDeSerie
responde por la forma de la consulta y _AlmacenQueLee guarda lo escrito, así que
el test lee el TSV de verdad.

El que importa es test_the_shards_concatenate_into_the_whole_series: cuatro
shards deben escribir exactamente las filas que escribe una corrida. Revirtiendo
el guardia a mano, falla con Left contains 9 more items, que es el +3·(k−1)
predicho.

Los dos bordes están fijados por separado, porque son los dos sitios donde la
aritmética sobre el número falla: la semilla de la 194 es la 192 y la de la 211 es
la 205.

Lo que esto NO hace

No cambia ningún número de una corrida sin seed_from_previous: los artefactos de
un rango completo son byte a byte los mismos. No convierte las temporales en
UNLOGGED, que es la otra vía para habilitar paralelismo y que cuesta la
propiedad de que una ejecución interrumpida no deja residuo. Y no reparte nada por
su cuenta: deja el reparto expresable y exacto, y quién lo encola es otra
decisión.

La corrida en curso, 74 releases con la operación anterior, no se tocó para esto.

Tres cosas que salieron de medir por que esta operacion usa un solo nucleo
mientras hay doce ociosos. La respuesta es que no puede usar mas: Postgres no
planifica un camino paralelo sobre una relacion temporal. En el fuente de la
18.6, `set_rel_consider_parallel` vuelve en cuanto ve `RELPERSISTENCE_TEMP`, asi
que los caminos parciales no se descartan por coste, no se generan; y
`plan_create_index_workers` se niega igual para los dos indices. Medido sobre dos
ciclos completos: el backend ocupa el 96% de UN nucleo con el 3,9% de sus
muestras fuera de CPU. Ningun valor de `max_parallel_workers_per_gather` cambia
eso. La via para gastar mas de un nucleo es correr varios rangos, no acelerar uno.

1. EL REPARTO NECESITA SU SEMILLA, Y LA SEMILLA NO SE DESCRIBE. `composicion` es
   una fila por release y `estratos`/`detalle` son una fila por transicion, asi
   que un rango no puede servir para las dos cosas: o describe su primera release
   y pierde la transicion que entra en ella, o la conserva empezando una release
   antes y entonces describe dos veces una release de la serie. `seed_from_previous`
   es la tercera opcion, y la operacion resuelve el predecesor ella misma sobre
   las releases que EXISTEN: antes de la 194 esta la 192, porque la 193 es el
   mismo fichero, y antes de la 211 esta la 205, porque la 206 a la 210 no se
   publicaron. Quien llamara restando uno se equivocaria en los dos sitios, y en
   silencio. Es la clase de defecto que ya tenemos anotada para la 192 y la 193:
   una serie contando un intervalo dos veces.

2. UNA TRANSACCION PARA TODA LA SERIE RETIENE TODO. Los ficheros de una temporal
   borrada no se desenlazan hasta el COMMIT: 577 MB por release medidos, 43 GB
   proyectados sobre las 74, y un xmin clavado que detiene el vacuum de la base
   entera durante las horas que dure la corrida. Se comitea por release. Las
   temporales sobreviven porque son de sesion y por defecto preservan filas.
   Eso regala la garantia de leer toda la serie en un solo snapshot, asi que la
   garantia deja de ser gratis y pasa a comprobarse: `_comprobar_serie` compara
   la serie como se leyo contra la serie como esta al terminar, y si se movio
   falla en vez de publicar dos artefactos que describen corpus distintos.

3. `temp_buffers` ERA EL NUMERO MAS DESATENDIDO DEL LOTE. Las temporales se leen
   por esos buffers y nunca por `shared_buffers`, y estaban en los 8 MB de
   fabrica contra un conjunto vivo de ~1 GB de relaciones temporales por ciclo.
   Ahora es un campo del payload, con `None` dejando el servidor como esta, y se
   fija antes de la primera temporal de la sesion porque despues el servidor
   ignora el cambio sin decir nada.

El bucle sale de `execute` a `_recorrer` y los tres TSV a `_artefactos`, que es
lo que el presupuesto de complejidad pedia; y la semilla y el rango viajan juntos
en `_Plan` en lugar de como dos locales que cada llamante tenia que recordar
emparejar.
@github-actions
github-actions Bot enabled auto-merge (squash) October 9, 2026 20:55
@codecov

codecov Bot commented Oct 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.59155% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 88.01%. Comparing base (d8bc801) to head (3715961).
⚠️ Report is 98 commits behind head on develop.

Files with missing lines Patch % Lines
...ea/core/operations/analyze_annotation_evolution.py 98.59% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #999      +/-   ##
===========================================
+ Coverage    87.85%   88.01%   +0.16%     
===========================================
  Files          306      330      +24     
  Lines        23414    25454    +2040     
===========================================
+ Hits         20570    22403    +1833     
- Misses        2844     3051     +207     
Files with missing lines Coverage Δ
...ea/core/operations/analyze_annotation_evolution.py 98.79% <98.59%> (ø)

... and 37 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
github-actions Bot merged commit f7cf5ad into develop Oct 9, 2026
21 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant