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,81 @@
|
||||
export function projectSessionLoadPerformance(events, recordingStartedAt) {
|
||||
const sourceEvents = Array.isArray(events) ? events : []
|
||||
if (!Number.isFinite(recordingStartedAt)) {
|
||||
return { bufferAtCapacity: sourceEvents.length >= 1000, events: [] }
|
||||
}
|
||||
const allowedOperations = new Set([
|
||||
"bootstrap.directory",
|
||||
"bootstrap.sessions.all",
|
||||
"bootstrap.sessions.archived",
|
||||
"bootstrap.sessions.roots",
|
||||
"global-sessions.active",
|
||||
"global-sessions.archived",
|
||||
"session-messages.initial",
|
||||
"session-messages.older",
|
||||
"session-messages.page",
|
||||
"session-messages.refresh",
|
||||
"session-messages.visible",
|
||||
"session-prefetch",
|
||||
])
|
||||
const allowedCallers = new Set([
|
||||
"action-demand",
|
||||
"current-directory",
|
||||
"initial",
|
||||
"initial-page",
|
||||
"known-project",
|
||||
"known-worktree",
|
||||
"older",
|
||||
"pagination",
|
||||
"prefetch",
|
||||
"project-expanded",
|
||||
"refresh",
|
||||
"selected-session",
|
||||
"server-connected",
|
||||
"worktree-expanded",
|
||||
])
|
||||
const allowedOutcomes = new Set(["complete", "error", "stale", "deduplicated", "canceled"])
|
||||
const optionalNonNegativeNumber = (value) => Number.isFinite(value) && value >= 0 ? value : undefined
|
||||
const optionalNonNegativeInteger = (value) => Number.isInteger(value) && value >= 0 ? value : undefined
|
||||
return {
|
||||
bufferAtCapacity: sourceEvents.length >= 1000,
|
||||
events: sourceEvents.flatMap((event) => {
|
||||
const {
|
||||
operation,
|
||||
caller,
|
||||
queuedMs,
|
||||
requestLimit,
|
||||
cursorPresent,
|
||||
durationMs,
|
||||
outcome,
|
||||
retryCount,
|
||||
recordCount,
|
||||
at,
|
||||
} = event && typeof event === "object" ? event : {}
|
||||
if (!allowedOperations.has(operation)
|
||||
|| !allowedCallers.has(caller)
|
||||
|| !allowedOutcomes.has(outcome)
|
||||
|| !Number.isFinite(durationMs)
|
||||
|| durationMs < 0
|
||||
|| !Number.isFinite(at)) {
|
||||
return []
|
||||
}
|
||||
const projected = {
|
||||
operation,
|
||||
caller,
|
||||
durationMs,
|
||||
outcome,
|
||||
offsetMs: Math.max(0, at - recordingStartedAt),
|
||||
}
|
||||
const safeQueuedMs = optionalNonNegativeNumber(queuedMs)
|
||||
const safeRequestLimit = optionalNonNegativeInteger(requestLimit)
|
||||
const safeRetryCount = optionalNonNegativeInteger(retryCount)
|
||||
const safeRecordCount = optionalNonNegativeInteger(recordCount)
|
||||
if (safeQueuedMs !== undefined) projected.queuedMs = safeQueuedMs
|
||||
if (safeRequestLimit !== undefined) projected.requestLimit = safeRequestLimit
|
||||
if (typeof cursorPresent === "boolean") projected.cursorPresent = cursorPresent
|
||||
if (safeRetryCount !== undefined) projected.retryCount = safeRetryCount
|
||||
if (safeRecordCount !== undefined) projected.recordCount = safeRecordCount
|
||||
return [projected]
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import assert from "node:assert/strict"
|
||||
import test from "node:test"
|
||||
|
||||
import { projectSessionLoadPerformance } from "./profile-browser-session-load.mjs"
|
||||
|
||||
test("session-load summary exports only the approved diagnostic fields", () => {
|
||||
const projectInBrowser = Function(
|
||||
"events",
|
||||
"recordingStartedAt",
|
||||
`return (${projectSessionLoadPerformance.toString()})(events, recordingStartedAt)`,
|
||||
)
|
||||
const projected = projectInBrowser([{
|
||||
operation: "session-messages.initial",
|
||||
caller: "initial",
|
||||
queuedMs: 3,
|
||||
requestLimit: 50,
|
||||
cursorPresent: false,
|
||||
durationMs: 17,
|
||||
outcome: "complete",
|
||||
retryCount: 1,
|
||||
recordCount: 50,
|
||||
at: 1_250,
|
||||
runtimeKey: "secret-runtime",
|
||||
directory: "/secret/worktree",
|
||||
sessionID: "secret-session",
|
||||
message: "secret-message",
|
||||
content: "secret-content",
|
||||
authorization: "Bearer secret-token",
|
||||
token: "secret-token",
|
||||
password: "secret-password",
|
||||
cookie: "secret-cookie",
|
||||
credentials: { apiKey: "secret-api-key" },
|
||||
}, {
|
||||
operation: "session-messages.older",
|
||||
caller: "older",
|
||||
queuedMs: "secret-queued",
|
||||
durationMs: 5,
|
||||
outcome: "complete",
|
||||
retryCount: { value: "secret-retry" },
|
||||
recordCount: Number.POSITIVE_INFINITY,
|
||||
at: 1_300,
|
||||
}, {
|
||||
operation: "secret-operation",
|
||||
caller: "secret-caller",
|
||||
durationMs: { secret: "secret-duration" },
|
||||
outcome: "secret-outcome",
|
||||
at: 1_300,
|
||||
}], 1_000)
|
||||
|
||||
assert.deepEqual(projected, {
|
||||
bufferAtCapacity: false,
|
||||
events: [{
|
||||
operation: "session-messages.initial",
|
||||
caller: "initial",
|
||||
queuedMs: 3,
|
||||
requestLimit: 50,
|
||||
cursorPresent: false,
|
||||
durationMs: 17,
|
||||
outcome: "complete",
|
||||
retryCount: 1,
|
||||
recordCount: 50,
|
||||
offsetMs: 250,
|
||||
}, {
|
||||
operation: "session-messages.older",
|
||||
caller: "older",
|
||||
durationMs: 5,
|
||||
outcome: "complete",
|
||||
offsetMs: 300,
|
||||
}],
|
||||
})
|
||||
|
||||
const serialized = JSON.stringify(projected)
|
||||
for (const secret of [
|
||||
"secret-runtime",
|
||||
"/secret/worktree",
|
||||
"secret-session",
|
||||
"secret-message",
|
||||
"secret-content",
|
||||
"secret-token",
|
||||
"secret-password",
|
||||
"secret-cookie",
|
||||
"secret-api-key",
|
||||
"secret-operation",
|
||||
"secret-caller",
|
||||
"secret-duration",
|
||||
"secret-outcome",
|
||||
"secret-queued",
|
||||
"secret-retry",
|
||||
]) {
|
||||
assert.equal(serialized.includes(secret), false)
|
||||
}
|
||||
})
|
||||
|
||||
test("session-load summary reports when the source buffer is at capacity", () => {
|
||||
const events = Array.from({ length: 1000 }, () => ({ at: 1_000 }))
|
||||
assert.equal(projectSessionLoadPerformance(events, 1_000).bufferAtCapacity, true)
|
||||
})
|
||||
|
||||
test("session-load summary rejects an invalid recording timestamp", () => {
|
||||
assert.deepEqual(projectSessionLoadPerformance([{
|
||||
operation: "session-messages.initial",
|
||||
caller: "initial",
|
||||
durationMs: 1,
|
||||
outcome: "complete",
|
||||
at: 1_000,
|
||||
}], Number.NaN), {
|
||||
bufferAtCapacity: false,
|
||||
events: [],
|
||||
})
|
||||
})
|
||||
@@ -41,7 +41,12 @@ sensitive URL parameters. The trace applies the same key and URL-parameter
|
||||
redaction, but profiling artifacts can still reveal project paths and endpoint
|
||||
names. Do not publish them without review.
|
||||
|
||||
The capture bypasses the PWA service worker and reloads without the browser cache before recording, so repeated optimization runs execute the current local build instead of a previously cached bundle. Network recording begins after that reload, so startup asset downloads are not included in the HAR totals.
|
||||
`summary.json.sessionLoadPerformance.events` contains the bounded session-loading
|
||||
operation timeline without runtime keys, directories, session IDs, message
|
||||
content, or credentials. It includes recording-relative timing, caller, outcome,
|
||||
retry count, and downloaded record count where available.
|
||||
|
||||
The capture bypasses the PWA service worker and reloads without the browser cache before recording, so repeated optimization runs execute the current local build instead of a previously cached bundle. By default, network recording begins after that preparation reload. Pass `--reload` to perform another cache-bypassing reload after recording starts and include startup requests in the HAR and session-load timeline.
|
||||
|
||||
Useful options:
|
||||
|
||||
@@ -49,6 +54,7 @@ Useful options:
|
||||
bun run profile:browser -- --duration 120
|
||||
bun run profile:browser -- --url http://localhost:4173
|
||||
bun run profile:browser -- --output /tmp/openchamber-profile
|
||||
bun run profile:browser -- --reload --no-prompt --duration 60
|
||||
```
|
||||
|
||||
Run `bun run profile:browser -- --help` for all options.
|
||||
|
||||
@@ -9,6 +9,8 @@ import { join, resolve } from "node:path"
|
||||
import { createInterface } from "node:readline/promises"
|
||||
import process from "node:process"
|
||||
|
||||
import { projectSessionLoadPerformance } from "./profile-browser-session-load.mjs"
|
||||
|
||||
const HELP = `Usage: bun run profile:browser -- [options]
|
||||
|
||||
Options:
|
||||
@@ -19,6 +21,7 @@ Options:
|
||||
--profile-dir <path> Reusable isolated Chrome profile
|
||||
--headless Run without a visible browser
|
||||
--no-prompt Start after a 5 second preparation delay
|
||||
--reload Reload after recording starts to capture startup
|
||||
--help Show this help
|
||||
|
||||
The command records a Chrome performance trace, a redacted HAR, browser metrics,
|
||||
@@ -33,12 +36,14 @@ const parseArgs = (argv) => {
|
||||
profileDir: join(homedir(), ".openchamber", "browser-profile-google-chrome"),
|
||||
headless: false,
|
||||
prompt: true,
|
||||
reload: false,
|
||||
}
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const value = argv[index]
|
||||
if (value === "--help") return { ...options, help: true }
|
||||
if (value === "--headless") options.headless = true
|
||||
else if (value === "--no-prompt") options.prompt = false
|
||||
else if (value === "--reload") options.reload = true
|
||||
else if (value === "--url") options.url = argv[++index]
|
||||
else if (value === "--duration") options.duration = Number(argv[++index])
|
||||
else if (value === "--output") options.output = argv[++index]
|
||||
@@ -374,6 +379,7 @@ const main = async () => {
|
||||
await evaluateValue(client, `
|
||||
localStorage.setItem("openchamber_sync_perf", "1")
|
||||
localStorage.setItem("openchamber_stream_perf", "1")
|
||||
localStorage.setItem("openchamber_session_load_perf", "1")
|
||||
`)
|
||||
const reloaded = client.once("Page.loadEventFired", 30_000)
|
||||
await client.send("Page.reload", { ignoreCache: true })
|
||||
@@ -392,6 +398,7 @@ const main = async () => {
|
||||
await evaluateValue(client, `window.__openchamberSyncPerformance?.reset()`)
|
||||
await evaluateValue(client, `window.__openchamberStreamPerformance?.setEnabled(true)`)
|
||||
await evaluateValue(client, `window.__openchamberStreamPerformance?.reset()`)
|
||||
await evaluateValue(client, `if (window.__openchamberSessionLoadPerformance) window.__openchamberSessionLoadPerformance.events.length = 0`)
|
||||
const records = new Map()
|
||||
const traceEvents = []
|
||||
const startedAt = new Date().toISOString()
|
||||
@@ -440,11 +447,21 @@ const main = async () => {
|
||||
})
|
||||
|
||||
console.log(`Recording for ${options.duration} seconds. Use OpenChamber normally during this window.`)
|
||||
await wait(options.duration * 1000)
|
||||
const recordingStartedAt = Date.now()
|
||||
if (options.reload) {
|
||||
const recordedReload = client.once("Page.loadEventFired", 30_000)
|
||||
await client.send("Page.reload", { ignoreCache: true })
|
||||
await recordedReload
|
||||
}
|
||||
await wait(Math.max(0, options.duration * 1000 - (Date.now() - recordingStartedAt)))
|
||||
const afterMetrics = metricMap((await client.send("Performance.getMetrics")).metrics)
|
||||
const afterHeap = await client.send("Runtime.getHeapUsage")
|
||||
const syncCounters = await evaluateValue(client, `window.__openchamberSyncPerformance?.getSnapshot() ?? null`)
|
||||
const streamPerformance = await evaluateValue(client, `window.__openchamberStreamPerformance?.getSnapshot() ?? null`)
|
||||
const sessionLoadPerformance = await evaluateValue(
|
||||
client,
|
||||
`(${projectSessionLoadPerformance.toString()})(window.__openchamberSessionLoadPerformance?.events ?? [], ${JSON.stringify(recordingStartedAt)})`,
|
||||
)
|
||||
const traceCompleteEvent = client.once("Tracing.tracingComplete", 120_000)
|
||||
let traceComplete = true
|
||||
try {
|
||||
@@ -482,6 +499,8 @@ const main = async () => {
|
||||
heapAfter: afterHeap,
|
||||
syncCounters,
|
||||
streamPerformance,
|
||||
sessionLoadPerformance,
|
||||
includesRecordedReload: options.reload,
|
||||
traceComplete,
|
||||
traceFileComplete: false,
|
||||
privacy: "Headers and sensitive URL parameters are redacted. Response bodies are not captured.",
|
||||
|
||||
Reference in New Issue
Block a user