# between — benchmark

Workload: 10_000 rows; range [25, 75] on `val`. Tick rewrites one row's
`val` (potentially crossing a bound). Batch streams 1000 such ticks.
Generated by [comparisons/bench/operators/between.bench.ts](../../comparisons/bench/operators/between.bench.ts).

| Library | Setup (ms) | Single (ms) | vs data | Batch 1000 (ms) | vs data |
|---|---:|---:|---:|---:|---:|
| **data** | 9.66 | **0.037** | — | **1.88** | — |
| svelte-store | 1.05 | 0.400 | 10.8× | 453.39 | 241× |
| rxjs | 1.02 | 0.402 | 10.9× | 415.28 | 221× |
| react | 1.45 | 0.499 | 13.5× | 511.31 | 272× |
| preact-signals | 3.22 | 0.925 | 25.0× | 42.78 | 22.8× |
| crossfilter | 5.20 | 1.50 | 40.5× | 398.40 | 212× |
| solid | 13.96 | 2.18 | 58.9× | 178.61 | 95.0× |
| mobx | 109.16 | 3.90 | 105× | 301.95 | 161× |
| vue-reactivity | 12.76 | 7.61 | 206× | 1027.83 | 547× |



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

## How

[index.ts](index.ts) splits the work cleanly between source-mutation and
bound-mutation events:

- BU2 (a row's col value changes) is now O(1): emits BI0/BR1/BU1 based on
  the row's before/after membership in `view.value`, sets `sortedDirty =
  true`, and returns. No splice into the sorted index — that's deferred.
- `set extent` (the user dragged a brush) calls `_resort()` if dirty
  before walking sorted from the bound's current bisect position. Sort
  rebuild is O(N log N), but it only fires when a bound actually changes
  — bound changes are rare (one per brush frame) relative to attribute
  updates (thousands per stream second).

Previously every BU2 paid O(N) for `sorted.indexOf` + two `splice`s,
which dominated this bench at ~82ms for the batch. Deferring brings it to
10ms while keeping `set extent` (the crossfilter brushing path) correct
via the dirty flag.

- BI0/BR1 (source insert/remove) now defer the same way (P1): they make
  the membership decision (`_inRange`, which needs only `lo_val`/`hi_val`)
  and write `view.value`, then set `sortedDirty = true` — they never touch
  `sorted`. The old incremental maintenance paid O(N) per row (object:
  `sorted.indexOf` + `splice`; array: a key-shift loop, plus an O(N²)
  key-shift recompute for batch removes). This is the births/deaths
  workload — an object-keyed population streaming inserts/removes with
  brushes only occasionally. Measured on a 10k object source: **remove
  churn 1000 rows 105.87ms → 0.91ms (~116×)**, insert churn 7.54ms →
  2.48ms (~3×); the `narrow/widen` brush path is unchanged (it does no
  inserts, so `sortedDirty` is never set and no `_resort` fires). For an
  array source the `view.value` splice that mirrors the source's
  positional shift is inherent to arrays and stays O(N); only the
  redundant sorted bookkeeping is shed. This deferral was first landed in
  `1d3bc15`, reverted in `105cfc7` because the dropped `sortedDirty → XU0`
  bailout had been masking the C8 spurious-`BR1` bug, and re-landed here
  once C8 was fixed at its root (the masking heal is no longer needed) and
  guarded by `between→length`/`sum`/`avg` differential scenarios.

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