El flujo de actualización de JMathAnim (modelo pull)
Este documento describe el funcionamiento interno del sistema de versiones y actualización tras la migración de push (listeners de versión) a pull (versiones efectivas). Es documentación de desarrollo, no del manual de usuario.
Idea central
Cada objeto que puede cambiar (Vec, Point, JMPath, MathObject,
MODrawProperties, Camera...) lleva un número de versión. Todas las
versiones salen de un único contador global:
// AbstractVersioned
public void changeVersion() {
version = ++JMathAnimScene.globalVersion;
}
Dos propiedades se derivan de esto:
- Toda mutación produce una versión estrictamente mayor que cualquier versión anterior en cualquier objeto. Comparar versiones equivale a comparar "quién cambió más recientemente".
- Si
globalVersionno ha cambiado, nada ha cambiado en ninguna parte. Esta es la base de la caché de versiones efectivas y del testSteadyStateUpdateTest(una escena estática debe mantenerglobalVersionconstante entre frames).
Versión efectiva: el mecanismo pull
Un objeto declara sus entradas con addDependency(...). La detección de
cambios ya no se hace notificando a nadie: cada objeto pregunta por el
estado de sus dependencias cuando lo necesita.
AbstractVersioned.getVersion() no devuelve la versión propia en bruto, sino
la versión efectiva:
versionEfectiva(obj) = max( obj.version,
versionEfectiva(dep) para cada dep de obj )
Es decir, el máximo recursivo sobre todo el árbol de dependencias. Con esto, la comprobación de si un objeto está sucio es una sola comparación:
// AbstractUpdateable
public boolean needsUpdate() {
return getVersion() != lastProcessedVersion;
}
lastProcessedVersion es la versión efectiva que el objeto vio la última vez
que se actualizó. Si cualquier cosa en su árbol de dependencias (directa o
transitiva) ha cambiado desde entonces, el máximo habrá subido y la
comparación falla.
Dos protecciones dentro de getVersion():
- Caché por
globalVersion: el resultado se memoriza junto con el valor deglobalVersionen el momento del cálculo. MientrasglobalVersionno cambie, la caché es válida (nada pudo cambiar). En una escena en reposo el coste por objeto y frame es O(1); la recursión completa solo se paga cuando algo realmente mutó. - Guarda anti-ciclos: si durante la recursión se vuelve a entrar en un
objeto que ya está calculando (ciclo accidental de dependencias), la
reentrada devuelve la versión propia en bruto y corta la recursión. Un ciclo
degrada la precisión del orden de actualización, pero no cuelga el programa
(coherente con el aviso de ciclo del
DependencyGraph).
Además, addDependency/removeDependency llaman a changeVersion() sobre el
objeto dueño: un cambio estructural de entradas lo marca como sucio y, de
paso, invalida todas las cachés (porque sube globalVersion).
El ciclo de un frame
Por cada frame, la escena hace DependencyGraph.updateAll():
- Si la estructura de dependencias pudo cambiar (se detecta con el contador
estático
dependencyStructureVersion), se reconstruyen las aristas desdegetDependencies()y se reordena topológicamente. - Se recorre el orden topológico (dependencias antes que dependientes) y se
llama a
update()en cada nodoUpdateable.
AbstractUpdateable.update() hace, en orden:
1. applyUpdaters(true) // Updaters "before"
2. si needsUpdate(): // comparación pull
performMathObjectUpdateActions() // recálculo real del objeto
3. applyUpdaters(false) // Updaters "after"
4. si algo cambió (flag):
changeVersion() // el objeto publica su nueva versión
performUpdateBoundingBox()
5. postUpdateActions() // p.ej. objetos internos (labels, tips)
6. lastProcessedVersion = getVersion() // "hasta aquí he visto"
El paso 5 corre después del bump de versión a propósito: los objetos
internos (registrados con addInternalObject) dependen de su padre, y así ven
la versión definitiva del padre en el mismo frame. Con el orden antiguo la
veían un frame tarde y generaban un frame de eco.
El renderer usa el mismo mecanismo: cachea la última versión dibujada de cada
objeto y compara con getVersion(). Como ahora getVersion() es la versión
efectiva, un cambio en un Vec interno de un path invalida el dibujo del
Shape sin que nadie tenga que avisarle.
Ejemplo: un Arrow cuyo extremo es un Vec que se mueve
Vec A = Vec.to(0, 0);
Vec B = Vec.to(2, 0);
Arrow arrow = Arrow.make(A, B, ArrowType.ARROW1);
scene.add(arrow);
// ... más tarde ...
A.shift(0.5, 1); // movemos el extremo inicial
Al construirse, Arrow (vía AbstractConnector) declara sus entradas:
protected AbstractConnector(Coordinates<?> A, Coordinates<?> B, ...) {
this.A = A;
this.B = B;
addDependency(A);
addDependency(B);
...
}
El grafo queda así (las flechas significan "depende de"):
arrow ──► A (Vec)
└──► B (Vec)
└──► mp (estilo)
Supongamos que tras estabilizarse la escena globalVersion = 100, con
A.version = 40, B.version = 41, arrow.version = 100 y
arrow.lastProcessedVersion = 100 (su versión efectiva:
max(100, 40, 41) = 100).
Frame N — se mueve el Vec. A.shift(0.5, 1) recalcula las coordenadas y
termina en A.changeVersion():
globalVersion: 100 → 101
A.version: 40 → 101
Nadie notifica a arrow. A no sabe quién depende de él, y no le hace falta.
Frame N — actualización. El DependencyGraph recorre el orden topológico.
A y B son nodos hoja sin nada que recalcular; le toca a arrow:
arrow.needsUpdate()
= getVersion() != lastProcessedVersion
= max(arrow.version=100, A=101, B=41, mp=…) != 100
= 101 != 100 → true
(La caché de la versión efectiva estaba invalidada porque globalVersion
cambió al mover A.)
Como está sucio, arrow ejecuta performMathObjectUpdateActions(), que llama
a rebuildShape(): reconstruye cuerpo y cabeza de la flecha con las nuevas
coordenadas de A. Después:
arrow.changeVersion() → globalVersion: 101 → 102, arrow.version = 102
performUpdateBoundingBox()
postUpdateActions() → actualiza labels/tips internos, que ya ven la 102
lastProcessedVersion = getVersion() = max(102, 101, 41, …) = 102
Frame N — render. El renderer compara arrow.getVersion() = 102 con la
última versión que dibujó (100) y redibuja la flecha.
Frame N+1 — reposo. Nadie muta nada, así que globalVersion sigue en 102:
arrow.getVersion()es un acierto de caché (O(1)): devuelve 102.needsUpdate()→102 != 102→false. No hay recálculo ni bump.- El renderer ve la misma versión y no redibuja.
La escena queda estable en un solo frame: sin frames de eco y sin trabajo por frame en reposo.
Comparación con el modelo push anterior
| Push (listeners) | Pull (versiones efectivas) | |
|---|---|---|
| Propagación | changeVersion() recorría la lista de listeners y les subía la versión en cascada |
Nadie propaga nada; el dependiente compara getVersion() cuando le toca |
| Estado por objeto | Lista de listeners + flag notifying |
Caché de versión efectiva (2 longs + 1 boolean) |
| Registro | addDependency tenía que registrar el listener simétrico; olvidarlo rompía la propagación en silencio |
addDependency es la única fuente de verdad |
| Frames de eco | El bump del padre re-ensuciaba a quien ya se había actualizado | Imposible por construcción: cada uno anota lo último que vio |
| Ciclos | Flag notifying contra recursión infinita en la notificación |
Guarda de reentrada en getVersion() |
| Listener que lanza excepción | Podía dejar la propagación rota | No existe el caso |
Convenciones a mantener
- Toda mutación de estado observable debe terminar en
changeVersion(). Los setters deMathObject,Vec.applyAffineTransform, etc. ya lo hacen. - Vecs expuestos que se recalculan durante
update()deben subir su versión (idealmente con comprobación de igualdad para no ensuciar en vano). Cachés puramente internas pueden escribir campos en bruto, pero entonces el dueño debe hacerchangeVersion()al final (test guardián:InnerVecPropagationTest). - Las consultas de solo lectura no deben mutar versiones (test guardián:
SteadyStateUpdateTest). - Declarar entradas con
addDependency. Si un objeto lee otro sin declararlo, ningún mecanismo (ni el antiguo ni este) detectará el cambio.
Clases implicadas
| Clase | Papel |
|---|---|
Versionable |
Contrato mínimo: changeVersion() y getVersion() (efectiva) |
Dependable |
Añade getDependencies() / addDependency() |
AbstractVersioned |
Implementa versión efectiva con caché y guarda anti-ciclos |
AbstractUpdateable |
Ciclo needsUpdate()/update() y hook postUpdateActions() |
DependencyGraph |
Orden topológico y updateAll() por frame |
Updater |
Lógica adicional por objeto; sus targets se registran como dependencias en registerUpdater |