Primera transacción por encima del presupuesto

Empate

Requisito

Recorre el feed de tarjeta de esta semana en orden de llegada e informa de la primera transacción por encima del presupuesto de 100 — y deja de buscar. Imprime también cuántas transacciones se examinaron realmente, para demostrar que la búsqueda cortocircuitó. Los datos están en el código; las dos versiones deben imprimir las líneas que aparecen bajo Salida esperada.

Salida esperada
First over budget: T-04 (128)
Examined 4 of 8 transactions

Lado a lado

RxDart

FxDart

Por qué difieren

Aquí ambos lados son genuinamente perezosos, cada uno en su propio dialecto. El firstWhere de RxDart resuelve su future en la primera coincidencia y cancela la suscripción — las cuatro transacciones restantes nunca se entregan. El find de FxDart simplemente deja de tirar — las cuatro transacciones restantes nunca se demandan. Cancelación y demanda son las palabras de cada modelo para la misma economía, y la línea «Examined 4 of 8» sale idéntica en ambos lados.

La diferencia instructiva es dónde vive el contador. Un stream tiene un «entre»: doOnData pincha la tubería entre operadores, así que el predicado rx se mantiene puro mientras el tap observa el tráfico. Una cadena pull no tiene «entre» — el momento de la demanda es la propia llamada al predicado, así que el lado FxDart cuenta dentro de él. Ninguna de las dos formas es mejor; son los idiomas de observación nativos de push y pull. Veredicto: empate — un operador cada uno, y ambos se detienen exactamente en el momento justo.

Por qué la diferencia del benchmark es tan grande

Las barras de abajo no miden la pereza. A la escala del benchmark, la primera transacción por encima del presupuesto está en el elemento 900 001 de un millón, y ambos lados examinan exactamente 900 001 — los checksums lo demuestran. Lo que miden las barras es el precio de que un elemento atraviese cada modelo: unos 1 ns del lado pull, unos 88 ns del lado push.

Eso no es un defecto de RxDart, ni una forma de escribirlo que se arregle eligiendo mejor los operadores. Medimos las alternativas sobre el mismo conjunto de datos — 900 001 elementos, resultados idénticos:

Incluso ese suelo es 20× la cadena pull. La razón es estructural. Un Stream es un mecanismo de entrega: cada valor se entrega a una suscripción, a través de todas las capas de transformación que tenga la cadena, con la disciplina del bucle de eventos que hace seguro compartir, pausar, cancelar y componer un stream a través de fronteras asíncronas. El find de FxDart sobre una List compila a un bucle indexado que llama a un closure por elemento y retorna — no hay entrega, ni suscripción, ni planificación, porque aquí nada es realmente asíncrono.

¿No está amañado por dónde se sitúa el disparador?

Pregunta justa, y lo único de este caso que es una decisión de criterio. Como la búsqueda se cortocircuita, es el conjunto de datos el que decide cuánto trabajo se hace: el benchmark coloca la primera transacción por encima del presupuesto al 90% del recorrido, así que una ejecución de un millón examina 900.001. Muévela y ambos lados trabajan proporcionalmente menos. Si la diferencia fuese un artefacto de esa elección, adelantar el disparador la cerraría.

No la cierra. Medido seguido en una máquina ociosa, cinco rondas cada uno, con el conjunto del 90% ejecutado dos veces para mostrar el ruido:

EscalaDisparadorExaminados RxDartFxDartFactor
10090%91 10 µs286 ns35×
10090% (repetición)91 11 µs280 ns39×
10050%51 7,5 µs256 ns29×
1.000.00090%900.001 79,1 ms861 µs92×
1.000.00090% (repetición)900.001 78,9 ms901 µs88×
1.000.00050%500.001 44,2 ms374 µs118×

Reducir el trabajo a la mitad reduce a la mitad ambos lados — y el factor se ensancha, de 88× a 118×. Divide las filas del millón entre los elementos examinados y la razón queda clara: RxDart cuesta 87,7, 87,9 y 88,4 ns por elemento en las tres ejecuciones, plano independientemente de dónde esté el disparador, mientras que FxDart cuesta 1,00, 0,96 y 0,75 ns — el recorrido más corto sale incluso algo más barato por elemento. Adelantar el disparador, si acaso, favorece a FxDart. Las filas de 100 elementos las domina el coste fijo de arranque y no el coste por elemento, por eso sus factores son menores y más ruidosos; y por eso mismo existe la escala grande.

Así que el 90% no está ahí para inflar nada — está ahí para que la escala grande mida el tirar de los datos y no el arranque, y es la posición menos favorable a FxDart de las que probamos. Lo que la posición no puede cambiar es el precio por elemento, y en eso consiste toda la diferencia.

Así que la lectura honesta de estas barras es estrecha: cuando la fuente ya está en memoria y la pregunta es síncrona, hacerla pasar por un stream es puro sobrecoste. Convierte la fuente en algo genuinamente asíncrono — un socket, un websocket, una API paginada — y ese coste por elemento desaparece bajo la E/S que el stream fue diseñado para gestionar, que es justo el terreno que cubre la Parte 4. El veredicto del código sigue siendo empate: ambos escriben el mismo cortocircuito con un operador, y ambos se detienen en el momento justo.

Benchmark

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

N = 100

Tiempo Empate

RxDart 11 µs
FxDart 293 ns

Memoria pico Empate

RxDart 17.1 MB
FxDart 16.3 MB

N = 1,000,000

Tiempo Gana FxDart

RxDart 78.8 ms
FxDart 870 µs

Memoria pico Gana FxDart

RxDart 122.4 MB
FxDart 111.8 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í.