fix: improve startup readiness performance

This commit is contained in:
Bohdan Triapitsyn
2026-06-05 15:09:24 +03:00
parent 0b881fec8e
commit e192359da9
32 changed files with 502 additions and 110 deletions
@@ -360,8 +360,10 @@ export const settingsDict = {
'settings.projects.actions.toast.saved': 'Project actions saved',
'settings.openchamber.about.title': 'About OpenChamber',
'settings.openchamber.about.field.version': 'Version',
'settings.openchamber.about.field.openCodeVersion': 'OpenCode version',
'settings.openchamber.about.state.checking': 'Checking...',
'settings.openchamber.about.state.upToDate': 'Up to date',
'settings.openchamber.about.state.unknown': 'unknown',
'settings.openchamber.about.actions.checkUpdates': 'Check updates',
'settings.openchamber.about.actions.update': 'Update',
'settings.openchamber.about.actions.updateToVersion': 'Update to {version}',
@@ -327,8 +327,10 @@ export const settingsDict = {
"settings.projects.actions.toast.saved": "Acciones del proyecto guardadas",
"settings.openchamber.about.title": "Acerca de OpenChamber",
"settings.openchamber.about.field.version": "Versión",
"settings.openchamber.about.field.openCodeVersion": "Versión de OpenCode",
"settings.openchamber.about.state.checking": "Comprobando...",
"settings.openchamber.about.state.upToDate": "Actualizado",
"settings.openchamber.about.state.unknown": "desconocido",
"settings.openchamber.about.actions.checkUpdates": "Comprobar actualizaciones",
"settings.openchamber.about.actions.update": "Actualizar",
"settings.openchamber.about.actions.updateToVersion": "Actualizar a {version}",
@@ -327,8 +327,10 @@ export const settingsDict = {
'settings.projects.actions.toast.saved': '프로젝트 작업이 저장되었습니다',
'settings.openchamber.about.title': 'OpenChamber 정보',
'settings.openchamber.about.field.version': '버전',
'settings.openchamber.about.field.openCodeVersion': 'OpenCode 버전',
'settings.openchamber.about.state.checking': '확인 중...',
'settings.openchamber.about.state.upToDate': '최신 상태',
'settings.openchamber.about.state.unknown': '알 수 없음',
'settings.openchamber.about.actions.checkUpdates': '업데이트 확인',
'settings.openchamber.about.actions.update': '업데이트',
'settings.openchamber.about.actions.updateToVersion': '{version}으로 업데이트',
@@ -640,8 +640,10 @@ export const settingsDict = {
'settings.openchamber.about.actions.update': 'Aktualizuj',
'settings.openchamber.about.actions.updateToVersion': 'Aktualizuj do wersji {version}',
'settings.openchamber.about.field.version': 'Wersja',
'settings.openchamber.about.field.openCodeVersion': 'Wersja OpenCode',
'settings.openchamber.about.state.checking': 'Sprawdzanie...',
'settings.openchamber.about.state.upToDate': 'Aktualna wersja',
'settings.openchamber.about.state.unknown': 'nieznane',
'settings.openchamber.about.title': 'O OpenChamber',
'settings.openchamber.about.toast.latestVersion': 'Używasz najnowszej wersji',
'settings.openchamber.defaults.field.defaultAgent': 'Domyślny Agent',
@@ -327,8 +327,10 @@ export const settingsDict = {
"settings.projects.actions.toast.saved": "Ações do projeto salvas",
"settings.openchamber.about.title": "Sobre o OpenChamber",
"settings.openchamber.about.field.version": "Versão",
"settings.openchamber.about.field.openCodeVersion": "Versão do OpenCode",
"settings.openchamber.about.state.checking": "Verificando...",
"settings.openchamber.about.state.upToDate": "Atualizado",
"settings.openchamber.about.state.unknown": "desconhecido",
"settings.openchamber.about.actions.checkUpdates": "Verificar atualizações",
"settings.openchamber.about.actions.update": "Atualizar",
"settings.openchamber.about.actions.updateToVersion": "Atualizar a {version}",
@@ -327,8 +327,10 @@ export const settingsDict = {
"settings.projects.actions.toast.saved": "Дії проєкту збережено",
"settings.openchamber.about.title": "Про OpenChamber",
"settings.openchamber.about.field.version": "Версія",
"settings.openchamber.about.field.openCodeVersion": "Версія OpenCode",
"settings.openchamber.about.state.checking": "Перевірка...",
"settings.openchamber.about.state.upToDate": "В актуальному стані",
"settings.openchamber.about.state.unknown": "невідомо",
"settings.openchamber.about.actions.checkUpdates": "Перевірити оновлення",
"settings.openchamber.about.actions.update": "Оновити",
"settings.openchamber.about.actions.updateToVersion": "Оновити до {version}",
@@ -327,8 +327,10 @@ export const settingsDict = {
'settings.projects.actions.toast.saved': '项目操作已保存',
'settings.openchamber.about.title': '关于 OpenChamber',
'settings.openchamber.about.field.version': '版本',
'settings.openchamber.about.field.openCodeVersion': 'OpenCode 版本',
'settings.openchamber.about.state.checking': '检查中...',
'settings.openchamber.about.state.upToDate': '已是最新',
'settings.openchamber.about.state.unknown': '未知',
'settings.openchamber.about.actions.checkUpdates': '检查更新',
'settings.openchamber.about.actions.update': '更新',
'settings.openchamber.about.actions.updateToVersion': '更新到 {version}',
@@ -324,8 +324,10 @@
'settings.projects.actions.toast.saved': '專案操作已儲存',
'settings.openchamber.about.title': '關於 OpenChamber',
'settings.openchamber.about.field.version': '版本',
'settings.openchamber.about.field.openCodeVersion': 'OpenCode 版本',
'settings.openchamber.about.state.checking': '檢查中...',
'settings.openchamber.about.state.upToDate': '已是最新',
'settings.openchamber.about.state.unknown': '未知',
'settings.openchamber.about.actions.checkUpdates': '檢查更新',
'settings.openchamber.about.actions.update': '更新',
'settings.openchamber.about.actions.updateToVersion': '更新到 {version}',
+1 -1
View File
@@ -235,7 +235,7 @@ export const buildOpenCodeStatusReport = async (): Promise<string> => {
};
const probeTargets: Array<{ label: string; path: string; includeDirectory?: boolean; timeoutMs?: number }> = [
{ label: 'health', path: '/global/health', includeDirectory: false },
{ label: 'health', path: '/api/health', includeDirectory: false },
{ label: 'config', path: '/config', includeDirectory: true },
{ label: 'providers', path: '/config/providers', includeDirectory: true },
{ label: 'agents', path: '/agent', includeDirectory: true, timeoutMs: 12000 },
+17 -18
View File
@@ -18,6 +18,7 @@ import { waitForWorktreeBootstrap } from "@/lib/worktrees/worktreeBootstrap";
import { getRuntimeUrlResolver } from "@/lib/runtime-url";
import { runtimeFetch } from "@/lib/runtime-fetch";
import { getRegisteredRuntimeAPIs } from "@/contexts/runtimeAPIRegistry";
import { markStartupTrace } from "@/lib/startupTrace";
import {
assertProviderCircuitClosed,
recordProviderSuccess,
@@ -346,7 +347,14 @@ class OpencodeService {
// Set the current working directory for all API calls
setDirectory(directory: string | undefined) {
this.currentDirectory = this.normalizeCandidatePath(directory) ?? directory;
const normalized = this.normalizeCandidatePath(directory) ?? directory;
if (this.currentDirectory !== normalized) {
markStartupTrace('opencodeClient:setDirectory', {
previous: this.currentDirectory ?? null,
next: normalized ?? null,
});
}
this.currentDirectory = normalized;
}
getDirectory(): string | undefined {
@@ -1414,33 +1422,24 @@ class OpencodeService {
}
}
// Health Check - using /health endpoint for detailed status
// Lightweight readiness check. Full diagnostics still live at /health.
async checkHealth(): Promise<boolean> {
try {
// Health endpoint is at root, not under /api
let healthUrl: string;
const normalizedBase = this.baseUrl.endsWith('/') ? this.baseUrl.replace(/\/+$/, '') : this.baseUrl;
if (normalizedBase === '/api') {
healthUrl = '/health';
} else if (normalizedBase.endsWith('/api')) {
// Desktop: http://127.0.0.1:PORT/api -> http://127.0.0.1:PORT/health
healthUrl = `${normalizedBase.slice(0, -4)}/health`;
} else {
healthUrl = `${normalizedBase}/health`;
}
const healthUrl = normalizedBase === '/api' || normalizedBase.endsWith('/api')
? '/api/opencode/health'
: `${normalizedBase}/opencode/health`;
markStartupTrace('opencodeClient.checkHealth:url', { baseUrl: this.baseUrl, healthUrl });
const response = await runtimeFetch(healthUrl);
markStartupTrace('opencodeClient.checkHealth:response', { status: response.status });
if (!response.ok) {
return false;
}
const healthData = await response.json();
markStartupTrace('opencodeClient.checkHealth:result', { healthy: healthData?.healthy });
// Check if the upstream API is ready (not just OpenChamber server)
if (healthData.isOpenCodeReady === false) {
return false;
}
return true;
return healthData?.healthy === true;
} catch {
return false;
}
+6 -8
View File
@@ -126,14 +126,12 @@ export const runtimeFetch = async (input: string | URL | Request, init: RuntimeF
const inputHeaders = resolvedInput instanceof Request ? resolvedInput.headers : undefined;
const headers = await mergeHeaders(inputHeaders, requestInit.headers, shouldAttachRuntimeAuth(resolvedInput));
if (resolvedInput instanceof Request) {
return fetch(new Request(resolvedInput, { ...requestInit, headers }));
}
return fetch(resolvedInput, {
...requestInit,
headers,
});
return resolvedInput instanceof Request
? fetch(new Request(resolvedInput, { ...requestInit, headers }))
: fetch(resolvedInput, {
...requestInit,
headers,
});
};
let runtimeFetchBridgeInstalled = false;
+90
View File
@@ -0,0 +1,90 @@
export type StartupTraceEvent = {
t: number;
name: string;
data?: Record<string, unknown>;
};
declare global {
interface Window {
__OPENCHAMBER_STARTUP_TRACE__?: StartupTraceEvent[];
__OPENCHAMBER_STARTUP_TRACE_START__?: number;
}
}
const MAX_STARTUP_TRACE_EVENTS = 500;
const enabled = () => {
if (typeof window === 'undefined') return false;
try {
const params = new URLSearchParams(window.location.search);
return params.get('startupTrace') === '1' || window.localStorage?.getItem('OPENCHAMBER_STARTUP_TRACE') === '1';
} catch {
return false;
}
};
export const startupTraceEnabled = () => enabled();
export const markStartupTrace = (name: string, data?: Record<string, unknown>) => {
if (!enabled()) return;
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
window.__OPENCHAMBER_STARTUP_TRACE_START__ ??= now;
window.__OPENCHAMBER_STARTUP_TRACE__ ??= [];
window.__OPENCHAMBER_STARTUP_TRACE__.push({
t: Math.round(now - window.__OPENCHAMBER_STARTUP_TRACE_START__),
name,
...(data ? { data } : {}),
});
if (window.__OPENCHAMBER_STARTUP_TRACE__.length > MAX_STARTUP_TRACE_EVENTS) {
window.__OPENCHAMBER_STARTUP_TRACE__.splice(
0,
window.__OPENCHAMBER_STARTUP_TRACE__.length - MAX_STARTUP_TRACE_EVENTS,
);
}
};
export const getStartupTraceSummary = () => {
const trace = typeof window !== 'undefined' ? window.__OPENCHAMBER_STARTUP_TRACE__ ?? [] : [];
const readyIndex = trace.findIndex((event) => event.name === 'ModelControls:ready');
const endIndex = readyIndex >= 0 ? Math.min(trace.length, readyIndex + 8) : trace.length;
return trace.slice(0, endIndex).filter((event) => (
event.name.includes('checkConnection')
|| event.name.includes('checkHealth')
|| event.name.includes('initializeApp')
|| event.name.includes('initApp')
|| event.name.includes('loadProviders')
|| event.name.includes('loadAgents')
|| event.name.includes('config.defaults')
|| event.name.includes('modelsMetadata')
|| event.name.includes('ModelControls')
|| event.name.includes('activateDirectory')
|| event.name.includes('opencodeClient:setDirectory')
));
};
if (typeof window !== 'undefined') {
(window as typeof window & { __OPENCHAMBER_STARTUP_TRACE_SUMMARY__?: typeof getStartupTraceSummary })
.__OPENCHAMBER_STARTUP_TRACE_SUMMARY__ = getStartupTraceSummary;
}
export const measureStartupTrace = async <T>(
name: string,
fn: () => Promise<T>,
data?: Record<string, unknown>,
): Promise<T> => {
const started = typeof performance !== 'undefined' ? performance.now() : Date.now();
markStartupTrace(`${name}:start`, data);
try {
const result = await fn();
const ended = typeof performance !== 'undefined' ? performance.now() : Date.now();
markStartupTrace(`${name}:end`, { durationMs: Math.round(ended - started) });
return result;
} catch (error) {
const ended = typeof performance !== 'undefined' ? performance.now() : Date.now();
markStartupTrace(`${name}:error`, {
durationMs: Math.round(ended - started),
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
};