fix(github): stop PR-status requests from starving startup connection pool
Watching N worktrees fired N PR-status requests at once (startWatching called refresh() directly, bypassing the batch limiter). Each request can take 20s+ under GitHub secondary-rate-limiting, and N of them saturate the browser's ~6 HTTP/1.1 connections per origin, starving the critical path (bootstrap session.status, diffs, sending messages) until they finish — the UI appeared frozen for ~20s on startup. - Gate all PR-status network calls through a global concurrency semaphore (max 2), so free sockets always remain for critical traffic. - Bound resolveGitHubPrStatus with a 12s timeout so a slow request fails fast instead of holding a socket; the client keeps its last-known status. - Reuse already-fetched repo metadata for the default branch instead of a redundant repos.get, reducing serial GitHub calls (less rate-limiting).
This commit is contained in:
@@ -93,6 +93,43 @@ const timers = new Map<string, number>();
|
||||
const bootstrapTimers = new Map<string, number[]>();
|
||||
const inFlightBySignature = new Set<string>();
|
||||
const lastRefreshBySignature = new Map<string, number>();
|
||||
|
||||
// Global concurrency gate for PR-status network requests.
|
||||
//
|
||||
// PR status is non-critical chrome, but each request can be slow (the server
|
||||
// makes many serial GitHub API calls and GitHub secondary-rate-limits bursts,
|
||||
// so a single request can take 20s+). The browser allows only ~6 concurrent
|
||||
// HTTP/1.1 connections per origin. Without this cap, watching N worktrees fires
|
||||
// N PR-status requests at once (each startWatching() calls refresh() directly,
|
||||
// bypassing refreshTargets' batch limiter), which saturates the connection pool
|
||||
// and starves the critical path (bootstrap session.status, diffs, sending
|
||||
// messages) for the full duration — the whole UI appears frozen on startup.
|
||||
//
|
||||
// Capping concurrency low guarantees free sockets remain for critical traffic.
|
||||
const PR_STATUS_NETWORK_CONCURRENCY = 2;
|
||||
let prStatusNetworkActive = 0;
|
||||
const prStatusNetworkWaiters: Array<() => void> = [];
|
||||
|
||||
const acquirePrStatusNetworkSlot = (): Promise<void> => {
|
||||
if (prStatusNetworkActive < PR_STATUS_NETWORK_CONCURRENCY) {
|
||||
prStatusNetworkActive += 1;
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
prStatusNetworkWaiters.push(resolve);
|
||||
});
|
||||
};
|
||||
|
||||
const releasePrStatusNetworkSlot = (): void => {
|
||||
const next = prStatusNetworkWaiters.shift();
|
||||
if (next) {
|
||||
// Hand the slot directly to the next waiter — keep the active count steady.
|
||||
next();
|
||||
return;
|
||||
}
|
||||
prStatusNetworkActive = Math.max(0, prStatusNetworkActive - 1);
|
||||
};
|
||||
|
||||
const createEntry = (): PrStatusEntry => ({
|
||||
status: null,
|
||||
isLoading: false,
|
||||
@@ -488,7 +525,13 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
|
||||
activeRequestCount: prev.activeRequestCount + 1,
|
||||
totalRequestCount: prev.totalRequestCount + 1,
|
||||
}));
|
||||
const next = await params.github.prStatus(params.directory, params.branch, params.remoteName ?? undefined, { force: options?.force });
|
||||
await acquirePrStatusNetworkSlot();
|
||||
let next: GitHubPullRequestStatus;
|
||||
try {
|
||||
next = await params.github.prStatus(params.directory, params.branch, params.remoteName ?? undefined, { force: options?.force });
|
||||
} finally {
|
||||
releasePrStatusNetworkSlot();
|
||||
}
|
||||
set((prev) => {
|
||||
const nextEntries = { ...prev.entries };
|
||||
signatureKeys.forEach((signatureKey) => {
|
||||
|
||||
Reference in New Issue
Block a user