~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

7
113 lines · 5.6 KB Raw History

Performance

On a real network, an agent spends most of its time waiting on round trips to the PDS, so every operation is planned around them.

Round trips

OperationRound trips, warm
git push2: one to read the refs and newest packs (three reads in parallel), one applyWrites
git fetch1 when the new packs are under 192 KiB, as they arrive inside the listing; larger ones add parallel blob reads
knotless commit, no checkout2, plus 3 per lost race
A ref's compare-and-swap0 extra: its claim record goes in the same applyWrites

"Warm" means after the first call: the identity, session and repository key are cached on disk, and every request in a process shares one connection. Storage explains each count.

Benchmarks

Measured with cargo run --release --example bench -- --rtt 50 (just bench 50): the npm reference PDS (@atproto/pds 0.5.43, SQLite) on the same machine, behind a proxy adding 50 ms of round-trip time, on an Apple-silicon laptop.

p50p95
git push, one small commit223 ms276 ms
git fetch, one new commit177 ms211 ms
knotless commit, no checkout366 ms596 ms
git clone, 21 packs240 ms253 ms
git clone, compacted158 ms172 ms
git clone --depth 1 via knotless-serve208 ms299 ms
One file via the knotless-serve API, warm45 ms57 ms
16 agents pushing 16 branches at once443 ms total
16 agents committing to one branch at once10.9 s total, 97 pushes

Many writers on one branch

A branch is one compare-and-swap, so of the writers pushing straight to one branch, one lands per round. The reference PDS also serializes an account's writes. That is why the last row above is slow. Give each agent its own branch and merge, or send commits through a gateway, which queues them per branch and lands each queue as one push.

Large repositories

Measured by pushing three real repositories into the npm reference PDS (@atproto/pds 0.5.43, SQLite) on the same Apple-silicon laptop, with no round-trip time added, so the times are mostly CPU and disk. Over a real network, add the time it takes to move the bytes. Each repository's default branch went first, with its whole history, then its tags.

golang/gopython/cpythonnodejs/node
History68,207 commits, 500 tags133,994 commits, 685 tags49,240 commits, 969 tags
On the PDS, compacted450 MB723 MB1.53 GB
First push, the branch6.5 s10.7 s15.4 s
Push of the tags6.3 s18.7 s35.9 s
git clone, cold20 s38 s51 s
git push of one small commit0.2 s0.2 s0.2 s
git fetch of it0.1 s0.2 s0.2 s
knotless compact25 s44 s79 s
Peak memory: push, clone, compact0.9, 1.3, 1.9 GB1.4, 2.1, 2.1 GB2.8, 4.3, 3.7 GB

A repository's size does not slow everyday work. A push or fetch moves only its own pack, so one small commit takes the same time in a 1.5 GB repository as in a new one. Size shows up in four places:

  • Memory. The helper holds a whole pack in memory, and more than once: pack-objects is read into one buffer, which a push then copies into its blob parts, and a fetch joins every part before index-pack reads it. Cloning or compacting takes two to three times the repository's packed size. Streaming the pack to and from the blobs would keep this flat.
  • Packs after the first. Every pack is self-contained, so a later push cannot store objects as deltas against what the PDS already holds. Pushing node's tags added 780 MB to its 1.44 GB branch. Compaction folds them back into one pack, here of 1.53 GB.
  • Write points. A PDS with rate limits on, as bsky.social has, gives an account 5000 write points an hour and 35000 a day, counting every write inside an applyWrites, so batching saves round trips but not points. A new ref is one create, 3 points, so about 1600 new refs fit in an hour, and node's 969 tags push in one go. Running out stops more than the push: until the hour is up, every write from the account fails, a one-commit push, compaction, and any other app writing as that account. A push the limit stops keeps what landed, and names the refs it did not push and when to push again. A repository with several thousand tags takes more than an hour; a record holding many tags at once, as git's packed-refs does, would make them a few writes, at the cost of a compare-and-swap for each tag.
  • Blob uploads. The same PDS allows 1000 uploadBlob a day, at 4 MB a part. Node's branch took about 360 parts, its tags about 200, and a compaction would take about 380 more, so importing a repository this size and compacting it uses most of a day's uploads.

Web pages

knotless-serve reads what a page shows from a mirror's objects in process, in about a millisecond warm; a highlighted 40 KB source file takes a few more. Drawing the page takes a few milliseconds more. A social card takes about 5 ms to draw the first time, is about 30 KB, and is served from memory after that (just bench-cards measures it).

Caches in knotless-serve

WhatKept
Social cardsby version, the oldest dropped past 512; at most one drawn per core
Pictures for cards128, each fetched within 2 s and at most 1 MiB
Avatars and email claims600 s; a PDS that did not answer is asked again
Languages barsby tree, 1024 trees, cleared all at once past that
Objects read in process4 MiB of object cache per thread's handle on a mirror