fix: improve startup readiness performance
This commit is contained in:
@@ -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}',
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user