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

@@ -285,3 +285,38 @@ test("a failed assignment query surfaces as a failed section instead of silent l
assert.ok(result.issues.failedSections.includes("assignments"));
});
test("fetchAssignmentOperationEvents stops paginating once its signal aborts", async () => {
// A permanently-full page would paginate forever without the cursor; abort
// after the first page and require the loop to stop with AbortError.
const controller = new AbortController();
let fetches = 0;
const fullPage = (until) =>
Array.from({ length: 500 }, (_, index) => ({
id: `${until ?? "head"}-${index}`.padEnd(64, "0"),
kind: 1,
pubkey: "a".repeat(64),
created_at: 1_000_000 - fetches * 1_000 - index,
content: JSON.stringify({ type: "assign" }),
tags: [],
}));
const fetchEvents = async (filter) => {
fetches += 1;
// Bound the fake: without the abort support the loop would paginate
// forever (each page has a fresh cursor) and OOM the test run.
if (fetches > 3) throw new Error("kept paginating after abort");
const page = fullPage(filter.until);
controller.abort();
return page;
};
await assert.rejects(
fetchAssignmentOperationEvents(
["issue".padEnd(64, "1")],
fetchEvents,
controller.signal,
),
(error) => error.name === "AbortError",
);
assert.equal(fetches, 1);
});