# Projections

> **Preview** — Ranvier is pre-release; its APIs may change.

Projections keep keyed, per-row reactive state over a collection source. Selectors (`Lookup`) track, per key, whether that key matches a source value. See [Benchmarks](index.md) for configuration, environment and caveats.

## ProjectionBenchmarks

A projection over `Items` rows (8, 64 or 512), observed by one effect so that the projection stays scheduled.

| Method | What it measures |
| --- | --- |
| `ReadRow` | A tracked read of one cached row. |
| `EditOneItem` | A write that changes the value of the first row, re-running that row's effect. |
| `Reorder` | A write of the same keys and values in reverse order: the key order changes and every row keeps its value. |

Recorded 2026-09-28. Source: `Ranvier.Benchmarks.Projections.ProjectionBenchmarks` at commit `d87920f`.

| Method      | Items | Mean         | Error       | StdDev     | Op/s          | Gen0   | Gen1   | Allocated |
|------------ |------ |-------------:|------------:|-----------:|--------------:|-------:|-------:|----------:|
| **ReadRow**     | **8**     |     **6.184 ns** |   **0.0715 ns** |  **0.0669 ns** | **161,707,225.4** |      **-** |      **-** |         **-** |
| EditOneItem | 8     |   214.629 ns |   2.3013 ns |  2.1527 ns |   4,659,197.7 | 0.0057 |      - |      96 B |
| Reorder     | 8     |   183.185 ns |   2.5630 ns |  2.1403 ns |   5,458,957.0 | 0.0210 |      - |     352 B |
| **ReadRow**     | **64**    |     **6.013 ns** |   **0.0744 ns** |  **0.0696 ns** | **166,315,149.2** |      **-** |      **-** |         **-** |
| EditOneItem | 64    |   925.895 ns |  11.0672 ns | 10.3522 ns |   1,080,035.8 | 0.0057 |      - |      96 B |
| Reorder     | 64    | 1,140.921 ns |  10.3918 ns |  9.2121 ns |     876,485.0 | 0.1411 |      - |    2368 B |
| **ReadRow**     | **512**   |     **6.005 ns** |   **0.0735 ns** |  **0.0688 ns** | **166,522,611.8** |      **-** |      **-** |         **-** |
| EditOneItem | 512   | 6,653.607 ns |  45.0946 ns | 35.2069 ns |     150,294.4 |      - |      - |      96 B |
| Reorder     | 512   | 8,307.363 ns | 104.3172 ns | 92.4745 ns |     120,375.1 | 1.0986 | 0.0916 |   18496 B |

## Key churn and key readers

A key reader (`Projection.NewKeyReader`) reports the keys added, removed and moved since its previous read. These cases measure a write that removes one key and adds another, and what a reader adds to it.

`ProjectionBenchmarks.ChurnOneKey` runs on the fixture above's `Items`: one effect observes each row except the last, and one observes `Keys`.

| Method | What it measures |
| --- | --- |
| `ChurnOneKey` | A write removing the source's last key and adding a key the source has never held, with no key reader. |

`DeltaReaderChurnBenchmarks` is `ChurnOneKey` with `Readers` key readers (1 or 4), each read by its own effect after every write, at `Items` 8, 64 or 512.

| Method | What it measures |
| --- | --- |
| `ChurnOneKey` | The same write, then one read per reader. The difference from `ProjectionBenchmarks.ChurnOneKey` is the readers' cost. |

`DeltaReaderBenchmarks` compares two ways of finding one change. The source toggles key `Items / 2` out and back in on every write, at `Items` 64, 512 or 10 000.

| Method | What it measures |
| --- | --- |
| `SetDiffAfterOneRemoval` | Baseline. The toggle, then a set difference of the previous and current `Keys`: linear in `Items`. |
| `ReadAfterOneRemoval` | The toggle, then the change read from the key reader. |
| `ReadIdle` | A read of the key reader with nothing changed since the previous read. |

The counter scenario `project-churn` measures the same write over 1000 rows, with no key reader and with one read by an effect. Figures are retired instructions per operation at commit `e13f159`, .NET 10 with tiered compilation and PGO off (see [Instruction counts](counters.md)):

| Variant | instr/op | bytes/op | Library counters/op |
| --- | ---: | ---: | --- |
| No key reader | 539,775 | 4,592 | SignalsCreated 1, MemosCreated 1, EffectRuns 1, Flushes 1 |
| One key reader | 541,276 | 4,992 | SignalsCreated 1, MemosCreated 1, EffectRuns 2, Flushes 1 |

A key reader adds about 1,500 instructions and 400 bytes to the churn pass, about 0.3 %. The BenchmarkDotNet cases, including `DeltaReaderBenchmarks` and four readers, are in the suite for local runs (`dotnet run --project bench/Ranvier.Benchmarks -c Release -- --filter "*ChurnOneKey*" "*DeltaReader*"`) and have no published figure yet. The full report is [`e13f159.md`](https://github.com/shayanhabibi/Ranvier/blob/master/docs/.ai/benchmarks/counters/e13f159.md).

## LookupBenchmarks

A selector created with `createSelector`, whose key 7 loses its last observer and is read again before the next transition. Each case should reuse the key's cell; a regression would rebuild it.

| Method | What it measures |
| --- | --- |
| `RemountKey` | Mounting and disposing the only reader of a key. |
| `ReadAheadOfGet` | Re-running a reader that inserts or drops a read ahead of its `Get`. |
| `UntrackedGet` | An untracked read of an unobserved key. |

Recorded 2026-09-28. Source: `Ranvier.Benchmarks.Projections.LookupBenchmarks` at commit `d87920f`.

| Method         | Mean      | Error    | StdDev   | Op/s         | Gen0   | Allocated |
|--------------- |----------:|---------:|---------:|-------------:|-------:|----------:|
| RemountKey     | 101.88 ns | 1.721 ns | 1.525 ns |  9,815,568.7 | 0.0257 |     432 B |
| ReadAheadOfGet |  54.37 ns | 0.434 ns | 0.406 ns | 18,392,960.7 | 0.0014 |      24 B |
| UntrackedGet   |  21.15 ns | 0.170 ns | 0.159 ns | 47,272,722.8 | 0.0014 |      24 B |
