Skip to content

perf(relay): coalesce startup snapshot writes across discovery batches - #2593

Draft
kalvinnchau wants to merge 1 commit into
mainfrom
peon/rg-discovery-save
Draft

kalvinnchau wants to merge 1 commit into
mainfrom
peon/rg-discovery-save

Conversation

@kalvinnchau

Copy link
Copy Markdown
Contributor

Problem

At startup the session catches up channel rosters and metadata as many small live discovery batches. applyDiscovery called saveDiscovery() synchronously for every accepted batch that was not served from cache. Each call rebuilt savedEvents() and the saved profiles, and wrote the whole startup snapshot, which the IndexedDB adapter structured-clones and sizes. A community with 300 channels paid for 300 full snapshot writes on the main thread during catch-up, all but the last of them superseded immediately.

Change

applyDiscovery schedules the save on the existing discoverySave timer (discoverySave ??= setTimeout(saveDiscovery, 500)), the same pending save that profile-triggered saves already use. So the store writes at most once per 500 ms window, and saveDiscovery reads the current state when it fires. A batch that revokes access to a known channel still saves immediately, so a denied channel's events and profiles never stay in the snapshot.

No behavior changes

The snapshot is only read by restoreStartup on the next launch, which already rejects old snapshots and skips them once live discovery has been observed. Pending saves are dropped on dispose, clearCache and the access-denied reset, as before. The difference is timing:

  • A quit within 500 ms of a batch skips that snapshot update; the next launch falls back to a network read.
  • A cached channel dropped by a partial read leaves the snapshot up to 500 ms later. That is not a revocation, which still saves immediately.

Three startup tests that read the snapshot right after such a read now wait for the write. A new test checks that 5 batches produce one write with the final content after 500 ms. It fails on main (5 writes).

Evidence

Bench: a startup catch-up burst of 300 roster batches (one per channel, 40 members each), delivered through the session's live receive path. The persistence stub structured-clones and serializes each startup patch, standing in for the IndexedDB write. Medians, stock and this PR interleaved on 3 Linux and 3 macOS workstations.

metric Linux: stock Linux: this PR Linux Δ % macOS: stock macOS: this PR macOS Δ %
burst, main thread 730.7 ms 92.9 ms −87.3% 421.0 ms 95.4 ms −77.4%
snapshot writes 300 1 −99.7% 300 1 −99.7%

Lower is better. There are 18 samples per cell. These are Node timings without CPU throttling. The burst time excludes the single deferred write, and the stub understates a real IndexedDB write, so the saving in the app is at least this large.

Checks: the relay unit suites pass, along with tsc and biome.

🤖 Generated with Claude Code

applyDiscovery called saveDiscovery synchronously per accepted non-cached
batch, rebuilding savedEvents() and writing the whole startup snapshot
(structured clone, byteSize, getAll) once per batch during catch-up.
write at most once per 500 ms window instead, sharing the pending save that
profile-triggered saves already use. a batch that revokes access still
saves immediately so denied channel content and profiles never linger in
the snapshot. pending saves are dropped on dispose, clearCache and denyAll,
as before, so a quit within 500 ms of a batch skips that snapshot update.

Signed-off-by: Kalvin C <kalvin@squareup.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant