perf(desktop): stop the Projects fan refetching on re-entry and running after leave (#6458)

Entering Projects fires a large fan: an exhaustive paginated relay
enumeration (projects/repos/tombstones), five 2,000-event work-item
queries plus assignment-operation scans, per-repo activity summaries,
and a local-repository filesystem scan. Measured on a large community
(101 issues / 258 PRs):

| query | measured cost |
|---|---|
| work-items (5 × 2,000-event REQs + assignment scans) | 3.5–3.9s |
| activity summaries | 4.1s |
| repository activity | 1.0–2.2s |
| local repository scan | 1.7s |

Two lifecycle bugs made the fan far more expensive than it needs to be:

- **Freshness windows guaranteed a full refetch on nearly every
re-entry** (60s enumeration, 30s work-items/activity, 10s local scan) —
i.e., the costs above were re-paid on almost every visit. Every local
write path already invalidates its keys explicitly (issue/PR mutations,
project creation, repo sync), so the short windows only served
remote-actor freshness. Raised to 5m/2m/2m with a 30m enumeration cache:
re-entries now paint from cache, and the fan re-runs at most every 2–5
minutes.
- **Leaving Projects left the whole fan running**, competing with the
next surface's channel fetches on the same relay connection. AbortSignal
is now threaded through the enumeration and assignment pagination loops
(optional params — behavior identical without a signal), and leaving the
surface cancels the work-items query. Deliberately NOT cancelled: the
enumeration (the always-mounted sidebar projects section observes it and
its 30m cache is valuable), repo snapshots and local scans (native work
that can't abort — cancelling would discard the finished result and
force the same clones again), and activity summaries (a single bounded
request).

Abort behavior is covered by red-first unit tests on both pagination
loops. Remaining follow-up (out of scope): the queries themselves want a
relay-side aggregate instead of shipping thousands of events to compute
counts client-side.

---------

Signed-off-by: Max Lampert <maxwell@squareup.com>
This commit is contained in:
Max Lampert
2026-08-21 16:40:35 -07:00
committed by GitHub
parent b0466ac465
commit b85d680fb1
10 changed files with 172 additions and 13 deletions

View File

@@ -68,6 +68,7 @@ export async function fetchAssignmentOperationEvents(
fetchEvents: (
filter: FetchEventsInput,
) => Promise<RelayEvent[]> = relayClient.fetchEvents.bind(relayClient),
signal?: AbortSignal,
): Promise<RelayEvent[]> {
if (issueIds.length === 0) return [];
const chunks: string[][] = [];
@@ -75,7 +76,9 @@ export async function fetchAssignmentOperationEvents(
chunks.push(issueIds.slice(i, i + ISSUE_ID_CHUNK_SIZE));
}
const pages = await Promise.all(
chunks.map((chunk) => fetchIssueCommentsExhaustively(chunk, fetchEvents)),
chunks.map((chunk) =>
fetchIssueCommentsExhaustively(chunk, fetchEvents, signal),
),
);
const seen = new Map<string, RelayEvent>();
for (const page of pages) {
@@ -91,11 +94,15 @@ export async function fetchAssignmentOperationEvents(
async function fetchIssueCommentsExhaustively(
issueIds: string[],
fetchEvents: (filter: FetchEventsInput) => Promise<RelayEvent[]>,
signal?: AbortSignal,
): Promise<RelayEvent[]> {
const seen = new Map<string, RelayEvent>();
let limit = ASSIGNMENT_PAGE_LIMIT;
let until: number | undefined;
for (;;) {
// Leaving the Projects surface cancels its queries; stop queuing pages
// behind the next surface's fetches.
signal?.throwIfAborted();
const page = await fetchEvents({
kinds: [KIND_TEXT_NOTE],
"#e": issueIds,