Salidas grandes y streaming
Por omisión, TDC genera directo a disco y funciona a cualquier tamaño — hasta miles de millones de filas. La memoria no crece con la cantidad de filas: cada fila se calcula al vuelo a partir de su número, no se guarda en un arreglo. Sin preparación especial — así es simplemente como funciona una corrida normal. Un puñado de formas de configuración son la excepción y están listadas en Qué motor corre su configuración.
Las salidas de ejemplo de esta página son ilustrativas y pueden variar entre versiones del core; donde la página hace una afirmación numérica («exactamente 70/30», «128 MB planos»), observe la forma del resultado, no los bytes exactos.
- Aguardar cada fila en memoria: el costo crece con la corrida
- Bstreaming: una ventana a la vez, así que el costo se mantiene plano por larga que sea la corrida
Dos motores de disco, elegidos por usted
Bajo «disco» corren dos motores, y TDC elige entre ellos a partir de su configuración:
-
El motor rápido de streaming — se usa para casi todo. Es perezoso, multihilo (vea
--jobs), con memoria O(cantidad de campos). Porcentajes exactos, dependenciasparent,<mix>,<distinct>— todo al vuelo. -
El motor exacto en disco — para una promesa sobre la columna terminada y no sobre la fila actual: un grupo
<uniq>a nivel de env,uniq="true"en una secuencia compuesta o en un contador, unparentcuyo padre no es una secuencia de texto, y unadvanced_regexcon pesos —(?%{…}). Garantiza el resultado de forma exacta y lo paga revisando los datos con un ordenamiento externo y una pasada de reparación — una revisión que se vuelve drásticamente más lenta a medida que crece el número de filas (vea la advertencia abajo).La memoria se mantiene acotada mientras las repeticiones queden por debajo del tope de la reparación; pasado ese tope la corrida se entrega al motor en memoria y la memoria vuelve a seguir a
count. Vea la tabla de abajo: lo que hay que razonar es el tope, no el número de filas.La unicidad es una promesa sobre el conjunto terminado, no sobre una fila, y no se puede resolver fila a fila. Un worker sólo ve su propio rango de filas y no distinguiría un duplicado fuera de ese rango de un valor que nunca ha visto — así que no se le pregunta. La disposición se calcula UNA vez, antes de que arranque ningún worker, y se les entrega hecha; los workers sólo colocan las filas que les tocaron. Por eso un grupo
<uniq>a nivel de<env>se reparte con--jobscomo cualquier otra cosa, en las cinco implementaciones.uniq="true"sobre una secuencia no, y no puede: reordena los generadores dentro de una columna compuesta, algo que un worker que resuelve una fila por su cuenta no puede reproducir. Esa forma se queda en un solo hilo y lo dice.
El modo disco tiene un tercer destino, y es el que conviene conocer: siete formas de
configuración mandan la corrida de vuelta al motor pequeño en memoria, donde la memoria
crece con count. Una de ellas es la manera más común de escribir uniq. Qué motor corre
su configuración lista las cinco.
La elección es determinista — se basa en la configuración, no en el hardware — así que la misma configuración da el mismo resultado en cualquier máquina (la reproducibilidad entre máquinas es una garantía central de TDC).
Qué motor corre su configuración
mode="disk" pide memoria acotada. No siempre la consigue. TDC lee primero la
configuración, y siete formas rutean la corrida al motor pequeño en memoria, cuya
memoria crece con la cantidad de filas. Se revisan en este orden.
| Forma | Por qué no puede correr en streaming |
|---|---|
Un value de template que interpola un campo — common.vehicle.model.${{Brand}} | La dirección no se conoce hasta que la columna vecina tiene un valor, así que hay que resolverla fila por fila contra las otras secuencias. |
weight= y row= en el mismo generador file | Ponderar un sorteo de fila enlazada hasta una cuota exacta necesita los totales del archivo por adelantado. weight= por sí solo sí transmite — lo que no puede es la pareja. |
uniq="true" sobre una sola columna sorteada, sola o junto a texto literal | El sorteo corre sin reemplazo, así que tanto la bolsa como el conjunto de lo ya tomado abarcan la columna entera. |
type="http" — una llamada de red | No es reproducible ni síncrona, y se resuelve en una pasada asíncrona después de que se arma el resto del registro. |
Un percent= dentro de una rama de <switch> con varias claves — is="US|CA|MX" — o dentro de <default> | La proporción es una cuota sobre las filas propias de la rama, y esas filas son una unión de subconjuntos, o lo que dejaron las demás ramas. Ninguna de las dos se puede numerar fila a fila. |
| Una columna derivada de otra columna — un total acumulado, un estadístico, una fecha medida desde otra columna, una fórmula | Cada una lee una columna que tiene que existir antes, y el constructor de flujo la rechaza por nombre. |
Un parent="Nombre" pelado sin valor | Reduce la columna a las filas donde el padre produjo algo, y eso no se sabe sin la columna entera del padre. |
Otra forma más llega al mismo motor sin estar en esta lista: un cuerpo de paquete con su
propio <valid>. Rechazar una fila sorteada y volver a sortear es una decisión sobre la
columna entera y no tiene forma fila a fila, así que el constructor en streaming la rechaza
por nombre — y un rechazo rutea aquí igual que cualquier otro.
Todo lo demás que pregunte por la columna terminada va al motor exacto en disco, y el resto corre en streaming.
Así que uniq cae en dos motores distintos según cómo esté escrito:
uniq escrito como | Motor | Memoria |
|---|---|---|
uniq="true" sobre una sola columna sorteada — text, number, date, template | en memoria | crece con count |
uniq="true" sobre una columna hecha de una parte sorteada y literales <data> | en memoria | crece con count |
uniq="true" sobre un contador | exacto en disco | crece con count |
uniq="true" sobre una secuencia compuesta (campos <gen> con nombre) | exacto en disco | acotada mientras haya pocas repeticiones |
Un grupo <uniq> a nivel de env | exacto en disco | acotada mientras haya pocas repeticiones |
Las dos últimas están acotadas, y el límite se demostró PROHIBIENDO la memoria en vez de observarla: a cada corrida de abajo se le puso un techo duro de heap y tuvo que terminar dentro de él.
grupo <uniq> de env 4.800.000 filas terminó con el heap limitado a 256 MB
uniq compuesto 4.800.000 filas terminó con el heap limitado a 256 MB
grupo <uniq> de env 10.000.000 filas terminó con el heap limitado a 512 MB
Observar en vez de prohibir es justo lo que dejó mal esta tabla antes. Sin límite, la corrida de 4.800.000 filas llega a un pico de unos 850 MB — pero termina en 256 MB cuando no se le permite más, porque un entorno con recolector de basura toma el sitio que le dan. La memoria residente máxima mide lo que el entorno ELIGIÓ; el techo mide lo que la corrida NECESITA.
Lo que rompe el límite son las repeticiones, no las filas. La reparación trabaja con las
filas cuyas tuplas chocan, y las sostiene; pasadas max(20.000, count / 1000) se detiene y
entrega la corrida al motor en memoria, donde la memoria vuelve a seguir a count. Las mismas
4.800.000 filas, dos espacios de valores:
20.000 x 20.000 valores → falló incluso con el heap limitado a 512 MB
40.000 x 40.000 valores → terminó dentro de 256 MB
Así que lo que hay que razonar es cuántas tuplas pueden formar sus columnas frente a cuántas
filas pidió — no el número de filas por sí solo. Ensanche una columna y la misma corrida cabe.
Cuántas repeticiones produce de verdad una configuración sí la predice bien la fórmula del
cumpleaños — una corrida de 6.000.000 de filas sobre 900.000.000 de pares posibles entregó al
verificador 19.851 grupos candidatos donde la fórmula predice unos 20.000 —, pero lo que eso
cuesta en tiempo no. Ejecútelo y mire --progress.
Las que de verdad siguen a count son las dos filas en memoria, y
TDC299 avisa de ellas a partir de 100.000 filas. Un contador
también crece: 2.000.000 de filas necesitaron 512 MB y 4.000.000 necesitaron 1 GB, medido de
la misma manera.
Ninguna forma de uniq corre en el motor rápido de streaming. Rechaza uniq por su
nombre, así que a una configuración que pidió streaming se le dice, en vez de entregarle
datos que se repiten en silencio:
tdcv2: stream mode: uniq (a whole-column rearrangement) ("K") is not supported yet — use mode="disk" instead (the router then picks an engine that can), or remove it.Normalmente usted nunca ve un mensaje así. El ruteo elige el motor por su cuenta, y un rechazo solo sale a la luz cuando una configuración nombra un motor de streaming y con eso pidió que se le dijera.
Una bajada de motor no es un error. Cada una de las siete formas es una promesa sobre una
columna entera, y un motor que la respondiera desde una sola fila emitiría datos que se
ven bien y no lo son. Lo que cuesta es memoria: en el motor en memoria se retiene la
columna entera, así que una corrida con una de estas formas queda acotada por la RAM y no
por el disco. preflight() lo estima
antes de la corrida.
La lista de arriba se decide antes de la corrida, y eso importa para --jobs: una
corrida paralela le da a cada worker un motor de flujo forzado, y un worker no tiene
adónde caer. Queda igual una red de último recurso, para un caso límite raro de
uniq/distinct/parent que la lista no ve: una corrida a disco ruteada automáticamente
cae de vuelta al motor en memoria en lugar de fallar. Un --engine 2 forzado igual
falla, que es justamente el punto de forzarlo:
tdcv2: a statistic ("Avg") is computed over every row of the run, including the ones after this one, so it cannot be computed one row at a time; the in-memory engine handles it (run without a forced streaming engine)Un --engine 3 forzado es la excepción, y conviene saberlo antes de medir nada: rechaza
solo un uniq demasiado ajustado para su reparación acotada, y en las demás formas de la
lista de arriba cae al motor en memoria e imprime sus bytes — código 0, ni una palabra. Es
deliberado: esas formas son justo las que el camino perezoso no puede expresar, y cubrirlas
es para lo que existe el motor 3. Eso sí implica que una lectura de memoria tomada con
--engine 3 sobre alguna de ellas es una lectura del motor 1.
Lo que sí hace streaming aunque no lo parezca
Dos construcciones leen las columnas de al lado y aun así hacen streaming, porque cada una necesita solo su PROPIA fila — y una fila es exactamente lo que el registro perezoso sabe producir:
| Construcción | ¿Streaming? | Por qué |
|---|---|---|
<gen type="formula"> | sí | La fila i se calcula a partir de la fila i. Nada anterior participa. |
| Un parámetro de distribución escrito como expresión | sí | Cambia el valor en que se convierten los sorteos, nunca cuántos gasta una fila. |
read="quantile" sobre un archivo | sí | El archivo se ordena una vez; luego una fila toma un punto sobre él. |
<gen type="running"> | no | La fila 900.000.000 ES la suma de todo lo anterior. |
<gen type="stat"> | no | Las filas POSTERIORES a esta forman parte de la respuesta. |
La línea divisoria no es «sorteado o calculado», sino si la respuesta necesita alguna fila distinta de esta.
uniq sobre una salida enorme es LENTO — y uniq + percent es lo más lento que hace TDCGarantizar que ninguna fila se repita en un archivo enorme es fundamentalmente caro: el motor exacto genera, luego ordena toda la salida y repara cada colisión, y ese trabajo crece más rápido que linealmente con el número de filas. Cientos de miles de filas únicas ya tardan minutos; los millones pueden correr horas o más. La memoria se mantiene plana — el tiempo no.
El peor caso, por lejos, es uniq y percent sobre las mismas columnas. Acertar
proporciones exactas y no repetir a la vez es un problema de acomodo con restricciones
encima del ordenamiento, así que es dramáticamente más lento otra vez: una corrida que
sería rápida sin uno de los dos puede tardar un tiempo irrazonable con ambos. Si puede
soltar la exactitud (deje que las proporciones sean aproximadas) o la unicidad, hágalo.
Para unicidad a escala, prefiera lo que es barato por construcción: un
contador, o un rango number lo
bastante amplio como para que una colisión sea prácticamente imposible. Reserve
uniq="true" — y sobre todo uniq + percent — para los tamaños donde pueda permitirse
la espera. Una corrida normal (sin uniq) de cualquier tamaño sigue siendo rápida.
El viejo mode="disk" ahora es el valor por omisión — no hace falta ninguna bandera.
mode="memory" corre un motor pequeño en RAM (exacto, pero que no escala) — el mismo
que está detrás de la API de objetos
(toArray/iterate/getAt). Fuerce un motor específico con
--engine 1|2|3; --stream es un alias heredado del motor rápido
de streaming.
Mil millones de filas
<env count="1000000000" seed="s">
<sequence name="Gender"><gen type="text" value="M,F" percent="70,30"/></sequence>
<sequence name="Id"><gen type="increment" value="1"/></sequence>
</env>
Estas son las primeras ocho filas de esa corrida:
M,1 F,2 M,3 M,4 M,5 F,6 M,7 M,8
count para previsualizar una corrida que usa percentCambie count="1000000000" por count="8" para verlo rápido y obtendrá filas
distintas: M M M M M M F F, no las ocho de arriba. percent es una cuota exacta
repartida sobre el count completo, así que reducir la corrida vuelve a repartir la
columna entera; la corrida pequeña no es el comienzo de la grande. Seis M y dos F son
exactamente 70/30 de ocho, y ese es el punto: la cuota se respeta en cualquier tamaño, y
por eso mismo no puede ser además un prefijo. Lo mismo con uniq y con un pack
ponderado — vea Determinismo y proporciones, la
sección sobre los diseños que abarcan toda la ejecución. Un count pequeño es la forma
correcta de comprobar la forma (el formato, las proporciones, que los campos
concuerden) y la forma incorrecta de predecir qué valor caerá en la fila 5 de la corrida
real.
Aquí TDC usa el motor rápido de streaming (no hay uniq pesado). No materializa un registro — el valor de cada fila se calcula a partir de su número, así que la memoria es O(campos), no O(filas), y los porcentajes se mantienen exactos (precisamente 70/30, sin ningún arreglo guardado). El resultado es determinista.
El motor rápido resuelve casi todo: <sequence>
simples y compuestas, generadores independientes
(text, number,
date, regex,
symbol, template),
percent exacto, contadores,
los integrados (_count/_first/_last/_total),
dependencias parent (a cualquier profundidad),
<distinct> y <mix> — todo al vuelo,
exacto y en paralelo. Lo que no hace es ninguna forma de
uniq. La unicidad es una promesa sobre la columna
terminada y este motor solo ve una fila a la vez, así que todo uniq se va a otra parte —
al motor exacto en disco o al motor en memoria, según cómo esté escrito. Qué motor corre
su configuración dice cuál.
(En el motor rápido, parent funciona solo cuando el padre es una secuencia con una lista
finita de valores — una secuencia text. Heredar de un rango
numérico manda la configuración al motor exacto.)
Dependencias parent en el stream
Una secuencia hija con parent="Parent.Value" está activa
exactamente en las filas donde el padre produjo ese valor, y sus porcentajes son exactos
dentro del subconjunto. El anidamiento llega a cualquier profundidad (padre → hijo →
nieto).
<env count="1000" seed="s">
<sequence name="Gender"><gen type="text" value="M,F" percent="70,30"/></sequence>
<sequence name="Male" parent="Gender.M"><gen type="text" value="Juan,Carlos,Miguel" percent="50,30,20"/></sequence>
<sequence name="Female" parent="Gender.F"><gen type="text" value="María,Elena" percent="60,40"/></sequence>
</env>
En las filas M se llena el campo Male; en las filas F, el campo Female. Las
primeras 6 de 1000 filas:
F,,María M,Carlos, F,,Elena F,,María F,,María F,,María
La distribución es exacta en todos los niveles — sin arreglos, cada fila calculada a partir de su número:
Gender M 700 Gender F 300 Male Juan 350 Male Carlos 210 Male Miguel 140 Female María 180 Female Elena 120
Exactamente 700 M y 300 F; dentro de los 700 hombres, exactamente 350/210/140 (50/30/20
de 700), y dentro de las 300 mujeres, exactamente 180/120 (60/40 de 300). En las filas
«ajenas» el campo hijo queda en blanco — Male está vacío en las filas femeninas, y
Female en las masculinas.
Unicidad en todo el conjunto de datos (uniq)
uniq="true" en una secuencia compuesta hace que la tupla de todos
sus campos sea única en todo el conjunto de datos. Eso es una promesa sobre la columna
terminada, así que corre en el motor exacto en disco y no en el de streaming. Cada columna
se reparte hasta su cuota exacta y luego las tuplas se comparan entre sí; cuando una sola
columna ya le da a cada fila un valor distinto, la comparación se omite.
<env count="6" seed="s">
<sequence name="Combo" uniq="true">
<gen name="Letter" type="text" value="A,B,C"/>
<gen name="Digit" type="text" value="1,2"/>
</sequence>
</env>
Las 6 filas son distintas — ese es el espacio completo de 3 × 2:
C,2 A,1 B,2 A,2 C,1 B,1
Lo mismo vale para el <uniq> a nivel de env, donde la tupla única se
construye a partir de secuencias separadas en vez de campos de una sola:
<env count="6" seed="s">
<uniq>
<sequence name="A"><gen type="text" value="x,y,z"/></sequence>
<sequence name="B"><gen type="text" value="m,n"/></sequence>
</uniq>
</env>
z,n x,n x,m y,n y,m z,m
La memoria se mantiene acotada en las dos formas: las columnas se resuelven a partir del número de fila, y la revisión de duplicados corre por fuera en vez de retener el conjunto de datos. Lo que cuesta es tiempo, no RAM — vea la advertencia arriba.
La capacidad se revisa antes de que empiece la corrida. Si pide más filas únicas de las que los datos pueden producir, TDC falla de inmediato con un error claro — no ocho horas después, a media escritura del archivo:
tdcv2: uniq: group "K1 × K2" cannot produce 10000000 unique combinations — the values drawn for these sequences allow at most 100 distinct rows. Add more values to a member (more distinct names, wider ranges…) or lower the count.
El chequeo corre antes de construir una sola fila, solo a partir de la configuración:
una lista de diez nombres da diez valores, un rango entero 1..100 da cien, y su producto
es el máximo de filas distintas que el grupo podría contener. Un count= de miles de
millones se responde así en milisegundos, y no después de que el run haya pedido memoria
que no va a conseguir.
Lo único que no hace es adivinar. Si la capacidad de algún miembro no se puede saber por su escritura — una regex, un sorteo de un pack, un fichero — el grupo queda sin cota aquí y la respuesta viene del chequeo sobre las columnas ya construidas, igual que antes. Un rechazo es siempre una prueba.
<mix> en el stream
<mix> elige un case para cada fila por porcentaje exacto (la
misma matemática que percent) y luego arma el cuerpo del case: texto, generadores y
<mix> anidados a cualquier profundidad. Un generador o un <mix> anidado dentro de un
case corre sobre el subconjunto de filas de ese case — su contador cuenta dentro del
case, y los porcentajes anidados son exactos dentro del subconjunto.
<env count="1000" seed="s">
<mix name="Status" percent="20,50,30">
<case><data>new</data></case>
<case><data>active-</data><gen type="number" value="1..3"/></case>
<case><data>closed</data></case>
</mix>
</env>
Las primeras 6 de 1000 filas:
active-1 new active-3 closed active-1 closed
Sobre 1000 filas el reparto es exacto: 200 new, 500 active-N, 300 closed. <mix>
también se compone con parent — y entonces está activo
solo en las filas del padre.
Por qué percent + uniq es la pareja cara
Los porcentajes exactos se reparten sobre la columna entera; la unicidad se revisa sobre la columna entera. Cada cosa por separado es asequible. Pedir las dos a la vez es un problema de acomodo con restricciones encima de la revisión, y eso es o materialización total o una búsqueda NP-difícil. El motor exacto en disco lo hace igual, a cualquier tamaño, reteniendo el acomodo y reparando las colisiones que encuentra — de forma correcta, y mucho más lentamente que cualquiera de las dos restricciones por separado.
Paralelismo — automático
La generación está limitada por CPU, no por disco (escribir es mucho más rápido que calcular filas), y las filas del motor de streaming son independientes (cada una sale de su propio número), así que TDC las calcula en varios núcleos — gratis, por arquitectura.
Usted no configura nada. Si la configuración es divisible (motor rápido, sin
generadores integrados en línea) y el archivo es lo bastante grande, TDC usa
núcleos − 1 (7 en una máquina de 8 núcleos); si no, corre calladamente en un solo núcleo:
npx tdcv2 customers.tdc -o customers.csv
El resultado es idéntico byte por byte sin importar la cantidad de núcleos (con la misma semilla): cada núcleo calcula un rango contiguo de filas hacia un archivo temporal, y luego se concatenan estrictamente en orden. La cantidad de hilos es solo cuestión de velocidad — nunca afecta los datos.
La cantidad automática también se recorta para caber en memoria, y esa suele ser la
respuesta a «¿por qué no va más rápido?». A cada worker se le cobran 120 MB más 50× el
tamaño en disco de cada archivo src= que vaya a analizar, y el total no puede pasar de
la mitad de la RAM física — así que una configuración que lee un CSV grande recibe muchos
menos workers que núcleos hay. El recorte se anuncia solo cuando --jobs se pasó de forma
explícita; una corrida automática lo hace en silencio.
Una medición — 1 000 000 de filas, seis campos (un contador, dos nombres de plantilla, una
columna con percent, una distribución normal y una fecha), un archivo de 74 MB, en una
máquina de 12 núcleos:
--jobs | tiempo | aceleración |
|---|---|---|
| 1 | 6.93 s | ×1 |
| 2 | 4.04 s | ×1.7 |
| 4 | 2.27 s | ×3.1 |
| 8 | 1.57 s | ×4.4 |
| 12 | 1.72 s | ×4.0 |
| auto | 1.69 s | ×4.1 |
Dos lecciones. Más hilos no siempre es más rápido: doce hilos en doce núcleos pierden contra ocho — se pelean por los mismos núcleos y el mismo disco. Y los números de arriba son de esa máquina, no de TDC.
Donde más importa es en una máquina cuyos núcleos no son todos iguales. Auto toma
núcleos − 1, y en un Apple M2 Max — 12 núcleos, pero 8 de rendimiento y 4 de eficiencia —
eso alcanza a los cuatro lentos. La misma configuración de seis campos, medida aquí:
--jobs | tiempo |
|---|---|
| 1 | 8,82 s |
| 2 | 6,57 s |
| 4 | 5,00 s |
| 8 | 5,63 s |
| 12 | 6,40 s |
| auto | 6,07 s |
Auto queda 21 % del mejor, y el pico es ×1,8 en vez de ×4,4. En una columna cara en CPU
puede ir peor: una formula con sin, log y gauss sobre 1 000 000 de filas tomó 3,05 s
en un hilo y 4,71 s en auto — más lenta en tiempo y seis veces la CPU.
Así que: en una máquina con núcleos uniformes déjelo estar, y si una corrida importa,
tómele el tiempo con varios valores de --jobs en la máquina que la va a hacer. Lo que no
cambia nunca son los datos: todas esas corridas produjeron los mismos bytes.
La aceleración depende de qué tan cara sea una fila. En una configuración de verdad barata
(dos campos, un contador y M,F) la ganancia es de apenas ~×1.6 — levantar hilos cuesta
tiempo, y en trabajo liviano ese costo se come casi toda la ganancia. Una cifra de
aceleración sin su configuración al lado no significa nada.
Ajústelo a mano con --jobs N si quiere (--jobs 1 fuerza un solo
hilo); la salida es idéntica de cualquier forma:
npx tdcv2 customers.tdc --jobs 8 -o customers.csv
A veces el paralelismo no entra en acción, y la regla es más angosta de lo que parece.
Los dos motores fila a fila reparten una corrida entre núcleos — el rápido de streaming y el
exacto en disco — así que una configuración que solo cayó del motor 2 al 3 sigue usando
todos los núcleos. Lo que no se puede repartir es una corrida en el motor en memoria, y
una forma en particular: uniq="true" sobre una secuencia, que reordena los generadores
dentro de una columna compuesta, y un worker que resuelve una fila por su cuenta no puede
reproducir eso.
Un GRUPO <uniq> a nivel de <env> no es esa forma y sí se paraleliza. Medido sobre
1 500 000 filas: --jobs 1 gastó 17,4 s de CPU, --jobs 8 gastó 52,0 s repartidos entre
ocho workers, y los dos archivos son idénticos byte a byte, tal como promete --jobs.
Auto se queda callado cuando no puede repartir, pero si pidió --jobs explícitamente TDC
le dice por qué. La salida es correcta de cualquier forma.
Tampoco entra por debajo de 100 000 filas. Por debajo de esa cifra levantar los hilos y unir las piezas cuesta más de lo que ahorra el reparto, así que auto se queda en uno. Vale la pena saber el número por una razón: es lo único que cambia entre una corrida de 99 999 filas y una de 100 000, así que si esas dos llegan a diferir en algo más que la longitud, el reparto es donde hay que mirar.
El motor se elige por su configuración, no por su hardware
Cuál de los tres motores corre lo decide TDC por el contenido de la configuración, nunca por la máquina. Esto importa: si la elección dependiera de «cuánta RAM hay libre en este momento», entonces la misma configuración con la misma semilla podría producir datos distintos en computadoras distintas — y la reproducibilidad entre máquinas es la garantía central de TDC. Como el ruteo depende solo de la configuración, una configuración siempre toma un motor y da un resultado en todas partes.
La estimación de memoria (preflight(), abajo) es solo un consejo; no cambia nada ni
altera la salida. Para forzar un motor específico use
--engine 1|2|3 (avanzado); mode="memory" es el motor pequeño en
RAM para conjuntos de datos chicos.
El mismo forzado existe dentro del config, como engine en <env>: la bandera sin
línea de comandos:
<env count="1000" seed="s" engine="1">
1 es en memoria, 2 en flujo, 3 exacto en disco; cualquier otro valor es un error.
Prefiera mode="memory" / mode="disk", que dicen qué quiere en lugar de qué
implementación se lo da: los números de motor son una salida de emergencia para
reproducir un comportamiento concreto, y un config clavado a un motor no se beneficiará
de un mejor enrutado más adelante. Si están los dos, engine gana sobre mode; un
--engine o --mode en la línea de comandos gana sobre cualquiera de ellos.
Métodos terminales (la biblioteca)
Todos los métodos de aquí pasan por el motor que elige el router, los de objetos incluidos. Ninguno fuerza el motor en memoria, así que ninguno materializa un registro: la memoria es O(campos) salvo que lo que crezca sea el propio valor devuelto.
| Método | Salida de texto | Memoria | Sirve para |
|---|---|---|---|
toString() | recolectada completa | O(campos) + texto completo | resultados chicos / medianos |
toIterator() | una fila a la vez | O(campos) | resultados de texto grandes, fila por fila |
toStream() | Readable de Node | O(campos) | canalizar a un archivo / HTTP / un archivador |
writeFile() | por trozos a un archivo | O(campos) | la salida más simple para un archivo grande |
| CLI | por trozos | O(campos) | la línea de comandos |
toArray() | filas como objetos, completas | O(filas) — son lo que devuelve | fixtures de objetos chicas / medianas |
iterate() | filas como objetos, una por una | O(campos) | salida de objetos, una fila a la vez |
getAt(index) | una fila como objeto | O(campos) | acceso puntual, no masivo |
toArray() es el único método de objetos cuya memoria crece con count, y crece porque el
arreglo que devuelve SON todas las filas. iterate() y getAt() no construyen nada de eso.
Medido sobre una configuración de 50 000 000 de filas: getAt(49_999_999) respondió en 4 ms
y las tres primeras filas de iterate() llegaron en 2 ms, con el heap plano en ambos casos.
Para archivos grandes, use la CLI, writeFile(), toIterator(), toStream() — o
iterate() si quiere objetos en vez de texto:
const tdc = new TDC({ configFile: "./customers.tdc" });
tdc.writeFile("./customers.csv");
O mediante un stream:
import { createWriteStream } from "node:fs";
tdc.toStream().pipe(createWriteStream("./customers.csv"));
Prueba: medio millón de filas, la memoria se mantiene plana
Las palabras bonitas sobre «O(campos)» valen más con números reales. Tome una configuración de 500 000 filas y ejecute los terminales.
writeFile() — un archivo en disco. Escribe por trozos conforme genera:
bytes: 4388895 // ~4.4 MB, 500,000 rows M,1 M,2 M,3
toIterator() — recorrer todas las filas, y la memoria no se mueve. Muestreando el RSS
del proceso en puntos de control conforme crece la cantidad de filas:
rows=100000 RSS=128 MB rows=200000 RSS=128 MB rows=300000 RSS=128 MB rows=400000 RSS=128 MB rows=500000 RSS=128 MB
La línea es plana — 128 MB con 100 000 filas y los mismos 128 MB con 500 000. Fíjese en lo plano, no en el número absoluto (el RSS depende de la máquina y la versión de Node — esto fue en una Apple M2 Max con Node 20); la línea es plana en todos lados.
toStream() es igual a writeFile() byte por byte. Ambos toman el mismo camino de
streaming:
new TDC({ configFile: "./customers.tdc" })
.toStream()
.pipe(createWriteStream("out2.csv"));
// md5(out2.csv) === md5(out.csv) → true
preflight() — una estimación del riesgo de memoria
preflight() estima el riesgo de memoria antes de generar, comparando la estimación con la
RAM total de la máquina (no con la «libre en este momento» — el sistema operativo le
entrega memoria a un proceso bajo demanda, así que una cifra de libre instantáneo es
engañosa).
const diagnostic = tdc.preflight();
En una corrida normal (a disco) ni siquiera medio millón de filas representa riesgo —
preflight() devuelve undefined. Solo advierte cuando hay un mode="memory" explícito
con un count grande, donde los datos sí se materializan de verdad:
// disk (default), 500,000 rows:
new TDC({ configFile: "./customers.tdc" }).preflight();
// → undefined
// mode="memory", 50,000,000 rows — returns a Diagnostic:
const d = new TDC({
configFile: "./customers.tdc",
mode: "memory",
count: 50_000_000,
}).preflight();
d.severity warning
d.code TDC200
d.message estimated memory need (~20.5 GB) is a large share of this
machine's RAM (32.0 GB) — may lean on swap and slow down
d.hint This will still run; for very large datasets mode="disk"
keeps memory flat regardless of count.Así que en una corrida ordinaria a disco preflight prácticamente nunca se dispara: el motor de streaming retiene O(campos), no O(filas), así que mil millones de filas pasan tan tranquilas — para eso está hecho. La estimación es solo un consejo; no cambia de motor ni altera la salida.
Si sabe que va a consumir la salida del streaming con toString(), nombre el escenario
explícitamente:
const diagnostic = tdc.preflight({ output: "streaming" });
Qué se materializa en RAM
Los dos motores de disco no guardan nada extra en memoria. La materialización ocurre en el
motor pequeño en RAM — al que se llega por la API de objetos
(toArray/iterate/getAt), por un mode="memory" explícito y por cualquiera de las
siete formas de configuración que rutean una corrida a disco de vuelta a
él. Ahí retiene, de entrada:
- los integrados
_count,_first,_last,_total; - cada
<sequence>simple; - cada campo de una secuencia compuesta;
- los arreglos de valores filtrados por parent, o
undefined; - los planes de enlace de filas CSV para datos externos enlazados.
Una estimación gruesa: count × cantidad_de_ranuras_de_secuencia. Por ejemplo, esta
secuencia compuesta:
<sequence name="Person">
<gen name="FirstName" type="template" value="person.female.firstName"/>
<gen name="LastName" type="template" value="person.lastName"/>
</sequence>
Salomé Zambrano Salomé Juárez Sol Miranda Olivia Rodas Úrsula López
ocupa dos ranuras de secuencia: Person.FirstName y Person.LastName.
Reglas prácticas
- Para un archivo de cualquier tamaño, simplemente use
writeFile()o la CLI — a disco por omisión, y la memoria no crece con las filas. - Antes de una corrida muy grande, revise la configuración contra las siete formas que la
rutean de vuelta a memoria. El
uniq="true"simple es la que hay que vigilar. - Para acelerar muchas filas, agregue
--jobs N(en el motor rápido). toString()es cómodo para pruebas y resultados chicos, pero junta todo el texto en una sola cadena — no sirve para archivos grandes.toArray()devuelve todas las filas como objetos, así que su memoria es el arreglo mismo; ese sí es para conjuntos chicos.iterate()ygetAt()no: corren en el mismo motor que la salida de texto y no retienen nada, así queiterate()sirve perfectamente para emitir objetos en streaming.
Vea también
- CLI —
--jobs,--mode,--engine. - Valores únicos —
uniq,<uniq>y<distinct>a fondo. - Dependencias jerárquicas —
parenta fondo. - Bindings de lenguajes — la API de la biblioteca completa.