Skip to content

perf(server): reaper sweep decodes only live provider bindings - #229

Merged
tusharbhardwaj-bk merged 2 commits into
expbkmainfrom
t3code/perf-16-reaper-live-bindings
Sep 27, 2026
Merged

tusharbhardwaj-bk merged 2 commits into
expbkmainfrom
t3code/perf-16-reaper-live-bindings

Conversation

@tusharbhardwaj-bk

Copy link
Copy Markdown
Collaborator

Problem

ProviderSessionReaper sweeps every 5 minutes. Its bindings phase called directory.listBindings(), which decodes every provider_session_runtime row (two Schema passes plus a provider-kind decode per row, concurrency: "unbounded"), then continued past every stopped one.

In prod, 1,351 of 1,355 rows are stopped. The subagent's journal analysis over 575 sweeps in 48 h:

  • sweep p50 157 ms, p90 1.86 s, max 5.3 s;
  • the time is almost all bindingsMs;
  • all of it on the shared event loop.

Fix

  • New fork module provider/liveProviderBindings.expbkt3.ts. It runs SELECT thread_id FROM provider_session_runtime WHERE status <> 'stopped' (a small table, no JSON), then directory.getBinding(id) for just those ids. The bindings keep exactly the shape listBindings returns. On an SQL failure it falls back to listBindings().
  • One marked seam at the reaper's call site, using the reaper's existing SQL context.

The decode-everything loop is upstream code, so the cleaner long-term fix is an upstream listBindings({ excludeStopped }). Noted as an upstreaming candidate.

Evidence

Lab benchmark on a VACUUM INTO copy of prod (1,355 rows, 1,352 stopped), 5 calls each:

rows decoded ms / call
listBindings (before) 1,355 62, 55, 67, 42, 37
listLiveProviderBindings (after) 3 2, 1, 1, 4, 1

The idle lab is far faster than prod under contention (p90 1.86 s), but the ratio holds.

Tests:

  • New liveProviderBindings.expbkt3.test.ts: 300 stopped rows, plus one running and one starting; the result is exactly listBindings().filter(status !== "stopped").
  • Existing ProviderSessionReaper.test.ts: all pass (20 total).
  • vp run typecheck in apps/server is clean, and the fork-marker check passes.

Model/harness: Claude Opus 5.5 via Claude Code in T3 Code.

🤖 Generated with Claude Code

Every 5 minutes the session reaper called listBindings(), which decodes
every provider_session_runtime row (two Schema passes plus a provider-kind
decode per row), then skipped every stopped one. In prod that is 1,352 of
1,355 rows; the bindings phase took ~2 s at p90 and up to 5 s on the shared
event loop.

A fork helper now selects the ids of non-stopped rows and decodes only
those through the directory (same binding shape); on a query failure it
falls back to the full list. On a copy of the prod database: 1,355 -> 3 rows
decoded, 37-67 ms -> 1-4 ms per sweep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M labels Sep 27, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tusharbhardwaj-bk
tusharbhardwaj-bk merged commit a448ba5 into expbkmain Sep 27, 2026
12 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants