# distinct — benchmark

Workload: 10_000 rows, 10 categorical buckets; tick rewrites one row's
`cat` (forces a bucket migration). Batch streams 1000 such ticks.
Generated by [comparisons/bench/operators/distinct.bench.ts](../../comparisons/bench/operators/distinct.bench.ts).

| Library | Setup (ms) | Single (ms) | vs data | Batch 1000 (ms) | vs data |
|---|---:|---:|---:|---:|---:|
| **data** | 2.81 | **0.014** | — | **1.59** | — |
| svelte-store | 1.09 | 0.387 | 27.6× | 435.11 | 274× |
| rxjs | 1.10 | 0.405 | 28.9× | 414.56 | 261× |
| react | 1.57 | 0.522 | 37.3× | 460.82 | 290× |
| preact-signals | 2.74 | 0.543 | 38.8× | 51.47 | 32.4× |
| solid | 3.60 | 1.73 | 124× | 180.19 | 113× |
| mobx | 103.55 | 6.70 | 479× | 624.88 | 393× |
| vue-reactivity | 31.28 | 17.37 | 1241× | 1544.65 | 971× |



data is fastest on both single (6.8x over svelte-store) and batch
(8.2x over preact-signals).

## How

[index.ts](index.ts) maintains a per-projection-key count plus a
`name → projection` map. BI0 bumps or admits a bucket (O(1) when the
bucket already exists, otherwise one push to the output array). BU2 looks
up the row's previous projection from the map, decrements the old
bucket, increments the new bucket, and edits the output array only when
a bucket transitions between count 0 and count >0.

BR1 / BU1 / XU0 still rebuild because the existing test suite encodes a
"first-seen order tracks current source iteration order" semantic that
isn't expressible as O(1) edits on remove — removing the row that
supplied a bucket's first instance can re-order other buckets. The
common workloads (BI0-heavy ingestion, BU2-heavy attribute rewrites in
brushable charts) stay on the incremental path.

Run `BENCH_OPS=distinct npm run bench:ops` to refresh — update this file
when the numbers change materially.
