Acotar la lectura atascada

Empate async

Requisito

Lee cuatro valores de sonda en secuencia; la tercera lectura se atasca durante 500 ms. Da a cada lectura un presupuesto de 150 ms: imprime las lecturas que llegan a tiempo, luego reading timed out para el atasco, y detente — la cuarta lectura no debe reportarse. El atasco se inyecta de forma determinista en el código; las dos versiones deben imprimir las líneas que aparecen bajo Salida esperada.

Salida esperada
reading: 21.5
reading: 21.7
reading timed out

Lado a lado

RxDart

FxDart

Por qué difieren

El presupuesto por lectura en sí es fácil en ambos lados — el panel de RxDart acota el Future dentro de asyncMap, el timeout de FxDart acota el pull — así que la diferencia interesante es qué mide el operador a nivel de stream del mismo nombre en cada modelo. Stream.timeout vigila el hueco entre eventos: el productor decide cuándo llegan los valores, así que «demasiado lento» solo puede significar «hace rato que no llega nada». El timeout de FxDart acota el tiempo demanda-a-elemento: el consumidor pregunta, y el reloj corre desde la pregunta hasta la respuesta. En esta tarea finita y secuencial ambos coincidirían — pero son cantidades genuinamente distintas: un pipeline pull sin demanda no tiene huecos que medir, y un stream push no le debe respuesta a la pregunta de nadie.

Cada lado necesita entonces un arruga real para cumplir la cláusula «y detente». En el lado push la fuente atascada sigue ahí fuera, y volvería a empujar lecturas en cuanto la lectura lenta por fin aterrizara — así que después de que onErrorReturnWith convierta el error en la línea del informe, takeWhileInclusive termina el stream y cancela la suscripción. En el lado pull detenerse es gratis — la TimeoutException simplemente sale del bucle y nada vuelve a tirar — pero conservar las lecturas que precedieron al atasco significa recolectar con each en lugar de un toList que las habría descartado al lanzar.

Un empate: un operador más una arruga en cada lado, y las arrugas son imágenes especulares de la naturaleza de cada modelo.

Benchmark

Apple M1 Max, 32 GB de RAM · Dart 3.12.2 (compilado AOT) · 2026-08-18

Caso async: la escala principal es N = 10,000, no 1,000,000. Cada elemento cuesta una vuelta del event loop en ambos lados, así que un millón de awaits reales mediría el event loop de Dart durante minutos — no el pipeline. Los retardos son de longitud cero y se conserva el límite de concurrencia del ejemplo; lo que comparan las barras es la maquinaria del pipeline.

N = 100

Tiempo Empate

RxDart 174 µs
FxDart 145 µs

Memoria pico Empate

RxDart 16.6 MB
FxDart 17.1 MB

N = 10,000

Tiempo Gana FxDart

RxDart 14.3 ms
FxDart 12.9 ms

Memoria pico Empate

RxDart 27.4 MB
FxDart 27.0 MB

Las barras son medianas de iteraciones cronometradas repetidas en procesos nuevos por lado (los N pequeños se agrupan por resolución del temporizador). Dos lados a menos del 5% entre sí — o a menos de 0.6 ms, una diferencia que nadie puede percibir — cuentan como empate; las carreras relativas ajustadas se vuelven a medir hasta 5 veces. En una app, cualquier cosa por debajo de unos pocos milisegundos es invisible para el usuario, gane la barra que gane. La memoria es el RSS pico del proceso. La VM de Dart y el dataset son idénticos en ambos lados, así que la diferencia entre las dos barras es lo que retiene el pipeline en sí.