fix: improve startup readiness performance
This commit is contained in:
@@ -620,10 +620,10 @@ async function performConfigRefresh(options: {
|
||||
const sdkRefreshTasks: Promise<void>[] = [];
|
||||
for (const directory of directoriesToRefresh) {
|
||||
if (refreshProviders) {
|
||||
sdkRefreshTasks.push(configStore.loadProviders({ directory }).then(() => undefined));
|
||||
sdkRefreshTasks.push(configStore.loadProviders({ directory, source: 'agentsStore:refreshConfig' }).then(() => undefined));
|
||||
}
|
||||
if (refreshSdkAgents) {
|
||||
sdkRefreshTasks.push(configStore.loadAgents({ directory }).then(() => undefined));
|
||||
sdkRefreshTasks.push(configStore.loadAgents({ directory, source: 'agentsStore:refreshConfig' }).then(() => undefined));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useDirectoryStore } from "@/stores/useDirectoryStore";
|
||||
import { streamDebugEnabled } from "@/stores/utils/streamDebug";
|
||||
import { parseModelIdentifier } from "@/lib/modelIdentifier";
|
||||
import { runtimeFetch } from "@/lib/runtime-fetch";
|
||||
import { markStartupTrace, measureStartupTrace } from "@/lib/startupTrace";
|
||||
|
||||
const MODELS_DEV_API_URL = "https://models.dev/api.json";
|
||||
const MODELS_DEV_PROXY_URL = "/api/openchamber/models-metadata";
|
||||
@@ -61,6 +62,18 @@ interface OpenChamberDefaults {
|
||||
}
|
||||
|
||||
const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
markStartupTrace('config.defaults:start');
|
||||
const started = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
const finish = (source: string, result: OpenChamberDefaults) => {
|
||||
const ended = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
markStartupTrace('config.defaults:end', {
|
||||
source,
|
||||
durationMs: Math.round(ended - started),
|
||||
hasDefaultModel: Boolean(result.defaultModel),
|
||||
hasDefaultAgent: Boolean(result.defaultAgent),
|
||||
});
|
||||
return result;
|
||||
};
|
||||
try {
|
||||
// 1. Runtime settings API (VSCode)
|
||||
const runtimeSettings = getRegisteredRuntimeAPIs()?.settings;
|
||||
@@ -86,7 +99,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
const sttSilenceThresholdDb = normalizeSttSilenceThresholdDb(data?.sttSilenceThresholdDb);
|
||||
const sttSilenceHoldMs = normalizeSttSilenceHoldMs(data?.sttSilenceHoldMs);
|
||||
|
||||
return {
|
||||
return finish('runtime-settings', {
|
||||
defaultModel: defaultModel.length > 0 ? defaultModel : undefined,
|
||||
defaultVariant: defaultVariant.length > 0 ? defaultVariant : undefined,
|
||||
defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined,
|
||||
@@ -101,7 +114,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
sttLanguage,
|
||||
sttSilenceThresholdDb,
|
||||
sttSilenceHoldMs,
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Fall through to fetch
|
||||
@@ -114,7 +127,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) {
|
||||
return {};
|
||||
return finish('settings-route-not-ok', {});
|
||||
}
|
||||
const data = await response.json();
|
||||
const defaultModel = typeof data?.defaultModel === 'string' ? data.defaultModel.trim() : '';
|
||||
@@ -134,7 +147,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
const sttSilenceThresholdDb = normalizeSttSilenceThresholdDb(data?.sttSilenceThresholdDb);
|
||||
const sttSilenceHoldMs = normalizeSttSilenceHoldMs(data?.sttSilenceHoldMs);
|
||||
|
||||
return {
|
||||
return finish('settings-route', {
|
||||
defaultModel: defaultModel.length > 0 ? defaultModel : undefined,
|
||||
defaultVariant: defaultVariant.length > 0 ? defaultVariant : undefined,
|
||||
defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined,
|
||||
@@ -149,9 +162,10 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
sttLanguage,
|
||||
sttSilenceThresholdDb,
|
||||
sttSilenceHoldMs,
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
});
|
||||
} catch (error) {
|
||||
markStartupTrace('config.defaults:error', { error: error instanceof Error ? error.message : String(error) });
|
||||
return finish('error', {});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -458,9 +472,11 @@ const ensureModelsMetadataFetch = (
|
||||
return;
|
||||
}
|
||||
|
||||
modelsMetadataInFlight = fetchModelsDevMetadata()
|
||||
markStartupTrace('modelsMetadata:queued');
|
||||
modelsMetadataInFlight = measureStartupTrace('modelsMetadata', fetchModelsDevMetadata)
|
||||
.then((metadata) => {
|
||||
if (metadata.size > 0) {
|
||||
markStartupTrace('modelsMetadata:set', { entries: metadata.size });
|
||||
setModelsMetadata(metadata);
|
||||
}
|
||||
return metadata;
|
||||
@@ -602,8 +618,8 @@ interface ConfigStore {
|
||||
|
||||
activateDirectory: (directory: string | null | undefined) => Promise<void>;
|
||||
|
||||
loadProviders: (options?: { directory?: string | null }) => Promise<void>;
|
||||
loadAgents: (options?: { directory?: string | null }) => Promise<boolean>;
|
||||
loadProviders: (options?: { directory?: string | null; source?: string }) => Promise<void>;
|
||||
loadAgents: (options?: { directory?: string | null; source?: string }) => Promise<boolean>;
|
||||
invalidateModelMetadataCache: () => void;
|
||||
setProvider: (providerId: string) => void;
|
||||
setModel: (modelId: string) => void;
|
||||
@@ -906,10 +922,14 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
})(),
|
||||
activateDirectory: async (directory) => {
|
||||
const directoryKey = toDirectoryKey(directory);
|
||||
let snapshotHadProviders = false;
|
||||
let snapshotHadAgents = false;
|
||||
|
||||
set((state) => {
|
||||
const snapshot = state.directoryScoped[directoryKey];
|
||||
if (snapshot) {
|
||||
snapshotHadProviders = snapshot.providers.length > 0;
|
||||
snapshotHadAgents = snapshot.agents.length > 0;
|
||||
return {
|
||||
activeDirectoryKey: directoryKey,
|
||||
providers: snapshot.providers,
|
||||
@@ -941,18 +961,36 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
return;
|
||||
}
|
||||
|
||||
await get().loadProviders({ directory: fromDirectoryKey(directoryKey) });
|
||||
await get().loadAgents({ directory: fromDirectoryKey(directoryKey) });
|
||||
if (snapshotHadProviders) {
|
||||
markStartupTrace('activateDirectory:skipProviders', { directoryKey });
|
||||
} else {
|
||||
await get().loadProviders({ directory: fromDirectoryKey(directoryKey), source: 'activateDirectory' });
|
||||
}
|
||||
|
||||
if (snapshotHadAgents) {
|
||||
markStartupTrace('activateDirectory:skipAgents', { directoryKey });
|
||||
} else {
|
||||
await get().loadAgents({ directory: fromDirectoryKey(directoryKey), source: 'activateDirectory' });
|
||||
}
|
||||
},
|
||||
|
||||
loadProviders: async (options) => {
|
||||
const directoryKey = toDirectoryKey(options?.directory ?? fromDirectoryKey(get().activeDirectoryKey));
|
||||
const requestedDirectory = options?.directory ?? fromDirectoryKey(get().activeDirectoryKey);
|
||||
const effectiveDirectory = requestedDirectory ?? opencodeClient.getDirectory() ?? null;
|
||||
const directoryKey = toDirectoryKey(requestedDirectory);
|
||||
const source = options?.source ?? 'unknown';
|
||||
markStartupTrace('loadProviders:called', { directoryKey, source, requestedDirectory, effectiveDirectory });
|
||||
|
||||
// Dedup: if a load is already in-flight for this directory, reuse it
|
||||
const existing = _inFlightProviders.get(directoryKey);
|
||||
if (existing) return existing;
|
||||
if (existing) {
|
||||
markStartupTrace('loadProviders:deduped', { directoryKey, source, requestedDirectory, effectiveDirectory });
|
||||
return existing;
|
||||
}
|
||||
|
||||
const promise = (async () => {
|
||||
const loaderStarted = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
markStartupTrace('loadProviders:start', { directoryKey, source, requestedDirectory, effectiveDirectory });
|
||||
const existingSnapshot = get().directoryScoped[directoryKey];
|
||||
const previousProviders = existingSnapshot?.providers ?? (get().activeDirectoryKey === directoryKey ? get().providers : []);
|
||||
const previousDefaults = existingSnapshot?.defaultProviders ?? (get().activeDirectoryKey === directoryKey ? get().defaultProviders : {});
|
||||
@@ -964,9 +1002,13 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
() => get().modelsMetadata,
|
||||
(metadata) => set({ modelsMetadata: metadata }),
|
||||
);
|
||||
const apiResult = await opencodeClient.withDirectory(
|
||||
fromDirectoryKey(directoryKey),
|
||||
() => opencodeClient.getProviders()
|
||||
const apiResult = await measureStartupTrace(
|
||||
'loadProviders:api',
|
||||
() => opencodeClient.withDirectory(
|
||||
fromDirectoryKey(directoryKey),
|
||||
() => opencodeClient.getProviders()
|
||||
),
|
||||
{ directoryKey, source, requestedDirectory, effectiveDirectory, attempt: attempt + 1 },
|
||||
);
|
||||
const providers = Array.isArray(apiResult?.providers) ? apiResult.providers : [];
|
||||
const defaults = apiResult?.default || {};
|
||||
@@ -1036,15 +1078,40 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
return nextState;
|
||||
});
|
||||
|
||||
const loaderEnded = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
markStartupTrace('loadProviders:end', {
|
||||
directoryKey,
|
||||
source,
|
||||
requestedDirectory,
|
||||
effectiveDirectory,
|
||||
durationMs: Math.round(loaderEnded - loaderStarted),
|
||||
providers: processedProviders.length,
|
||||
models: processedProviders.reduce((count, provider) => count + provider.models.length, 0),
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
markStartupTrace('loadProviders:attemptError', {
|
||||
directoryKey,
|
||||
source,
|
||||
requestedDirectory,
|
||||
effectiveDirectory,
|
||||
attempt: attempt + 1,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
const waitMs = 200 * (attempt + 1);
|
||||
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||
}
|
||||
}
|
||||
|
||||
console.error("Failed to load providers:", lastError);
|
||||
markStartupTrace('loadProviders:error', {
|
||||
directoryKey,
|
||||
source,
|
||||
requestedDirectory,
|
||||
effectiveDirectory,
|
||||
error: lastError instanceof Error ? lastError.message : String(lastError),
|
||||
});
|
||||
|
||||
set((state) => {
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
@@ -1314,13 +1381,22 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
},
|
||||
|
||||
loadAgents: async (options) => {
|
||||
const directoryKey = toDirectoryKey(options?.directory ?? fromDirectoryKey(get().activeDirectoryKey));
|
||||
const requestedDirectory = options?.directory ?? fromDirectoryKey(get().activeDirectoryKey);
|
||||
const effectiveDirectory = requestedDirectory ?? opencodeClient.getDirectory() ?? null;
|
||||
const directoryKey = toDirectoryKey(requestedDirectory);
|
||||
const source = options?.source ?? 'unknown';
|
||||
markStartupTrace('loadAgents:called', { directoryKey, source, requestedDirectory, effectiveDirectory });
|
||||
|
||||
// Dedup: if a load is already in-flight for this directory, reuse it
|
||||
const existing = _inFlightAgents.get(directoryKey);
|
||||
if (existing) return existing;
|
||||
if (existing) {
|
||||
markStartupTrace('loadAgents:deduped', { directoryKey, source, requestedDirectory, effectiveDirectory });
|
||||
return existing;
|
||||
}
|
||||
|
||||
const promise = (async (): Promise<boolean> => {
|
||||
const loaderStarted = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
markStartupTrace('loadAgents:start', { directoryKey, source, requestedDirectory, effectiveDirectory });
|
||||
const existingSnapshot = get().directoryScoped[directoryKey];
|
||||
const previousAgents = existingSnapshot?.agents ?? (get().activeDirectoryKey === directoryKey ? get().agents : []);
|
||||
let lastError: unknown = null;
|
||||
@@ -1329,12 +1405,22 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
try {
|
||||
// Fetch agents and OpenChamber settings in parallel
|
||||
const [agents, openChamberDefaults] = await Promise.all([
|
||||
opencodeClient.withDirectory(fromDirectoryKey(directoryKey), () => opencodeClient.listAgents()),
|
||||
measureStartupTrace(
|
||||
'loadAgents:api',
|
||||
() => opencodeClient.withDirectory(fromDirectoryKey(directoryKey), () => opencodeClient.listAgents()),
|
||||
{ directoryKey, source, requestedDirectory, effectiveDirectory, attempt: attempt + 1 },
|
||||
),
|
||||
fetchOpenChamberDefaults(),
|
||||
]);
|
||||
|
||||
const safeAgents = Array.isArray(agents) ? agents : [];
|
||||
|
||||
const providerLoad = _inFlightProviders.get(directoryKey);
|
||||
if (providerLoad) {
|
||||
markStartupTrace('loadAgents:awaitProviders', { directoryKey, source });
|
||||
await providerLoad;
|
||||
}
|
||||
|
||||
const providers = get().activeDirectoryKey === directoryKey
|
||||
? get().providers
|
||||
: (get().directoryScoped[directoryKey]?.providers ?? []);
|
||||
@@ -1452,6 +1538,15 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
return nextState;
|
||||
});
|
||||
|
||||
const loaderEnded = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
markStartupTrace('loadAgents:end', {
|
||||
directoryKey,
|
||||
source,
|
||||
requestedDirectory,
|
||||
effectiveDirectory,
|
||||
durationMs: Math.round(loaderEnded - loaderStarted),
|
||||
agents: safeAgents.length,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1592,15 +1687,39 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
});
|
||||
}
|
||||
|
||||
const loaderEnded = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
markStartupTrace('loadAgents:end', {
|
||||
directoryKey,
|
||||
source,
|
||||
requestedDirectory,
|
||||
effectiveDirectory,
|
||||
durationMs: Math.round(loaderEnded - loaderStarted),
|
||||
agents: safeAgents.length,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
markStartupTrace('loadAgents:attemptError', {
|
||||
directoryKey,
|
||||
source,
|
||||
requestedDirectory,
|
||||
effectiveDirectory,
|
||||
attempt: attempt + 1,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
const waitMs = 200 * (attempt + 1);
|
||||
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||
}
|
||||
}
|
||||
|
||||
console.error("Failed to load agents:", lastError);
|
||||
markStartupTrace('loadAgents:error', {
|
||||
directoryKey,
|
||||
source,
|
||||
requestedDirectory,
|
||||
effectiveDirectory,
|
||||
error: lastError instanceof Error ? lastError.message : String(lastError),
|
||||
});
|
||||
|
||||
set((state) => {
|
||||
const providers = state.activeDirectoryKey === directoryKey
|
||||
@@ -2064,13 +2183,31 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
},
|
||||
|
||||
checkConnection: async () => {
|
||||
markStartupTrace('checkConnection:start');
|
||||
const maxAttempts = 5;
|
||||
let attempt = 0;
|
||||
let lastError: unknown = null;
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
try {
|
||||
const isHealthy = await opencodeClient.checkHealth();
|
||||
markStartupTrace('checkConnection:attempt', { attempt: attempt + 1 });
|
||||
const isHealthy = await measureStartupTrace(
|
||||
'checkConnection:health',
|
||||
() => opencodeClient.checkHealth(),
|
||||
{ attempt: attempt + 1 },
|
||||
);
|
||||
if (!isHealthy && attempt < maxAttempts - 1) {
|
||||
const hasEverConnected = get().hasEverConnected;
|
||||
set({
|
||||
isConnected: false,
|
||||
connectionPhase: hasEverConnected ? "reconnecting" : "connecting",
|
||||
lastDisconnectReason: 'health_check_unhealthy',
|
||||
});
|
||||
attempt += 1;
|
||||
await sleep(400 * attempt);
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasEverConnected = get().hasEverConnected;
|
||||
set(isHealthy
|
||||
? { isConnected: true, hasEverConnected: true, connectionPhase: "connected" }
|
||||
@@ -2079,6 +2216,7 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
connectionPhase: hasEverConnected ? "reconnecting" : "connecting",
|
||||
lastDisconnectReason: 'health_check_unhealthy',
|
||||
});
|
||||
markStartupTrace('checkConnection:end', { healthy: isHealthy, attempts: attempt + 1 });
|
||||
return isHealthy;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
@@ -2096,15 +2234,19 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
connectionPhase: get().hasEverConnected ? "reconnecting" : "connecting",
|
||||
lastDisconnectReason: 'health_check_failed',
|
||||
});
|
||||
markStartupTrace('checkConnection:end', { healthy: false, attempts: maxAttempts });
|
||||
return false;
|
||||
},
|
||||
|
||||
initializeApp: async () => {
|
||||
if (_initializeAppInFlight) {
|
||||
markStartupTrace('initializeApp:deduped');
|
||||
return _initializeAppInFlight;
|
||||
}
|
||||
|
||||
const run = (async () => {
|
||||
const initStarted = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
markStartupTrace('initializeApp:start');
|
||||
try {
|
||||
const debug = streamDebugEnabled();
|
||||
if (debug) console.log("Starting app initialization...");
|
||||
@@ -2123,15 +2265,21 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
}
|
||||
|
||||
if (debug) console.log("Initializing app...");
|
||||
await opencodeClient.initApp();
|
||||
markStartupTrace('initApp:skipped', { reason: 'checkConnection already verified health' });
|
||||
|
||||
if (debug) console.log("Loading providers...");
|
||||
await get().loadProviders();
|
||||
|
||||
if (debug) console.log("Loading agents...");
|
||||
await get().loadAgents();
|
||||
if (debug) console.log("Loading providers and agents...");
|
||||
await Promise.all([
|
||||
get().loadProviders({ source: 'initializeApp' }),
|
||||
get().loadAgents({ source: 'initializeApp' }),
|
||||
]);
|
||||
|
||||
set({ isInitialized: true, isConnected: true, hasEverConnected: true, connectionPhase: "connected" });
|
||||
const initEnded = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
markStartupTrace('initializeApp:end', {
|
||||
durationMs: Math.round(initEnded - initStarted),
|
||||
providers: get().providers.length,
|
||||
agents: get().agents.length,
|
||||
});
|
||||
if (debug) console.log("App initialized successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize app:", error);
|
||||
@@ -2141,6 +2289,7 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
connectionPhase: get().hasEverConnected ? "reconnecting" : "connecting",
|
||||
lastDisconnectReason: 'init_error',
|
||||
});
|
||||
markStartupTrace('initializeApp:error', { error: error instanceof Error ? error.message : String(error) });
|
||||
}
|
||||
})().finally(() => {
|
||||
_initializeAppInFlight = null;
|
||||
@@ -2235,16 +2384,16 @@ let unsubscribeConfigStoreChanges: (() => void) | null = null;
|
||||
|
||||
if (!unsubscribeConfigStoreChanges) {
|
||||
unsubscribeConfigStoreChanges = subscribeToConfigChanges(async (event) => {
|
||||
const tasks: Promise<void>[] = [];
|
||||
const tasks: Promise<void>[] = [];
|
||||
|
||||
if (scopeMatches(event, "agents")) {
|
||||
const { loadAgents } = useConfigStore.getState();
|
||||
tasks.push(loadAgents().then(() => {}));
|
||||
tasks.push(loadAgents({ source: 'configChange:agents' }).then(() => {}));
|
||||
}
|
||||
|
||||
if (scopeMatches(event, "providers")) {
|
||||
const { loadProviders } = useConfigStore.getState();
|
||||
tasks.push(loadProviders());
|
||||
tasks.push(loadProviders({ source: 'configChange:providers' }));
|
||||
}
|
||||
|
||||
if (tasks.length > 0) {
|
||||
@@ -2263,6 +2412,7 @@ if (typeof window !== "undefined" && !unsubscribeConfigStoreDirectoryChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
markStartupTrace('directoryStore:changed', { previous: prevKey, next: nextKey });
|
||||
void useConfigStore.getState().activateDirectory(state.currentDirectory);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user