perf: optimize session loading and desktop startup (#2545)
* perf: optimize session loading and startup * fix(chat): stabilize history prepend virtualization * perf: unblock first session open from startup network contention Opening the first session after app start waited seconds for its message fetch. Three independent contributors, each measured via CDP network capture and Chromium net-log against the packaged desktop app: - The active-session watchdog fired an uncapped per-directory status poll and child-session discovery burst at startup, and other subsystems (git checks, global session pages, command/skill discovery) fanned out alongside it, saturating the browser's ~6 HTTP/1.1 sockets per origin. Add a shared background-network gate (concurrency 3) and route the watchdog, poll-shaped git reads (also priority: low), global session pages, command/skill loads, and the background update check through it. - The packaged renderer is cross-origin to the loopback backend, so every API call needs a CORS preflight; a few slow OpenCode-proxied requests held the whole pool while preflights and interactive traffic queued behind them. Lift Chromium's per-host connection cap for loopback via ignore-connections-limit in the Electron shell. - OpenCode initializes each directory lazily on its first request, so the first click paid that cost interactively. Warm the last-used directory and the three most recently opened projects right after OpenCode readiness, sequentially and best-effort, overlapping UI startup. Validation: new background-network tests, lifecycle warmup test, focused store/sync tests, UI type-check and lint, dead-code report, node --check plus electron type-check/lint, and CDP first-open measurements on the packaged app (message fetch socket queue 5.4s -> 0.03s). * fix(ui): keep interactive git reads out of background queue --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
09f0c64839
commit
aae889b904
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { getBackgroundNetworkState, runBackgroundNetworkTask } from "./background-network"
|
||||
|
||||
const deferred = <T>() => {
|
||||
let resolve!: (value: T) => void
|
||||
let reject!: (reason?: unknown) => void
|
||||
const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej })
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
describe("runBackgroundNetworkTask", () => {
|
||||
test("caps concurrent tasks at the limit and drains waiters in order", async () => {
|
||||
const { limit } = getBackgroundNetworkState()
|
||||
const gates = Array.from({ length: limit + 2 }, () => deferred<string>())
|
||||
const started: number[] = []
|
||||
const results = gates.map((gate, index) => runBackgroundNetworkTask(() => {
|
||||
started.push(index)
|
||||
return gate.promise
|
||||
}))
|
||||
|
||||
await Promise.resolve()
|
||||
expect(started).toEqual(Array.from({ length: limit }, (_, index) => index))
|
||||
expect(getBackgroundNetworkState().active).toBe(limit)
|
||||
expect(getBackgroundNetworkState().waiting).toBe(2)
|
||||
|
||||
gates[0].resolve("a")
|
||||
await results[0]
|
||||
expect(started).toContain(limit)
|
||||
|
||||
for (const [index, gate] of gates.entries()) gate.resolve(`v${index}`)
|
||||
expect(await Promise.all(results)).toEqual(["a", ...gates.slice(1).map((_, index) => `v${index + 1}`)])
|
||||
expect(getBackgroundNetworkState().active).toBe(0)
|
||||
expect(getBackgroundNetworkState().waiting).toBe(0)
|
||||
})
|
||||
|
||||
test("releases the slot when a task rejects", async () => {
|
||||
await expect(runBackgroundNetworkTask(() => Promise.reject(new Error("boom")))).rejects.toThrow("boom")
|
||||
expect(getBackgroundNetworkState().active).toBe(0)
|
||||
const value = await runBackgroundNetworkTask(() => Promise.resolve(42))
|
||||
expect(value).toBe(42)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Shared concurrency gate for background network traffic.
|
||||
*
|
||||
* The browser allows only ~6 concurrent HTTP/1.1 connections per origin, and
|
||||
* every runtime (web, desktop loopback, VS Code, mobile host) funnels API
|
||||
* traffic through one origin. During startup many subsystems fan out at once —
|
||||
* per-directory session/status polls, git checks per project and worktree,
|
||||
* command/skill discovery, global session pages — and several of those calls
|
||||
* are slow while the OpenCode server is still warming up. Uncapped, they
|
||||
* occupy the whole connection pool and interactive traffic (opening a session
|
||||
* and fetching its messages) queues for seconds behind them.
|
||||
*
|
||||
* Every poll/prefetch-shaped background call should run through
|
||||
* {@link runBackgroundNetworkTask} so the aggregate background footprint stays
|
||||
* bounded and sockets remain free for the critical path. GitHub PR status has
|
||||
* its own dedicated gate (see useGitHubPrStatusStore) because a single PR
|
||||
* request can hold a socket for up to 12s and must not starve other
|
||||
* background work either; the two caps combined still leave sockets free.
|
||||
*/
|
||||
|
||||
const BACKGROUND_NETWORK_CONCURRENCY = 3
|
||||
|
||||
let backgroundNetworkActive = 0
|
||||
const backgroundNetworkWaiters: Array<() => void> = []
|
||||
|
||||
const acquireBackgroundNetworkSlot = (): Promise<void> => {
|
||||
if (backgroundNetworkActive < BACKGROUND_NETWORK_CONCURRENCY) {
|
||||
backgroundNetworkActive += 1
|
||||
return Promise.resolve()
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
backgroundNetworkWaiters.push(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
const releaseBackgroundNetworkSlot = (): void => {
|
||||
const next = backgroundNetworkWaiters.shift()
|
||||
if (next) {
|
||||
// Hand the slot directly to the next waiter — keep the active count steady.
|
||||
next()
|
||||
return
|
||||
}
|
||||
backgroundNetworkActive = Math.max(0, backgroundNetworkActive - 1)
|
||||
}
|
||||
|
||||
/** Run one background network call under the shared concurrency gate. */
|
||||
export const runBackgroundNetworkTask = async <T>(task: () => Promise<T>): Promise<T> => {
|
||||
await acquireBackgroundNetworkSlot()
|
||||
try {
|
||||
return await task()
|
||||
} finally {
|
||||
releaseBackgroundNetworkSlot()
|
||||
}
|
||||
}
|
||||
|
||||
/** Test-only visibility into the gate. */
|
||||
export const getBackgroundNetworkState = () => ({
|
||||
active: backgroundNetworkActive,
|
||||
waiting: backgroundNetworkWaiters.length,
|
||||
limit: BACKGROUND_NETWORK_CONCURRENCY,
|
||||
})
|
||||
@@ -1,5 +1,13 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { getGitStatus, gitFetch, stageGitFile, stageGitFiles, unstageGitFile, unstageGitFiles } from './gitApiHttp';
|
||||
import {
|
||||
getGitBranches,
|
||||
getGitStatus,
|
||||
gitFetch,
|
||||
stageGitFile,
|
||||
stageGitFiles,
|
||||
unstageGitFile,
|
||||
unstageGitFiles,
|
||||
} from './gitApiHttp';
|
||||
|
||||
type FetchCall = {
|
||||
input: RequestInfo | URL;
|
||||
@@ -160,3 +168,18 @@ describe('gitApiHttp status cache', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('gitApiHttp request priority', () => {
|
||||
test('leaves low-level reads outside the background policy', async () => {
|
||||
installWindowMock();
|
||||
const calls = installFetchMock();
|
||||
try {
|
||||
await getGitBranches('/repo-interactive');
|
||||
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].init?.priority).toBe(undefined);
|
||||
} finally {
|
||||
restoreMocks();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user