···1818just bench blob
1919just bench metastore 10 1000
2020just bench get-cid 10 1000
2121+just bench get-block 10 1000
2222+just bench decode-record 10 1000
2323+just bench render-record 10 1000
2424+just bench get-record 10 1000
2525+just bench list-records 10 1000
2626+just bench official-pds
2127just bench write-profile 10 500
2228just bench run --scenario write --records 10000
2329```
···3238- `repo`: full repo CAR materialization through `writeRepoCar`.
3339- `blob`: blob write/read against disk blobstore plus SQLite metadata.
3440- `metastore`: Tranquil-shaped apply/get/list benchmark with caller counts and
3535- latency percentiles.
4141+ latency percentiles. Its get/list rows are ZDS-local probes, not all direct
4242+ Tranquil comparisons.
3643- `get-cid`: CID-only record lookup, matching Tranquil's `get_record_cid`
3744 metastore benchmark.
4545+- `get-block`: record index lookup plus `repo_blocks` byte fetch.
4646+- `decode-record`: DAG-CBOR decode of one seeded record block, without SQLite.
4747+- `render-record`: DAG-CBOR decode plus JSON rendering, without SQLite.
4848+- `get-record`: full `com.atproto.repo.getRecord` storage materialization.
4949+- `list-records`: full `com.atproto.repo.listRecords` storage materialization.
5050+- `official-pds`: copies the official-PDS read probe into a local
5151+ `bluesky-social/atproto` checkout and runs it with Vitest.
3852- `write-profile`: `applyWrites` stage timing under write concurrency.
39534054Each run uses temporary SQLite and blobstore state.
···5165- Do not compare paths that perform different correctness work. For example,
5266 CID-only metadata lookup is separate from full `getRecord` materialization.
53676868+## traffic model
6969+7070+For a small hosted PDS, the hot paths are not exotic. A hundred active accounts
7171+mainly means more session checks, appview-proxied feed/profile reads, record
7272+writes from clients and apps, blob uploads, sync subscribers, crawl requests,
7373+and occasional repo export/import. The storage benchmarks should therefore keep
7474+these domains separate:
7575+7676+- record index: current CID lookups, existence checks, and index-only lists.
7777+- record materialization: index lookup, block fetch, DAG-CBOR decode, and JSON
7878+ response rendering.
7979+- write path: validation, repo mutation, commit/block persistence, event
8080+ publication, and crawl notification.
8181+- blob path: upload body ingest, disk write, metadata persistence, and readback.
8282+- sync path: event delivery and repo CAR materialization.
8383+5484## current local baseline
55855686On this machine, with `--records 1000` unless noted:
···6292| write repo car | 252 ops/s, 440 MB/s |
6393| blob put+get | 368 ops/s, 46 MB/s |
64946565-## Tranquil comparison
9595+## reference matrix
66966797Run Tranquil's metastore benchmark:
6898···80110just bench get-cid 100 200
81111```
821128383-This table only includes operation/count pairs measured in both implementations.
8484-The operations are matched to Tranquil's metastore bench: write one record, look
8585-up one record CID, and list records from a seeded repo.
113113+This matrix only includes operation/count pairs measured with the same unit of
114114+work. Tranquil's `get_record_cid` and `list_records` are record-index
115115+operations: `list_records` returns `rkey + record_cid`, not record body JSON.
116116+The official PDS probe measures its actor-store read paths with Vitest. That is
117117+apples-to-apples for the internal read operations, but not for concurrency
118118+curves: Vitest reports a single async benchmark loop, while ZDS and Tranquil
119119+also have explicit caller-count runs.
120120+121121+Summary: ZDS is fastest in the single-caller read probes, Tranquil is fastest in
122122+the 10/100 caller CID lookup probes, and the official PDS is slower in these
123123+microbenchmarks because the probe goes through its actor-store/test-network
124124+stack rather than a bare SQLite reader. For writes, official PDS is faster than
125125+ZDS in the single-caller row, Tranquil is faster than ZDS at 10 callers, and ZDS
126126+is faster than Tranquil at 100 callers but with a worse p99/max tail.
127127+128128+| operation | callers | zds | tranquil | official PDS |
129129+|---|---:|---:|---:|---:|
130130+| apply one record commit | 1 | 642 ops/s, p95 2.9 ms | 287 ops/s, p95 4.1 ms | 486 ops/s, p99 3.9 ms |
131131+| current record CID lookup | 1 | 414k ops/s, p95 4 us | 96.8k ops/s, p95 15 us | 3.2k ops/s, p99 477 us |
861328787-Summary: Tranquil is faster on the read rows after ZDS moved record materialize
8888-paths to decode DAG-CBOR from `repo_blocks`: about 1.5-1.8x faster for CID
8989-lookup and about 1.6x faster for list records. Tranquil is also faster on
9090-10-caller writes, by about 2.8x. ZDS is faster on the 100-caller write row, by
9191-about 1.3x, which is the main sign that the sharded write lane is helping under
9292-contention.
133133+The current caller-count concurrency matrix is only ZDS and Tranquil until the
134134+official-PDS probe grows explicit concurrent workers.
9313594136| operation | callers | ops | zds | tranquil |
95137|---|---:|---:|---:|---:|
9696-| apply/write | 10 | 10000 | 282 ops/s, p95 93.8 ms | 780 ops/s, p95 14.1 ms |
9797-| apply/write | 100 | 20000 | 1357 ops/s, p95 66.7 ms | 1007 ops/s, p95 109 ms |
9898-| get record CID | 10 | 10000 | 296k ops/s, p95 6 us | 455k ops/s, p95 43 us |
9999-| get record CID | 100 | 20000 | 294k ops/s, p95 405 us | 537k ops/s, p95 233 us |
100100-| list records | 10 | 10000 | 22k ops/s, p95 52 us | 36k ops/s, p95 310 us |
101101-| list records | 100 | 20000 | 23k ops/s, p95 49 us | 36k ops/s, p95 2.9 ms |
138138+| apply one record commit | 10 | 10000 | 259 ops/s, p95 98.9 ms | 780 ops/s, p95 14.1 ms |
139139+| apply one record commit | 100 | 20000 | 1223 ops/s, p95 67.5 ms | 1007 ops/s, p95 109 ms |
140140+| current record CID lookup | 10 | 10000 | 222k ops/s, p95 225 us | 455k ops/s, p95 43 us |
141141+| current record CID lookup | 100 | 20000 | 211k ops/s, p95 887 us | 537k ops/s, p95 233 us |
142142+143143+Tranquil's metastore `list_records` benchmark is not in this table because ZDS
144144+does not currently expose an equivalent "list rkeys and CIDs only" benchmark.
145145+ZDS's `listRecords` follows the official Bluesky PDS read shape: join the record
146146+index to `repo_blocks`, decode DAG-CBOR, and return record values.
147147+148148+## official PDS probe
149149+150150+The official-PDS probe lives at `bench/official-pds-records.bench.ts` and is
151151+run with:
152152+153153+```sh
154154+ATPROTO_REPO=/tmp/bluesky-atproto just bench official-pds
155155+```
156156+157157+It seeds 1000 `app.bsky.feed.post` records through the official test network,
158158+then measures actor-store reads directly:
159159+160160+| operation | official PDS |
161161+|---|---:|
162162+| apply one record commit | 486 ops/s, p99 3.9 ms |
163163+| current record CID lookup | 3.2k ops/s, p99 477 us |
164164+| full getRecord materialization | 3.1k ops/s, p99 469 us |
165165+| full listRecords, limit 50 | 639 ops/s, p99 2.3 ms |
166166+167167+These are useful for the official-PDS column, but they should not be mixed with
168168+ZDS's caller-count rows. The matching ZDS single-caller rows are:
102169103103-Tranquil's read row is `get_record_cid`, so ZDS reports `get-cid` for that
104104-comparison. Full `com.atproto.repo.getRecord` is a separate local probe because
105105-it joins the record index to `repo_blocks`, decodes DAG-CBOR, and renders the
106106-XRPC JSON response shape. Tranquil's metastore bench intentionally measures a
107107-metadata lookup for the current CID, not record body materialization.
170170+| operation | zds |
171171+|---|---:|
172172+| apply one record commit | 642 ops/s, p95 2.9 ms |
173173+| current record CID lookup | 414k ops/s, p95 4 us |
174174+| full getRecord materialization | 48.6k ops/s, p95 28 us |
175175+| full listRecords, limit 50 | 21.6k ops/s, p95 60 us |
108176109177## local probes
110178···112180113181| operation | callers | ops | zds |
114182|---|---:|---:|---:|
115115-| full getRecord | 10 | 10000 | 40k ops/s, p95 352 us |
116116-| full getRecord | 100 | 20000 | 39k ops/s, p95 8.2 ms |
183183+| get record block | 10 | 10000 | 128k ops/s, p95 11 us |
184184+| get record block | 100 | 20000 | 121k ops/s, p95 15 us |
185185+| decode DAG-CBOR | 10 | 10000 | 40.6M ops/s, p95 below timer resolution |
186186+| decode DAG-CBOR | 100 | 20000 | 12.6M ops/s, p95 below timer resolution |
187187+| render record JSON | 10 | 10000 | 24.8M ops/s, p95 below timer resolution |
188188+| render record JSON | 100 | 20000 | 12.4M ops/s, p95 1 us |
189189+| full getRecord | 10 | 10000 | 38.4k ops/s, p95 173 us |
190190+| full getRecord | 100 | 20000 | 38.1k ops/s, p95 11.7 ms |
191191+| full listRecords, limit 50 | 10 | 10000 | 21.1k ops/s, p95 57 us |
192192+| full listRecords, limit 50 | 100 | 20000 | 19.5k ops/s, p95 78 us |
193193+194194+The read-path split says SQLite serialization and row materialization dominate
195195+single-record reads. Decode/render is effectively invisible for tiny benchmark
196196+records; it needs a larger-record probe before we can extrapolate image-heavy or
197197+rich-record traffic.
117198118199## write profile
119200···139220- [`crates/tranquil-store/benches/blockstore.rs`](https://tangled.org/tranquil.farm/tranquil-pds/tree/main/crates/tranquil-store/benches/blockstore.rs)
140221- [`crates/tranquil-store/benches/eventlog.rs`](https://tangled.org/tranquil.farm/tranquil-pds/tree/main/crates/tranquil-store/benches/eventlog.rs)
141222142142-The first apples-to-apples comparison should cover:
223223+Official PDS source for matching probes:
224224+225225+- [`packages/pds/src/actor-store/record/reader.ts`](https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/actor-store/record/reader.ts)
226226+- [`packages/pds/src/api/com/atproto/repo/applyWrites.ts`](https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/api/com/atproto/repo/applyWrites.ts)
227227+- [`packages/pds/src/actor-store/repo/sql-repo-reader.ts`](https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/actor-store/repo/sql-repo-reader.ts)
228228+229229+The next apples-to-apples comparison work should cover:
143230144231- single-repo write curve as record count grows
145232- multi-repo concurrent writes
146146-- listRecords latency under read concurrency
233233+- record-index list latency without record body materialization, across ZDS and
234234+ Tranquil
235235+- official-PDS concurrency probe for the same read/write boundaries
147236- full repo export cost
148237- blob/block store throughput
149238150150-ZDS and Tranquil both report latency percentiles for the metastore-shaped
151151-benchmark. The remaining comparison gap is broader coverage of blockstore,
152152-eventlog, and full repo export behavior.
239239+ZDS and Tranquil both report latency percentiles for the metastore-shaped write
240240+and CID lookup rows. The remaining comparison gap is broader coverage of
241241+blockstore, eventlog, full repo export behavior, an official-PDS benchmark
242242+harness, and an explicit ZDS index-only list probe that matches Tranquil's
243243+`list_records`.
+24
bench/justfile
···3333get-cid callers="10" ops_per_caller="1000":
3434 {{zig}} build bench -Doptimize=ReleaseFast -- --scenario get-cid --callers {{callers}} --ops-per-caller {{ops_per_caller}}
35353636+# benchmark record index lookup plus repo_blocks byte fetch
3737+get-block callers="10" ops_per_caller="1000":
3838+ {{zig}} build bench -Doptimize=ReleaseFast -- --scenario get-block --callers {{callers}} --ops-per-caller {{ops_per_caller}}
3939+4040+# benchmark DAG-CBOR decode of a seeded record block without SQLite
4141+decode-record callers="10" ops_per_caller="1000":
4242+ {{zig}} build bench -Doptimize=ReleaseFast -- --scenario decode-record --callers {{callers}} --ops-per-caller {{ops_per_caller}}
4343+4444+# benchmark DAG-CBOR decode plus JSON response rendering without SQLite
4545+render-record callers="10" ops_per_caller="1000":
4646+ {{zig}} build bench -Doptimize=ReleaseFast -- --scenario render-record --callers {{callers}} --ops-per-caller {{ops_per_caller}}
4747+4848+# benchmark full com.atproto.repo.getRecord storage materialization
4949+get-record callers="10" ops_per_caller="1000":
5050+ {{zig}} build bench -Doptimize=ReleaseFast -- --scenario get-record --callers {{callers}} --ops-per-caller {{ops_per_caller}}
5151+5252+# benchmark full listRecords storage materialization
5353+list-records callers="10" ops_per_caller="1000":
5454+ {{zig}} build bench -Doptimize=ReleaseFast -- --scenario list-records --callers {{callers}} --ops-per-caller {{ops_per_caller}}
5555+5656+# run the official Bluesky PDS read probe from a local atproto checkout
5757+official-pds:
5858+ ./bench/run-official-pds.sh
5959+3660# profile applyWrites stage timing under write concurrency
3761write-profile callers="10" ops_per_caller="500":
3862 {{zig}} build bench -Doptimize=ReleaseFast -- --scenario write-profile --callers {{callers}} --ops-per-caller {{ops_per_caller}}
···11+# benchmarking
22+33+Benchmark notes should separate behavior domains and only compare equivalent
44+work. If a measurement does not exist for the same unit of work, keep it out of
55+the comparison table instead of filling the cell with a nearby but different
66+operation.
77+88+## references
99+1010+- ZDS: `bench/main.zig`
1111+- Tranquil: `crates/tranquil-store/benches/metastore.rs`
1212+- Official PDS: `packages/pds/src/actor-store/record/reader.ts`
1313+1414+## current matrix
1515+1616+The active three-way matrix is:
1717+1818+- apply one record commit
1919+- current record CID lookup
2020+- record-index list
2121+- full record materialization
2222+- blob/block put and get
2323+- repo export
2424+- sync event delivery
2525+2626+ZDS, Tranquil, and the official PDS now have matching single-caller numbers for
2727+apply-one-record and current-CID lookup. ZDS and Tranquil also have explicit
2828+10/100 caller concurrency curves; the official probe should grow those workers
2929+before it is included in the concurrency matrix.
3030+3131+## read path
3232+3333+ZDS follows the official PDS read shape for `getRecord` and `listRecords`: read
3434+the current record index row, fetch matching `repo_blocks` bytes, decode
3535+DAG-CBOR, and render JSON for the XRPC response. Tranquil's metastore
3636+`list_records` is a different unit of work because it returns only rkeys and
3737+CIDs. Keep those rows separate.
3838+3939+Current ZDS read breakdown on this machine:
4040+4141+| operation | callers | ops | result |
4242+|---|---:|---:|---:|
4343+| current CID lookup | 10 | 10000 | 222k ops/s, p95 225 us |
4444+| current CID lookup | 100 | 20000 | 211k ops/s, p95 887 us |
4545+| index + block fetch | 10 | 10000 | 128k ops/s, p95 11 us |
4646+| index + block fetch | 100 | 20000 | 121k ops/s, p95 15 us |
4747+| full getRecord | 10 | 10000 | 38.4k ops/s, p95 173 us |
4848+| full getRecord | 100 | 20000 | 38.1k ops/s, p95 11.7 ms |
4949+| full listRecords, limit 50 | 10 | 10000 | 21.1k ops/s, p95 57 us |
5050+| full listRecords, limit 50 | 100 | 20000 | 19.5k ops/s, p95 78 us |
5151+5252+The official PDS probe on the same machine reports:
5353+5454+| operation | result |
5555+|---|---:|
5656+| apply one record commit | 486 ops/s, p99 3.9 ms |
5757+| current CID lookup | 3.2k ops/s, p99 477 us |
5858+| full getRecord | 3.1k ops/s, p99 469 us |
5959+| full listRecords, limit 50 | 639 ops/s, p99 2.3 ms |
6060+6161+For tiny records, DAG-CBOR decode and JSON rendering are below useful timer
6262+resolution. The important next read benchmark is larger records plus an
6363+index-only list probe matching Tranquil.
6464+6565+## traffic model
6666+6767+For roughly 100 hosted accounts, useful benchmarks should mirror real traffic:
6868+6969+- clients call `getSession`, preferences, profiles, timelines, and proxied
7070+ appview routes repeatedly while open.
7171+- apps call `createRecord`, `applyWrites`, `getRecord`, and `listRecords`.
7272+- media posts call `uploadBlob` before record creation.
7373+- relays and tools call sync endpoints, repo export, and blob reads.
7474+- the landing page reads residents, record counts, recent records, and profile
7575+ blobs.
7676+7777+This points to two instrumentation tasks: per-route latency counters in ZDS
7878+logs, and benchmark scenarios that replay observed route mixes instead of only
7979+isolated microbenchmarks.