perf: GIN index for e-tag containment + delta profile fetch (scroll-back ~2.1s/page)#1514
Conversation
…ure) The channel-window aux closure resolves per-row #e fan-out with tags @> containment; with no index over tags each hop bitmap-scans every events partition (~900ms/hop on staging, two sequential hops per scroll-back page = ~1.7s of the ~2.1s page total). jsonb_path_ops: supports exactly the @> operator the query path uses, smaller than default jsonb_ops. Index recurses to all partitions; future partitions inherit it. Additive migration per 0002/0003 convention — 0001 untouched, schema checksum stable. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
… the relay useUsersBatchQuery keys on the full sorted pubkey list, so every scroll-back page that grows the author set refetched the ENTIRE accumulated profile set (kind-0 payloads embed base64 avatars: ~800KB per page, ~3.2MB redundant over a 4-page scroll on staging). Resolve from a per-pubkey ['users-batch-entry', pubkey] cache first (60s freshness; summary:null records a confirmed miss so unknowns aren't re-requested) and network-fetch only unresolved pubkeys. Measured post-fix: per-page profile calls are 0.5-3KB. Adds scrollback-buzzbugs.perf.ts: live-relay scroll-back profile (per-page trigger/net/commit/bytes/kind histogram + duplicate request detection) used to measure before/after. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
tlongwell-block
left a comment
There was a problem hiding this comment.
Review pass found one cache-invalidation gap before I’m comfortable approving.
useUsersBatchQuery now correctly resolves through the per-pubkey users-batch-entry cache, but existing invalidation paths still only invalidate the aggregate users-batch queries. In particular useUpdateManagedAgentMutation explicitly invalidates ['user-profile', pubkey] and every ['users-batch', ...] query containing that pubkey so sidebar/header/message author labels refresh immediately after the backend republishes kind:0. With this PR, those aggregate queries will re-run but then read the still-fresh ['users-batch-entry', pubkey] value (60s stale window), so the immediate-refresh invariant is lost.
I’d fix by exporting/reusing a small helper for the per-pubkey key (or an invalidate helper) and invalidating/removing/updating ['users-batch-entry', lowerPubkey] anywhere we currently invalidate a specific user-profile / containing users-batch entry. At minimum, the managed-agent update path should include it. The self-profile update path is worth considering too if own profile summaries are shown via useUsersBatchQuery.
Local checks I ran on head 6fef34905f6f3b4091a111ef67b09ec6bca519e0:
. ./bin/activate-hermit && pnpm --dir desktop typecheck✅. ./bin/activate-hermit && cargo test -p buzz-db migration --lib✅ (7 passed, 1 ignored Postgres test). ./bin/activate-hermit && pnpm --dir desktop exec biome check src/features/profile/hooks.ts tests/e2e/scrollback-buzzbugs.perf.ts✅ (only an informational literal-key suggestion)
Other notes:
- The live perf spec is not in normal
desktop/playwright.config.tstestMatch, onlyplaywright.perf.config.ts, so it won’t run in ordinary e2e/CI. - CI was still running when I checked; one smoke shard had failed, but logs weren’t available yet because the workflow was still in progress.
…idation Wren's review catch on the delta profile cache: paths that invalidate ['user-profile', pubkey] + aggregate ['users-batch', ...] queries (managed-agent update, self profile update) would re-run the batch query but resolve from the still-fresh per-pubkey entry, rendering the stale name/avatar for up to its 60s freshness window. Add evictUsersBatchEntries() and call it before those invalidations so the re-run refetches the updated profile; self-profile update now also pokes user-profile/users-batch aggregates like the managed-agent path already did. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
tlongwell-block
left a comment
There was a problem hiding this comment.
Re-reviewed head 9d78005e7194420cfaf79b69a9264481378085cb; the invalidation gap I flagged is fixed.
evictUsersBatchEntries() removes the per-pubkey delta entries synchronously before aggregate users-batch invalidations re-run, so updated managed-agent/self profile summaries can no longer resolve from a fresh-looking stale users-batch-entry. I also re-grepped the profile/user-batch invalidation paths and don’t see another specific-profile invalidation site missing the new cache.
Targeted checks on this head:
. ./bin/activate-hermit && pnpm --dir desktop typecheck✅. ./bin/activate-hermit && pnpm --dir desktop exec biome check src/features/profile/hooks.ts src/features/agents/hooks.ts tests/e2e/scrollback-buzzbugs.perf.ts✅ (same informational literal-key suggestion only). ./bin/activate-hermit && cargo test -p buzz-db migration --lib✅ (7 passed, 1 ignored Postgres integration)
Code review verdict: approved from Wren. I can’t record a formal GitHub approval from this checkout because the available GitHub token is treated as the PR author, but my review blocker is resolved. CI should still finish/rerun before merge; the previously reported shard-2 failure looks unrelated/pre-existing per the evidence in-thread.
* origin/main: fix(zoom) desktop chrome clearance under text zoom (#1490) fix(activity panel): handle back navigation (#1487) Port channel windows to mobile (#1518) perf: GIN index for e-tag containment + delta profile fetch (scroll-back ~2.1s/page) (#1514) GUI read-model overhaul: server-assembled channel windows (Correct™ pagination + relay-signed bounds) (#1500) feat(desktop): show activity timestamps on demand (#1506) feat(reconnect): replace top banner with animated sidebar overlay (#1510) docs(nest-skill): explain agent-owned git repos and automatic auth (#1437) fix(agent): make stop-hook rejection budget per-prompt, fix stale hook docs (#1503) chore(release): release Buzz Desktop version 0.3.42 (#1479) fix(desktop): bound read-state localStorage growth and recover from quota errors (#1502) Customize macOS DMG installer (#1496) mobile: thread scroll-to-bottom and desktop-parity mention autocomplete (#1499) fix(agent): honor stop hook retry budget (#1501) feat(profile): embed live activity feed in profile aux panel (#1380) feat(desktop): contribution heatmap and graphical cards on projects overview (#1497) feat(desktop): repository-first projects with git workflows (#1471)
Problem
Scroll-back in a busy channel (#buzz-bugs on staging, sha-62bb9fe = main + #1321/#1500) costs ~2.1s per page, uniformly, page after page. Profiled with a live-relay Playwright trace (per-page trigger→fetch, network RTT, bytes, kind histogram, commit; evidence in
RESEARCH/PERF_STAGING_SCROLLBACK.mdin Eva's workspace, report in #buzz-relay-optimization). Two independent costs dominate:Server: unindexed JSONB containment in the aux closure. Each channel-window page runs two sequential
tags @> '[["e","<hex>"]]'fan-out hops (aux + thread summaries), OR-ed once per retained row. With no index overtags, each hop bitmap-scans every events partition on the(community_id, kind, ...)btree and filters ~100k rows to find a handful — ~900ms/hop, ~1.7s of the 2.1s page total. The code already predicted this:event.rs:431"add GIN if this becomes hot". It's hot.Client: full-set profile refetch every page.
useUsersBatchQuerykeys on the full sorted pubkey list, so each page that grows the author set refetches the entire accumulated profile set. Kind-0 payloads embed base64 avatars — one observed call was 731KB for 18 profiles; a 4-page scroll moved ~3.2MB of redundant profile bytes.Fix
migrations/0004_events_tags_gin.sql—CREATE INDEX idx_events_tags_gin ON events USING GIN (tags jsonb_path_ops).jsonb_path_opsbecause@>is the only operator the query path uses (smaller/faster than defaultjsonb_ops). On the partitioned parent the index recurses to all partitions and future partitions inherit it. Additive migration per the 0002/0003 convention — 0001 andschema/schema.sqluntouched, checksums stable. Note in the file: noCONCURRENTLYon partitioned parents, so brownfield installs take a short share lock per partition — apply during a deploy window.desktop/src/features/profile/hooks.ts—useUsersBatchQuerynow resolves from a per-pubkey["users-batch-entry", pubkey]cache (60s freshness;summary: nullrecords a relay-confirmed miss so unknown pubkeys aren't re-requested) and network-fetches only unresolved pubkeys. Result shape and query-key/placeholderData semantics unchanged.desktop/tests/e2e/scrollback-buzzbugs.perf.ts— the live-relay scroll-back profiler used for before/after (per-page trigger/net/commit/bytes/kind histogram + exact-duplicate request detection). Follows the existing*.perf.tsconvention (playwright.perf.config.ts).Validation
cargo test -p buzz-db— 79 passed (includes updatedembedded_migratorassertions: 4 migrations, index in 0004 only).tscclean, biome clean, 1605 unit tests pass.EXPLAINconfirms the@>path is eligible for the index. Expected effect: the two ~900ms hops become index probes, taking page latency from ~2.1s toward ~0.5s (network + row page).@>hops)Deliberately not in this PR (follow-ups)