Concepts

Performance

Why incremental maintenance is sub-microsecond and stays flat as data grows — measured end to end over a real dataset — plus the COW B-tree numbers against an equivalent JavaScript implementation.

The whole point of incremental view maintenance is that the cost of a write tracks the change, not the table. This page is the measured version of that claim — real numbers, on a real dataset, with the methodology you can rerun.

The headline: on the in-process engine — the same one that runs in the browser via @rindle/wasm and on the local-first client — an incremental update costs a few hundred nanoseconds to a few microseconds, and stays essentially flat as the dataset grows 30×.

Incremental maintenance is sub-microsecond — and flat

Measured over the Chinook dataset, scaled 1× / 10× / 30× (up to ~124k source rows), applying one row change to a materialized view and reading the result:

Pattern Per write (in-process engine) As data grows 30×
filtered match ~430 ns flat
ordered top-N (limit) ~420 ns flat
nested child (sub) ~2.5 µs flat
flipped EXISTS ~880 ns flat

The number that matters is the last column. A push_nested_child costs ~2.5 µs at 6.8k rows and ~4 µs at 124k — it does not grow with the table. That is the IVM contract made physical: you pay for the delta, not the dataset.

Compare that to the alternative everyone reaches for first — re-running the query on every write. A from-scratch hydrate of the same view is in the hundreds of microseconds to low milliseconds range and grows with the data. Incremental maintenance replaces that per-write recompute with a sub-microsecond delta. The gap widens every time the table does.

A small, embeddable engine

The engine is built to go anywhere, so the footprint is part of the contract:

  • ~228 kB gzipped — the entire engine, compiled to WebAssembly, running in-process in a browser tab. No server, no worker pool, no round-trip.
  • std-only core — the Rust engine (rindle) has no required C toolchain and no heavy dependencies; it embeds directly in a binary.
  • Single-threaded by design — one engine per thread (!Send), so there are no locks on the hot path. You scale with independent engines and message passing, not a shared mutex.

The data structure underneath: faster than its JavaScript equivalent

The hottest structure in Rindle’s engine — the copy-on-write B-tree that backs every index and view — was benchmarked 1:1 against an equivalent JavaScript BTreeSet, same operations, same data shape:

  • Rust wins 13 of 18 operations.
  • Iteration 1.45–2.13× faster — the per-row hot path for every view. The lending cursor vends a borrowed row with zero allocation per row.
  • Index build / sort 1.05–2.32× faster — building an index from rows.
  • Copy-on-write fork 2.37× faster — one refcount bump vs. a marked clone.
  • Realistic string-key lookups 1.84–1.97× faster.

The five operations where V8 edges ahead are all trivial integer point-lookups, where a small-integer fast path JITs to a register compare — tens of nanoseconds, and noisy run-to-run. On everything that dominates at scale, Rust wins.

Reproduce it

Both benchmarks are in the repository and runnable:

# end-to-end IVM matrix over Chinook (hydration + incremental push, both leaves)
cargo run -p rindle-sqlite --release --features fast-alloc --example bench_chinook_rs

# the COW B-tree, Rust vs the equivalent JavaScript implementation
cargo run --release --features fast-alloc --example bench_rs

See CHINOOK-PERF.md and BENCHMARKS.md for the full matrices, the timing harness, and the methodology.

Honesty about the edges

The numbers above are the in-process engine — the one the browser and local-first clients run. Two caveats, both documented in CHINOOK-PERF.md:

  • Hydration is not incremental. Materializing a view from scratch is proportional to the result it produces (hundreds of µs to low ms). It’s the steady-state push that’s sub-microsecond. Most apps hydrate once and push forever.
  • One pattern needs index stats. A correlated EXISTS against the SQLite-backed replica hydrates quadratically until you run ANALYZE on the database — a one-line, data-driven fix that returns it to linear. The in-process engine is unaffected.

Next steps