prolly-map 0.3.0

Content-addressed versioned map storage primitives.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
# Prolly performance guide

This guide explains how `prolly-map` gets good performance today, how to benchmark it, and where future optimization work should focus. It replaces the old root-level performance note, so this file is the canonical performance document for the crate.

`prolly-map` is not a raw key-value store. It is an immutable, content-addressed ordered map. Performance depends on keeping tree rewrites local, batching store input/output (I/O), reusing unchanged content-addressed nodes, and choosing storage backends that match the workload.

## Performance goals

Use these goals when you evaluate a change:

- Preserve immutable snapshot semantics
- Keep single-key updates bounded to the affected leaf and ancestor path
- Keep append workloads on the right edge of the tree
- Reuse content identifiers (CIDs) for unchanged subtrees
- Skip identical subtrees during diff, merge, sync, and garbage collection (GC)
- Use ordered batch reads and batch writes whenever stores support them
- Keep hints and caches correctness-optional
- Keep large payloads out of hot tree nodes
- Support both sync embedded stores and async remote stores
- Make every benchmark report enough verification data to catch incorrect fast paths

The key tradeoff is deliberate. Prolly trees add content-addressed metadata, but they buy cheap snapshots, structural diff, merge, sync, retention, and reproducible roots.

## Mental model

Think about cost in three layers:

```text
logical operation -> tree work -> store work
```

Tree work includes path traversal, node decoding, chunk boundary checks, rebalancing, CID computation, and serialization. Store work includes reads, writes, ordered batch reads, transactions, durable sync, and remote latency.

The fastest path usually:

1. touches the fewest leaves
2. rewrites the fewest ancestor nodes
3. batches reads and writes
4. reuses existing CIDs
5. avoids loading large values during metadata operations

## Implemented optimizations

The crate already includes several performance features. They should remain correctness-neutral unless the API explicitly states otherwise.

### Content-defined locality

Content-defined chunking keeps tree shape stable across replicas and rebuilds. A small edit usually rewrites one leaf and its ancestors, while unchanged subtrees keep the same CIDs.

Relevant configuration:

- `min_chunk_size`: avoids undersized nodes
- `max_chunk_size`: caps node size and rewrite size
- `chunking_factor`: controls average node size
- `hash_seed`: makes boundary placement deterministic for a dataset

### Packed node encoding

Newly written nodes use a compact deterministic encoding. Legacy CBOR node reads remain supported by tests, which protects existing persisted data while reducing the size of newly written nodes.

Why it matters:

- smaller node bytes reduce store I/O
- fewer bytes improve cache density
- stable bytes preserve CID identity
- deterministic encoding enables future cross-language fixtures

### Append fast path

`append_batch` detects sorted append-only mutation batches and updates the right edge directly. This avoids rediscovering the rightmost path from the root for every append batch.

Implemented pieces:

- in-process rightmost-path cache
- persisted rightmost-path hints for hint-capable stores
- ordered batch reads to hydrate a hinted path in a fresh manager
- fallback to normal traversal when hints are absent, stale, or unsupported

Use this path for:

- event logs
- document chunk ingestion
- monotonic timestamp indexes
- append-heavy agent traces
- staged imports sorted by key

### Batch mutation planning

`Prolly::batch` sorts and coalesces mutations, routes them to affected leaves, applies each touched leaf once, rebuilds affected ancestors, and flushes rewritten nodes in a batch.

This reduces repeated path rewrites compared with applying many `put` or `delete` calls individually.

Batch paths also support:

- prefetching when stores prefer batch reads
- append-heavy right-edge optimization
- bottom-up rebuild strategies through `BatchWriterConfig`
- deferred mutation planning for large mutation sets

### Bulk builders

`BatchBuilder` and `SortedBatchBuilder` build full trees from large input sets. Use them for first imports, fixture generation, index rebuilds, and materialized view rebuilds.

Use `SortedBatchBuilder` when input is already ordered by key. That avoids sorting and matches database export or log-compaction workflows.

### Ordered batch reads

The `Store` trait supports `batch_get_ordered` and `batch_get_ordered_unique`. Stores with native multi-get, request coalescing, transactions, or remote parallelism should override these methods.

Tree operations use ordered reads in:

- batch mutation route planning
- diff subtree hydration
- async traversal frontiers
- rightmost-path hint hydration
- missing-node copy

Ordered reads are important because callers often need results in the same order as requested child CIDs.

### Batch writes and transactions

Stores can override `batch_put` and `batch_put_with_hint`. SQLite uses transactions for batched writes. Hint-capable stores can atomically write nodes plus the performance hint that points to them.

Good store behavior:

- node writes are idempotent
- batch writes are atomic when the backend supports it
- hints never make correctness depend on local metadata
- root manifest writes use compare-and-swap when multiple writers may race

### Node cache and pinning

Each `Prolly` or `AsyncProlly` manager has a decoded-node cache. `Config` can bound it by node count and serialized-byte budget.

Cache features:

- hit, miss, and eviction counters
- optional hot-root or hot-path pinning
- explicit cache clearing
- correctness fallback to the store on every miss

Use bounded caches in long-running services. Use pinning only for hot snapshots that you know will be queried repeatedly.

### Diff and merge pruning

Diff, range diff, merge, and sync compare CIDs before descending. Equal CIDs mean the whole subtree is equal.

Implemented pruning:

- equal root fast path
- equal child-CID pruning
- range-disjoint child span pruning
- ordered batch hydration of changed child frontiers
- streaming diff for large comparisons
- range-limited merge for partitioned keyspaces

This is the main reason content-addressing pays for itself in versioned applications.

### Large value offload

`put_large_value` stores large payload bytes in a `BlobStore` and keeps only a `ValueRef` in the tree. That keeps leaf nodes small and preserves fast metadata diff.

Use large value offload for:

- document bodies
- transcript chunks
- tool outputs
- generated artifacts
- binary attachments

### Async store concurrency

`AsyncStore` supports async point reads, ordered batch reads, and bounded default read parallelism. `AsyncProlly` uses those APIs for async reads, writes, ranges, diff, merge, stats, and batch mutation.

Async does not make CPU-bound work faster by itself. It helps when node reads wait on object stores, browser APIs, remote peers, or network caches.

See [Async store support](async-store.md) for the async design track.

## Workload guidance

Use this section as a tuning checklist before changing code.

### Append-heavy event logs

Best APIs:

- `append_batch`
- `Prolly::batch` with sorted append-only mutations
- `BatchBuilder` for initial log import

Key guidance:

- Put timestamp and sequence segments near the end of a stable run prefix
- Keep event values small and versioned
- Offload large tool outputs to blobs
- Persist named roots after meaningful checkpoints
- Use retention-aware GC after compaction

Expected behavior:

- closed left-side leaves should keep their CIDs
- fresh managers should use rightmost-path hints when the store supports hints
- appends should avoid full-tree traversal chains

### Random point updates

Best APIs:

- `put` for a few changes
- `batch` for many changes
- `get_many` when reading multiple known keys

Key guidance:

- Batch related mutations
- Keep chunk sizes large enough to avoid excessive tree height
- Bound caches for broad update workloads
- Use store batch reads when point updates touch many leaves

Expected behavior:

- each changed key rewrites its leaf and ancestors
- unrelated subtrees keep existing CIDs
- the number of touched leaves matters more than total tree size

### Range scans

Best APIs:

- `range`
- `range_page`
- `RangeCursor`

Key guidance:

- Design keys around scan prefixes
- Use `prefix_range`
- Use pages for API responses and background jobs
- Avoid scanning large values if metadata is enough

Expected behavior:

- range scans load only relevant leaf spans
- internal nodes route directly to matching child ranges
- page cursors let long scans resume without restarting

### Diff, sync, and derived indexes

Best APIs:

- `diff`
- `stream_diff`
- `range_diff`
- `plan_missing_nodes`
- `copy_missing_nodes`

Key guidance:

- Diff roots rather than decoded application objects
- Keep derived indexes in separate trees
- Apply source diffs to secondary indexes in a batch
- Use range-limited diff for tenants, documents, or partitions

Expected behavior:

- equal CIDs skip unchanged subtrees
- appended suffixes should avoid comparing every earlier key
- missing-node sync should copy only unknown CIDs

### Merge-heavy collaboration

Best APIs:

- `merge`
- `merge_range`
- `merge_explain`
- `stream_conflicts`
- `MergePolicyRegistry`

Key guidance:

- Route conflicts by key prefix
- Keep resolvers deterministic
- Return `Resolution::Unresolved` for policy-sensitive data
- Use `merge_explain` when debugging fallback or resolver behavior

Expected behavior:

- disjoint changes merge without value decoding
- conflicting keys call the resolver with delete-aware optional values
- resolved deletes fall back when rebalancing requires the batch path

### Durable embedded storage

Best APIs and features:

- `sqlite`
- `rocksdb`
- `slatedb`
- `pglite`
- named roots
- store-native GC

Key guidance:

- Use named roots for every durable head
- Use CAS for multi-writer root publication
- Use write-ahead log (WAL) mode and a non-zero busy timeout for SQLite
- Run GC from explicit retained roots
- Benchmark with realistic payload sizes and cache settings

Expected behavior:

- batch writes should use backend transactions when available
- ordered batch reads should reduce read amplification
- root manifests and hints should live outside content-node namespaces

### Remote or object storage

Best APIs:

- `AsyncStore`
- `AsyncProlly`
- `AsyncBlobStore`
- `plan_missing_nodes`
- `copy_missing_nodes`

Key guidance:

- Make node writes idempotent
- Use conditional writes for root manifests
- Override ordered batch reads when the backend has native multi-get
- Set bounded read parallelism when it only has point reads
- Cache hot internal nodes near the application

Expected behavior:

- concurrent child-node reads hide network latency
- content-addressed nodes can be uploaded before root publication
- GC, not ordinary mutation, should delete object-store nodes

## Configuration guide

Defaults are good for development. Production workloads should benchmark with data that resembles the application.

| Setting | Effect | Tune when |
| --- | --- | --- |
| `min_chunk_size` | Avoids tiny nodes | Trees have too many sparse nodes |
| `max_chunk_size` | Caps node size and rewrite size | Leaves become too large or tree height grows |
| `chunking_factor` | Controls average boundary frequency | You need fewer nodes or finer sharing |
| `hash_seed` | Makes boundaries deterministic | You need dataset-specific stable shape |
| `node_cache_max_nodes` | Bounds decoded node count | Services run for a long time |
| `node_cache_max_bytes` | Bounds approximate cache bytes | Values or nodes vary widely in size |
| `Encoding` | Records value encoding metadata | You need JSON, CBOR, raw, or custom markers |

Rules of thumb:

- Larger chunks reduce tree height and store calls, but each edit rewrites more bytes
- Smaller chunks improve fine-grained sharing, but increase metadata and traversal work
- Stable `hash_seed` is part of reproducible tree shape
- Cache limits should be explicit in long-running processes
- Large values should move to blob stores before tuning chunk sizes

## Store backend guidance

| Backend | Best fit | Performance notes |
| --- | --- | --- |
| `MemStore` | Tests, examples, benchmarks | Fastest local baseline, not durable |
| `FileNodeStore` | Object-layout local testing | Verifies node bytes against CIDs, useful for sync layout |
| `SqliteStore` | Desktop apps, local-first apps, agents | Use transactions, WAL, batch reads, and named roots |
| `RocksDBStore` | Embedded write-heavy services | Tune RocksDB separately for write buffers and compaction |
| `SlateDbStore` | Object-store style storage | Use async/object-store patterns and benchmark latency |
| `PgliteStore` | Postgres-like embedded experiments | Keep sidecar test gates explicit |
| Custom `Store` | Local sync backend | Implement ordered batch reads and batch writes |
| Custom `AsyncStore` | Remote, browser, object store | Implement native multi-get or bounded read parallelism |

Conformance tests should cover both correctness and performance hooks:

- absent reads
- present reads
- overwrite
- delete
- duplicate batch reads
- ordered batch reads
- batch writes
- hints
- node scans
- manifests
- async parity if applicable

## Benchmark harnesses

Run benchmarks from the workspace root.

### Core operation benchmark

`prolly_bench` covers core operations:

- incremental inserts
- bulk build
- point gets
- point updates
- deletes
- range scans
- batch mutations
- append batches
- diff and stream diff
- range diff
- merge
- in-memory core store behavior

Command:

```sh
PROLLY_BENCH_SCALE=5000 \
cargo bench -p prolly-map --bench prolly_bench
```

### AI and local-first workload benchmark

`ai_workloads_bench` covers application-shaped workloads:

- conversation event appends
- document chunk ingestion
- metadata updates across many prefixes
- agent memory branch and merge
- sync-style missing-node exchange

Command:

```sh
PROLLY_AI_BENCH_SCALE=10000 \
PROLLY_AI_BENCH_BATCH=256 \
cargo bench --bench ai_workloads_bench
```

### Store diff and merge benchmark

`store_diff_merge_bench` measures the built-in memory store on branch, diff,
and merge workloads. Backend-specific scale harnesses live with their adapter
crates.

Command:

```sh
PROLLY_DIFF_MERGE_STAGES=10000,100000 \
PROLLY_DIFF_MERGE_CHANGES=1000 \
cargo bench -p prolly-map --bench store_diff_merge_bench
```

### SQLite scale benchmark

`sqlite_scale_bench` appends staged record counts to a SQLite store and verifies reopen reads.

Command:

```sh
PROLLY_SQLITE_SCALE_STAGES=1000000,10000000,100000000 \
PROLLY_SQLITE_SCALE_BATCH=100000 \
PROLLY_SQLITE_SCALE_MAX_SECONDS=900 \
PROLLY_SQLITE_SCALE_MAX_DB_GB=70 \
cargo bench --manifest-path stores/prolly-store-sqlite/Cargo.toml --bench sqlite_scale_bench
```

### Optional backend scale benchmarks

The crate also includes:

- `pglite_scale_bench`
- `slatedb_scale_bench`
- `slatedb_ops_bench`
- `slatedb_workload_bench`

Run them from their adapter packages when their external dependencies are
configured.

### SlateDB production workload benchmark

`slatedb_workload_bench` is the recommended harness for pushing a SlateDB-backed
prolly map toward production-scale object-store workloads. It runs staged ingest,
hot and random reads, range scans, sparse updates, deletes, suffix appends,
streaming diff, disjoint three-way merge, named-root publication, object-store
size sampling, and final reopen verification.

Create the local RustFS bucket used by the default workload profile:

```sh
AWS_ACCESS_KEY_ID=crab \
AWS_SECRET_ACCESS_KEY=crab \
AWS_DEFAULT_REGION=us-east-1 \
aws --endpoint-url http://localhost:9000 \
  s3api create-bucket --bucket prolly
```

Small local verification:

```sh
PROLLY_SLATEDB_BUCKET=prolly \
PROLLY_SLATEDB_ACCESS_KEY_ID=crab \
PROLLY_SLATEDB_SECRET_ACCESS_KEY=crab \
PROLLY_SLATEDB_WORKLOAD_STAGES=10k \
PROLLY_SLATEDB_WORKLOAD_BATCH=5k \
PROLLY_SLATEDB_WORKLOAD_OPS=500 \
cargo bench --manifest-path stores/prolly-store-slatedb/Cargo.toml --bench slatedb_workload_bench
```

Large staged run:

```sh
PROLLY_SLATEDB_BUCKET=prolly \
PROLLY_SLATEDB_ACCESS_KEY_ID=crab \
PROLLY_SLATEDB_SECRET_ACCESS_KEY=crab \
PROLLY_SLATEDB_WORKLOAD_STAGES=10M,1B,10B \
PROLLY_SLATEDB_WORKLOAD_BATCH=100k \
PROLLY_SLATEDB_WORKLOAD_OPS=100k \
PROLLY_SLATEDB_WORKLOAD_CYCLES=1 \
PROLLY_SLATEDB_WORKLOAD_VALUE_BYTES=128 \
PROLLY_SLATEDB_WORKLOAD_MAX_SECONDS=86400 \
PROLLY_SLATEDB_WORKLOAD_MAX_OBJECT_GB=20000 \
PROLLY_SLATEDB_WORKLOAD_STATS_MAX_RECORDS=1M \
PROLLY_SLATEDB_FLUSH_AFTER_WRITE=false \
PROLLY_SLATEDB_FLUSH_INTERVAL_MS=1000 \
PROLLY_SLATEDB_L0_SST_MB=256 \
PROLLY_SLATEDB_MAX_UNFLUSHED_MB=4096 \
PROLLY_SLATEDB_L0_MAX_SSTS=256 \
PROLLY_SLATEDB_L0_MAX_SSTS_PER_KEY=32 \
PROLLY_SLATEDB_L0_FLUSH_PARALLELISM=16 \
PROLLY_SLATEDB_READ_PARALLELISM=512 \
PROLLY_SLATEDB_COMPACTION_CONCURRENCY=16 \
PROLLY_SLATEDB_COMPACTION_SUBCOMPACTIONS=8 \
cargo bench --manifest-path stores/prolly-store-slatedb/Cargo.toml --bench slatedb_workload_bench
```

Important controls:

- `PROLLY_SLATEDB_WORKLOAD_STAGES` accepts suffixes such as `10M`, `1B`, and
  `10B`.
- `PROLLY_SLATEDB_WORKLOAD_SOAK_SECONDS` keeps running mixed operation cycles
  after the initial stages finish. Use it for long-duration load tests.
- `PROLLY_SLATEDB_WORKLOAD_MAX_SOAK_CYCLES` is an additional safety cap for
  soak runs.
- `PROLLY_SLATEDB_WORKLOAD_OBJECT_SAMPLE_CYCLES` controls how often the harness
  lists the object-store prefix to refresh footprint metrics.
- `PROLLY_SLATEDB_WORKLOAD_MAX_SECONDS` and
  `PROLLY_SLATEDB_WORKLOAD_MAX_OBJECT_GB` are safety stops.
- `PROLLY_SLATEDB_WORKLOAD_KEEP_DB=true` preserves the object-store prefix for
  later inspection.
- `PROLLY_SLATEDB_FLUSH_AFTER_WRITE=false` measures sustained ingest through
  SlateDB's WAL and flush interval. Set it to `true` when measuring
  synchronous durability after every prolly batch.
- Increase `PROLLY_SLATEDB_WORKLOAD_STATS_MAX_RECORDS` only when a full
  tree-stats traversal is acceptable at that scale.

For serious production use, treat this as a capacity harness rather than a
guarantee. Size the object store and network for the configured value size and
write amplification, keep durable heads in named roots, isolate independent
writer shards into separate SlateDB paths, keep large payloads in blob storage
instead of leaf values, and tune L0/compaction settings until read amplification
and write backpressure stay flat through the largest stage.

### 100M initial version plus soak test

Use this profile when you want to build a 100M-record initial version, publish
it as `main`, then keep simulating production-like changes over time:

```sh
PROLLY_SLATEDB_BUCKET=prolly \
PROLLY_SLATEDB_ACCESS_KEY_ID=crab \
PROLLY_SLATEDB_SECRET_ACCESS_KEY=crab \
PROLLY_SLATEDB_WORKLOAD_STAGES=100M \
PROLLY_SLATEDB_WORKLOAD_BATCH=100k \
PROLLY_SLATEDB_WORKLOAD_OPS=100k \
PROLLY_SLATEDB_WORKLOAD_CYCLES=0 \
PROLLY_SLATEDB_WORKLOAD_SOAK_SECONDS=21600 \
PROLLY_SLATEDB_WORKLOAD_MAX_SOAK_CYCLES=10000 \
PROLLY_SLATEDB_WORKLOAD_OBJECT_SAMPLE_CYCLES=10 \
PROLLY_SLATEDB_WORKLOAD_VALUE_BYTES=128 \
PROLLY_SLATEDB_WORKLOAD_MAX_SECONDS=25200 \
PROLLY_SLATEDB_WORKLOAD_MAX_OBJECT_GB=200 \
PROLLY_SLATEDB_WORKLOAD_STATS_MAX_RECORDS=1M \
PROLLY_SLATEDB_WORKLOAD_KEEP_DB=true \
PROLLY_SLATEDB_FLUSH_AFTER_WRITE=false \
PROLLY_SLATEDB_FLUSH_INTERVAL_MS=1000 \
PROLLY_SLATEDB_L0_SST_MB=256 \
PROLLY_SLATEDB_MAX_UNFLUSHED_MB=4096 \
PROLLY_SLATEDB_L0_MAX_SSTS=256 \
PROLLY_SLATEDB_L0_MAX_SSTS_PER_KEY=32 \
PROLLY_SLATEDB_L0_FLUSH_PARALLELISM=16 \
PROLLY_SLATEDB_READ_PARALLELISM=512 \
PROLLY_SLATEDB_COMPACTION_CONCURRENCY=16 \
PROLLY_SLATEDB_COMPACTION_SUBCOMPACTIONS=8 \
cargo bench --manifest-path stores/prolly-store-slatedb/Cargo.toml --bench slatedb_workload_bench
```

This command requires real capacity. With 128-byte values, the append-only 1M
local measurement was roughly 268 bytes per record in object storage before
long-lived history, so 100M needs about 27 GB before soak-cycle write
amplification. Mixed updates and retained roots can raise this materially; use
the object GB cap as a guardrail and keep the database prefix for post-run
inspection only when the backing store has enough space.

The generated records use deterministic pseudo-random payload bytes with
ordered primary keys so the initial import represents a sorted bulk load. The
over-time workload chooses random records for reads, updates, deletes, diffs,
and merges. For truly random primary keys, sort the source records before the
initial build or run a separate random-insert profile; otherwise the benchmark
will measure random insert amplification rather than bulk-load capacity.

## Reading benchmark output

Most benchmark rows include:

- operation name
- record count
- change count
- total time
- items per second or nanoseconds per item
- verification flag
- backend-specific size or status fields

Trust rows where `verified` is true. A fast row without verification is only a timing probe.

Compare results by workload, not only by raw throughput. A point-get benchmark, append benchmark, sparse update benchmark, and full rebuild benchmark answer different questions.

## Current evidence

The following figures are local measurements from the existing benchmark notes. Treat them as directionally useful, not universal claims. Re-run the harnesses on your target hardware and backend before setting production limits.

### SQLite disk results at scale 5000

| Bench | ns/item | Approx items/sec |
| --- | ---: | ---: |
| `sqlite_disk_batch_builder_persist` | 1146 | 872k |
| `sqlite_disk_batch_mutations` | 9686 | 103k |
| `sqlite_disk_append_batch_persist` | 1677 | 596k |
| `sqlite_disk_append_batch_chain_persist` | 3727 | 268k |
| `sqlite_disk_append_batch_chain_new_manager_persist` | 4493 | 223k |
| `sqlite_disk_diff_empty_to_full` | 342 | 2.92M |

### SQLite staged append results

| Target records | Total records | Total time ms | DB bytes | Bytes/record | Stage records/sec | Verified |
| ---: | ---: | ---: | ---: | ---: | ---: | :--- |
| 1,000,000 | 1,000,000 | 1554 | 135,456,400 | 135.46 | 643k | yes |
| 10,000,000 | 10,000,000 | 22,405 | 1,195,430,128 | 119.54 | 432k | yes |
| 100,000,000 | 100,000,000 | 606,434 | 11,721,597,960 | 117.22 | 154k | yes |

At the measured 100M footprint, a 1B-record SQLite database would require roughly 117 GB before write-ahead log, filesystem, and safety headroom.

## Performance playbooks

### Import a dataset

1. Sort records by key if the source can do it cheaply
2. Use `SortedBatchBuilder` for sorted input
3. Use `BatchBuilder` for unsorted input
4. Publish the resulting root by name
5. Record `collect_stats` output with the import job

### Maintain a secondary index

1. Keep source records in one tree
2. Keep the index in another tree
3. Diff source roots
4. Translate source diffs into index mutations
5. Apply mutations with `batch`
6. Store source and index roots in a manifest value

### Sync a remote peer

1. Compare root CIDs
2. Plan missing CIDs against the destination store
3. Copy missing nodes
4. Verify copied bytes against CIDs
5. Publish the received root after all nodes exist

### Serve RAG metadata

1. Store chunk metadata in the tree
2. Store chunk text in blobs when it is large
3. Store vectors in a sidecar vector engine
4. Save the prolly root in each answer record
5. Replay old answers against the saved root

### Run in an async application

1. Use `AsyncStore` for remote or browser storage
2. Use `TokioBlockingStore` only when wrapping blocking sync stores in Tokio
3. Override ordered batch reads for native multi-get
4. Limit read parallelism for remote stores
5. Keep root publication conditional

## Known limits

Prolly trees are not the fastest choice for every problem.

Use another structure when:

- you only need an append-only log with no diff, merge, or sync
- you never need immutable snapshots
- you need analytical scans over every row for every query
- values are huge and cannot be separated into blobs
- storage format stability matters more than pre-`1.0` API cleanup

Current technical limits:

- node encoding compatibility still needs fixture files before language ports can claim structural compatibility
- remote object-store backends are design patterns, not first-party implementations yet
- benchmark gates are local scripts rather than a published performance dashboard
- some root-level docs still describe broader Trail performance rather than `prolly-map` alone

## Future enhancements

This section tracks likely performance work after the current hardening.

### P0: release confidence

- Add a reproducible benchmark smoke suite to CI
- Add a `cargo bench` result parser for threshold checks
- Add fixture-backed performance tests for append, diff, and merge fast paths
- Add docs for interpreting `collect_stats`, `stats_diff`, and manager metrics
- Add compatibility fixtures before making format-level performance promises

### P1: store and cache improvements

- Add shared cache options for multiple managers using the same store
- Add request coalescing for repeated CID reads
- Add cache admission policies for broad scans
- Add prepared-statement reuse experiments for long SQLite read batches
- Add backend-specific tuning docs for SQLite, RocksDB, SlateDB, and PGlite
- Add store-size and reclaimable-byte reports to inspection tooling
- Add async prefetch strategies for wide diff and range traversals

### P1: mutation and merge improvements

- Add a direct subtree-splice builder for large middle-range insertions where neighboring boundaries are known
- Add more direct structural delete paths when rebalancing is provably unnecessary
- Add range-partitioned batch planning for large mutation sets
- Add benchmark cases for large delete ranges
- Add merge benchmarks for long-lived divergent branches
- Add policy-registry benchmark coverage for many key-family rules

### P1: remote and object-store improvements

- Add first-party object-store node backend
- Add async manifest store with conditional root writes
- Add bounded background prefetch for hot internal nodes
- Add retry and backoff guidance for remote stores
- Add sync progress metrics
- Add object-store GC dry-run reports
- Add remote blob store examples

### P2: encoding and layout research

- Evaluate prefix compression inside nodes
- Evaluate optional node compression for durable stores
- Evaluate zero-copy node decode views for read-heavy workloads
- Evaluate adaptive chunking parameters by workload
- Evaluate Bloom-filter or summary sidecars for negative lookups
- Evaluate content-defined chunking variants from current prolly-tree research

Any encoding or layout change must preserve a migration path. Once cross-language fixtures exist, performance work that changes node bytes must update those fixtures intentionally.

## Further reading

- [Async store support]async-store.md
- [Architecture]architecture.md
- [Design spec]design-spec.md
- [Implementation notes]implementation.md
- [Cookbook]cookbook.md
- [Trail performance and scale benchmarks]../../../docs/guides/performance-and-scale-benchmarks.md
- [Accelerating Prolly Trees: Simplified Chunking for Rapid Updates]https://ceur-ws.org/Vol-3791/paper8.pdf