El cuadro comparativo: qué eligió cada biblioteca y por qué
Comparación de WOOT, Logoot, LSEQ, RGA y Treedoc por tamaño del identificador, coste de inserción, coste de recorrido y metadatos residuales, y por qué todas las bibliotecas modernas acabaron en la misma rama del árbol.
Puestos los cuatro algoritmos uno junto a otro, la tentación es buscar un ganador y anotarlo. La tabla que sigue lo da, y de hecho la industria lo dio hace años con una unanimidad poco frecuente, pero el valor de este ejercicio no está en el veredicto sino en entender qué columna lo produjo. Los tres primeros ejes —tamaño del nombre, coste de insertar, coste de recorrer— son los que aparecen en todos los artículos y los que se miden en los bancos de pruebas, y son también los que menos decidieron: en los tres hay empates, compensaciones y regímenes donde cada familia gana. El cuarto eje, los metadatos que quedan cuando el usuario ya ha terminado, es el que no admite compensación, porque es el único cuyo coste no se paga una vez sino todos los días durante toda la vida del documento. Esta lección monta el cuadro completo, lo lee columna a columna y después mira lo que efectivamente escogieron Yjs, Automerge y sus contemporáneas, que es la única evidencia empírica disponible sobre qué importa de verdad.
- Comparar las cuatro familias en los cuatro ejes con las salvedades de cada medida.
- Distinguir el coste asintótico del coste que decide, que casi nunca es el mismo.
- Reconocer qué familia hay detrás de cada biblioteca moderna y qué añadió encima.
- Elegir con criterio entre secuencia convergente e índice fraccionario según el caso.
Los cuatro ejes y sus trampas
El tamaño del identificador admite una lectura engañosa. WOOT y RGA lo tienen de tamaño fijo —una réplica y un contador—, y Logoot, LSEQ y Treedoc lo tienen variable, pero la comparación honesta no es por elemento sino por documento, y ahí entra el agrupamiento en tramos: si una tanda de cien caracteres seguidos comparte un solo identificador con una longitud, el coste efectivo por carácter se desploma. Ese truco funciona en las dos familias, pero funciona mucho mejor donde los identificadores de una tanda son consecutivos y predecibles.
El coste de inserción hay que descomponerlo en dos partes que suelen mezclarse. Una es localizar el punto de anclaje a partir de lo que la operación cita, y es donde las familias se diferencian de verdad. La otra es traducir la posición visible que da la interfaz a un elemento concreto de la estructura, y esa parte es la misma para todo el mundo: exige un índice local con tamaños de subárbol y cuesta del orden del logaritmo del documento, gane quien gane el resto de la comparación.
Conviene además separar el coste de una inserción local, la que produce el usuario al teclear, del coste de una inserción remota, la que llega por la red. La local es siempre barata porque ya sabes dónde estás: tienes el cursor, tienes el nodo y no hay nada que buscar. La remota es la que hay que medir, y es la que sufre cuando el documento es grande, cuando hay muchas operaciones concurrentes acumuladas o cuando una réplica vuelve tras días desconectada y descarga miles de operaciones de golpe. Casi todas las comparaciones publicadas miden lo segundo, y casi todas las quejas de los usuarios vienen de lo segundo, pero muchas evaluaciones caseras miden lo primero y concluyen que todo va bien.
El coste de recorrido parece trivial y esconde el efecto más importante del cuadro. Recorrer es lineal en todas las familias, pero lineal en qué: en Logoot es lineal en los caracteres vivos, mientras que en WOOT y RGA es lineal en los caracteres que han existido alguna vez. En un documento con una historia larga de ediciones, esa diferencia puede ser de un orden de magnitud, y es la principal razón por la que las implementaciones de la familia RGA acaban manteniendo un índice que salta los tramos borrados de una vez.
Los metadatos residuales son lo que queda cuando nadie está escribiendo, y la pregunta útil no es cuánto ocupan sino si se pueden reducir sin coordinación. Las lápidas de RGA son un conjunto de rangos de sellos consecutivos y se comprimen a casi nada; los identificadores largos de Logoot son información incompresible; y el desequilibrio de Treedoc no es que ocupe mucho, es que solo se repara con un acuerdo global.
// La medida que ordena el campo, y que casi nadie hace: metadatos antes y despues de comprimir
async function perfil(estructura, texto) {
const crudo = serializar(estructura);
const comprimido = await comprimir(crudo); // compresion de proposito general
return {
porCaracterCrudo: crudo.byteLength / texto.length,
porCaracterComprimido: comprimido.byteLength / texto.length,
factor: crudo.byteLength / comprimido.byteLength // aqui esta el veredicto
};
}
El campo factor es el número que resume estas cinco lecciones. Una estructura cuyos metadatos son redundantes tiene un factor alto, y eso significa que su coste nominal es una ilusión contable: desaparece al escribir en disco y al mandar por la red, que son las dos operaciones que de verdad ocurren. Una estructura cuyos metadatos son entropía tiene un factor cercano a uno, y entonces el coste que mediste en memoria es el coste que vas a pagar en todas partes y para siempre. Dos implementaciones pueden empatar en la primera línea del resultado y diferir en un orden de magnitud en la segunda, y es la segunda la que decide si tu documento de un millón de caracteres cabe en el presupuesto.
El cuadro
| Eje | WOOT, 2006 | Logoot y LSEQ, 2009 y 2013 | RGA, 2011 | Treedoc, 2009 |
|---|---|---|---|---|
| Tamaño del identificador | Fijo, más dos referencias a vecinos por carácter | Variable: crece con las inserciones en un mismo punto | Fijo: contador lógico y réplica | Variable: la profundidad del camino en el árbol |
| Coste de localizar el anclaje | Recursivo sobre la ventana; polinómico en el original, muy mejorado con tabla | Búsqueda binaria por el orden del identificador, multiplicada por su longitud | Búsqueda directa del predecesor y avance sobre los hermanos concurrentes | Descenso por el árbol, del orden de la profundidad actual |
| Coste de recorrido | Lineal en el histórico, lápidas incluidas | Lineal en los elementos vivos | Lineal en el histórico, con saltos por tramos | Recorrido simétrico del árbol, esqueleto incluido |
| Metadatos residuales | Lápidas permanentes y vecinos por carácter | Ninguno por borrado; el peso está en el nombre | Lápidas comprimibles en rangos | Nodos internos que sostienen caminos vivos |
| Requiere coordinación | No | No | No | Sí, para reequilibrar |
| Se comprime bien | Mal: dos referencias por carácter | Mal: los caminos son entropía | Muy bien: sellos consecutivos y rangos | Regular, y no arregla el equilibrio |
Hay una fila que no está en el cuadro porque no admite una casilla y sin embargo condiciona todas las demás: quién paga el coste y cuándo. En las familias de identificador fijo, el precio se paga en espacio, se acumula de forma predecible y se puede diferir con compresión y poda, es decir, se parece a una deuda con intereses bajos. En las familias posicionales, el precio se paga en el tamaño de cada operación que se emite, de modo que llega antes, se paga en la red y no se puede aplazar. Y en Treedoc hay además un pago extraordinario e impredecible, el reequilibrado, que exige que todo el sistema esté disponible a la vez. Tres perfiles de coste muy distintos que el análisis asintótico presenta como si fueran comparables.
Léelo por filas y verás compensaciones; léelo por la última fila y verás el veredicto. Las tres primeras familias empatan en lo asintótico dentro de un factor pequeño, y las diferencias entre ellas se mueven según el patrón de escritura que uses para medir. Pero solo una tiene un formato que se deja comprimir en la dirección en que la escritura humana genera redundancia, y esa es la que ganó.
La literatura comparada de esta familia mejoró mucho cuando se dejó de medir con inserciones aleatorias y se empezó a usar trazas de edición reales, como la que registró la escritura completa de un artículo carácter a carácter y que se convirtió en referencia informal del campo. La diferencia entre ambos regímenes no es de matiz: cambia el orden del ranking. Si estás evaluando una implementación propia, consigue una traza de tu dominio antes de creerte cualquier número, porque las secuencias uniformemente aleatorias favorecen sistemáticamente a las familias posicionales y castigan a las que dependen de tandas contiguas.
Qué eligió cada biblioteca
flowchart TD W[WOOT en 2006 identidad por caracter] --> L[Logoot en 2009 camino de digitos] W --> T[Treedoc en 2009 camino de bits] W --> R[RGA en 2011 enganche al predecesor] L --> LS[LSEQ en 2013 base creciente] R --> Y[YATA hacia 2016 base de Yjs] R --> A[Automerge listas y texto] R --> F[Fugue hacia 2023 sin entrelazado] F --> C[bibliotecas recientes de listas] style R fill:#a6e3a1,color:#11111b style T fill:#f38ba8,color:#11111b style LS fill:#f9e2af,color:#11111b
Antes de repasar las bibliotecas conviene fijar una advertencia sobre el diagrama que sigue. Las flechas indican influencia conceptual, no derivación directa de código, y las fechas son las de la primera publicación de la idea, no las de su adopción: entre que un algoritmo aparece en un artículo y que alguien lo lleva a una biblioteca que aguanta un documento grande suelen pasar varios años, y en esos años casi todo el trabajo es de representación. Léelo como un mapa de qué respuesta dio cada grupo a la pregunta de cómo nombrar una posición, y observa que después de 2011 todas las ramas vivas salen del mismo tronco.
Yjs implementa YATA, la variante presentada hacia 2016 que engancha cada elemento por ambos lados y resuelve los empates con una regla propia; operativamente es la misma familia que RGA. Su aportación decisiva no está en el algoritmo sino en la representación: los caracteres escritos seguidos por una réplica se guardan como un único bloque con longitud, los borrados se registran como conjuntos de rangos y la estructura admite recolección de lo que ya nadie puede citar. Es el ejemplo más claro de que la victoria se jugó en el formato.
Automerge partió de RGA para sus listas y su texto, y dedicó su reescritura posterior a un formato binario por columnas donde cada campo de los sellos se codifica por separado y se comprime con la técnica que le conviene. Es exactamente la maniobra que la lección 3 describía como imposible en las familias posicionales: cambiar la representación de arriba abajo sin tocar el modelo replicado ni la corrección.
Las propuestas más recientes se mueven dentro de la misma rama. Fugue, hacia 2023, es una variante con garantías más fuertes contra el entrelazado de tramos concurrentes, y sobre ella se apoyan varias bibliotecas de listas de los últimos años. Otra línea, la de los recorridos sobre el grafo de eventos, guarda la historia de operaciones y reconstruye el estado al vuelo en lugar de mantener metadatos persistentes por elemento, lo que ataca el cuarto eje desde un ángulo distinto sin abandonar el modelo del enganche al predecesor.
Merece la pena señalar qué añadieron todas ellas por encima del algoritmo, porque es donde está el trabajo de la última década y no aparece en ningún artículo fundacional. Las cuatro piezas se repiten con nombres distintos: el agrupamiento en tramos para que una tanda de escritura sea un objeto y no cien; el formato binario compacto, casi siempre orientado a columnas, que codifica por separado cada componente del sello y aprovecha que unos avanzan de uno en uno y otros se repiten; el índice local con tamaños de subárbol para traducir posiciones en tiempo logarítmico sin tocar el modelo replicado; y algún mecanismo de poda de lo que ya nadie puede citar, que es la única forma de que las lápidas no crezcan indefinidamente. Ninguna de las cuatro es parte del CRDT; las cuatro son imprescindibles para que el CRDT sea usable.
Del otro lado, la evidencia es igual de nítida por ausencia. Ninguna biblioteca de uso general de la última década partió de Logoot, de LSEQ o de Treedoc. Se usaron en editores de investigación y demostraron sus propiedades, pero cuando llegó el momento de sostener un documento grande durante meses, el coste incompresible del nombre y la necesidad de reequilibrar los dejaron fuera.
Esa unanimidad es un dato inusual en ingeniería y merece interpretarse con cuidado, porque puede leerse de dos maneras. La lectura perezosa dice que la comunidad convergió por imitación y por disponibilidad de código, que es un fenómeno real y que sin duda influyó. La lectura que sostienen las mediciones dice otra cosa: los grupos que eligieron la rama de RGA lo hicieron en momentos distintos, con lenguajes distintos y con objetivos distintos, y varios de ellos publicaron comparaciones propias antes de decidir. Cuando equipos independientes llegan al mismo sitio por caminos separados, lo prudente es asumir que hay una razón estructural, y en esta lección ya está identificada: es la cuarta columna.
Yjs
Familia YATA, pariente directo de RGA, con bloques por tanda, borrados por rangos y recolección de lo inalcanzable.
Automerge
RGA para listas y texto, con formato binario por columnas que comprime cada componente del sello por separado.
Fugue y derivadas
Misma rama, reglas de desempate afinadas para que dos tramos concurrentes no se entrelacen carácter a carácter.
Índice fraccionario
La vía de Treedoc en aplicaciones de listas cortas, viable porque casi siempre hay un servidor que puede renumerar.
El criterio para tu caso
La última tarjeta merece una lectura desapasionada, porque el índice fraccionario sigue siendo la elección correcta en muchos productos y esta lección no pretende desaconsejarlo. Ordenar treinta tareas en un tablero no es ordenar un millón de caracteres en un documento: el número de inserciones en un mismo punto es pequeño, el crecimiento del nombre nunca llega a doler y, si algún día duele, hay un servidor que puede renumerar en una transacción y nadie se entera. Aplicar una secuencia convergente completa a ese problema es cargar con lápidas, sellos y un índice auxiliar para resolver algo que una clave ordenada resuelve en veinte líneas.
En la mayoría de los proyectos que adoptan una secuencia convergente, la decisión que más consecuencias tiene no es cuál de estas familias se elige sino si hacía falta una. Reordenar listas cortas, mantener el orden de unos cuantos elementos por usuario o gestionar una cola de tareas rara vez justifican el aparato completo, y una clave ordenada con renumeración ocasional resuelve el problema con una fracción del código y sin metadatos residuales de ningún tipo. Si en cambio el contenido es texto que dos personas editan a la vez, ninguna alternativa sencilla funciona y conviene no perder tiempo intentándolo: adopta una biblioteca madura de la rama de RGA y dedica el esfuerzo al modelo de datos que va encima.
El criterio que separa ambos mundos tiene tres preguntas y ninguna es sobre rendimiento. ¿Puede el número de inserciones entre dos elementos fijos crecer sin límite, como ocurre al escribir texto, o está acotado por el uso, como ocurre al reordenar una lista? ¿Existe algún momento en que un componente con autoridad pueda renumerar sin romper a nadie, o el sistema debe funcionar con réplicas desconectadas durante días? ¿Importa que dos ediciones concurrentes del mismo punto queden agrupadas en bloques, o basta con que el resultado sea determinista? Tres síes de la segunda columna llevan al índice fraccionario y tres de la primera llevan a la rama de RGA, y casi nunca hay dudas.
El caso genuinamente incómodo es el intermedio, y conviene reconocerlo antes de tropezar con él: listas largas de elementos estructurados que además se reordenan, como los bloques de un documento por secciones o las filas de una hoja de cálculo. Ahí el número de inserciones en un punto puede crecer bastante, no hay servidor que arbitre y, sobre todo, aparece una operación que ninguno de los cuatro algoritmos de este nivel sabe hacer: mover un elemento sin borrarlo y recrearlo. Un movimiento expresado como borrado más inserción se duplica cuando dos réplicas mueven el mismo elemento a sitios distintos, y ese es un problema propio que exige estructuras específicas y que el track aborda más adelante.
Queda un consejo final que no se deduce de la tabla y sí de la historia que la tabla resume. Estos cinco algoritmos tardaron quince años en converger a una respuesta, y el trabajo que separa una descripción correcta de una implementación que aguanta un documento real es mucho mayor que el que separa dos algoritmos entre sí. Implementa uno para entenderlo, y hazlo con la traza de tu dominio delante; después usa una biblioteca que alguien mantenga. Lo que has aprendido en estas cinco lecciones no sirve para escribir tu propio motor de secuencias, sirve para leer el que elijas, entender qué te está costando y saber, cuando algo vaya mal, en qué columna del cuadro está el problema.
Si tuvieras que quedarte con una sola idea de estas cinco lecciones, que sea esta, porque se traslada intacta al resto del track y a decisiones que todavía no has tomado. Durante casi una década la comunidad comparó estos algoritmos por su complejidad asintótica de inserción, que es lo que se compara cuando uno viene de la algoritmia clásica, y esa comparación produjo empates y regímenes cruzados y ningún ganador claro; lo que finalmente ordenó el campo fue una propiedad que no aparece en ningún análisis de complejidad, porque no es una propiedad del algoritmo sino de la forma de sus metadatos: si la información residual que la estructura acumula es redundante o es entropía. Las lápidas de RGA son enormemente redundantes —un millón de caracteres borrados de una tanda son un rango, dos números— y por eso su coste nominal, que en la tabla parece el peor, se evapora al escribirlo en disco o al mandarlo por la red. Los caminos largos de Logoot y de Treedoc son información genuina sobre el historial de inserciones en cada punto, irreductible por construcción, y por eso su coste nominal, que en la tabla parece moderado, es el que realmente se paga. La lección general es que en una estructura replicada lo que importa no es cuánto metadato generas sino cuánta estructura tiene ese metadato, porque la estructura se comprime, se difiere, se poda y se transmite por delta, y el ruido no admite ninguna de esas cuatro operaciones. De ahí se deduce una heurística de diseño que vale mucho más allá de las secuencias y que puedes aplicar mañana: cuando tengas que elegir entre dos representaciones convergentes, no preguntes cuál genera menos bytes en el caso medio, pregunta cuál genera bytes que se parecen entre sí. Un contador por réplica se parece al siguiente; un conjunto de identificadores aleatorios no se parece a nada. Un sello que avanza de uno en uno mientras alguien teclea es una secuencia aritmética disfrazada; una profundidad de árbol que depende de quién escribió antes en ese punto es historia irrepetible. Y hay un corolario final que explica por qué esta discusión no está cerrada: si el criterio real es la compresibilidad de los metadatos, entonces el siguiente salto no vendrá de un algoritmo de ordenación mejor sino de representaciones que guarden menos historia o que la reconstruyan bajo demanda, que es exactamente hacia donde apuntan las líneas de trabajo más recientes sobre grafos de eventos. El cuadro de esta lección es un retrato de 2011 leído con los ojos de hoy; la columna que lo decidió seguirá decidiendo lo que venga.
- Implementa las cuatro estructuras en su versión mínima, sin optimizaciones, y valida que todas convergen con el mismo conjunto de operaciones.
- Consigue o graba una traza de edición real de tu dominio y descarta las inserciones uniformemente aleatorias como banco de pruebas.
- Mide para cada familia bytes de identificador por byte de contenido, y repite la medida después de borrar la mitad del documento.
- Serializa cada estructura y vuelve a medir tras aplicar una compresión de propósito general, para ver cuál de los costes era redundancia.
- Añade agrupamiento por tramos a las dos familias que lo admiten y comprueba cuánto cambia el ranking.
- Con los números delante, escribe en una página qué elegirías para tu caso y qué pregunta de la tercera sección te llevó ahí.