perf(startup): defer noncritical startup probes

This commit is contained in:
777genius 2026-05-23 15:04:05 +03:00
parent a4861fa77d
commit 34a1b86b21
13 changed files with 353 additions and 53 deletions

View file

@ -11,6 +11,7 @@ const CODEX_PENDING_LOGIN_REFRESH_MS = 3_000;
const CODEX_VISIBLE_RATE_LIMITS_REFRESH_MS = 10_000;
const CODEX_VISIBLE_STANDARD_REFRESH_MS = 20_000;
const CODEX_HIDDEN_REFRESH_MS = 60_000;
export const CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS = 30_000;
function isDocumentVisible(): boolean {
if (typeof document === 'undefined') {
@ -41,6 +42,7 @@ function getRefreshIntervalMs(options: {
export function useCodexAccountSnapshot(options: {
enabled: boolean;
includeRateLimits?: boolean;
initialRefreshDelayMs?: number;
}): {
snapshot: CodexAccountSnapshotDto | null;
loading: boolean;
@ -62,6 +64,7 @@ export function useCodexAccountSnapshot(options: {
const [error, setError] = useState<string | null>(null);
const [visible, setVisible] = useState(() => isDocumentVisible());
const lastUpdatedAtRef = useRef<number | null>(null);
const initialRefreshDelayMs = options.initialRefreshDelayMs ?? 0;
const applySnapshot = useCallback((nextSnapshot: CodexAccountSnapshotDto) => {
lastUpdatedAtRef.current = Date.now();
@ -117,38 +120,74 @@ export function useCodexAccountSnapshot(options: {
return;
}
setLoading(true);
if (options.includeRateLimits) {
setRateLimitsLoading(true);
}
setError(null);
let active = true;
let initialRefreshTimer: number | null = null;
const initialSnapshotRequest = options.includeRateLimits
? api.refreshCodexAccountSnapshot({
includeRateLimits: true,
const startInitialSnapshotRequest = (): void => {
if (!active || lastUpdatedAtRef.current !== null) {
return;
}
setLoading(true);
if (options.includeRateLimits) {
setRateLimitsLoading(true);
}
setError(null);
const initialSnapshotRequest = options.includeRateLimits
? api.refreshCodexAccountSnapshot({
includeRateLimits: true,
})
: api.getCodexAccountSnapshot();
void initialSnapshotRequest
.then((nextSnapshot) => {
if (active) {
applySnapshot(nextSnapshot);
}
})
: api.getCodexAccountSnapshot();
.catch((nextError) => {
if (active) {
setError(
nextError instanceof Error ? nextError.message : 'Failed to load Codex account'
);
}
})
.finally(() => {
if (!active) {
return;
}
setLoading(false);
if (options.includeRateLimits) {
setRateLimitsLoading(false);
}
});
};
void initialSnapshotRequest
.then((nextSnapshot) => {
applySnapshot(nextSnapshot);
})
.catch((nextError) => {
setError(nextError instanceof Error ? nextError.message : 'Failed to load Codex account');
})
.finally(() => {
setLoading(false);
if (options.includeRateLimits) {
setRateLimitsLoading(false);
}
});
if (initialRefreshDelayMs > 0) {
initialRefreshTimer = window.setTimeout(startInitialSnapshotRequest, initialRefreshDelayMs);
} else {
startInitialSnapshotRequest();
}
const unsubscribe = api.onCodexAccountSnapshotChanged((_event, nextSnapshot) => {
applySnapshot(nextSnapshot);
});
return unsubscribe;
}, [applySnapshot, electronMode, options.enabled, options.includeRateLimits]);
return () => {
active = false;
if (initialRefreshTimer) {
window.clearTimeout(initialRefreshTimer);
}
unsubscribe();
};
}, [
applySnapshot,
electronMode,
initialRefreshDelayMs,
options.enabled,
options.includeRateLimits,
]);
useEffect(() => {
if (!electronMode || !options.enabled || typeof document === 'undefined') {
@ -167,6 +206,10 @@ export function useCodexAccountSnapshot(options: {
? CODEX_VISIBLE_RATE_LIMITS_REFRESH_MS
: CODEX_VISIBLE_STANDARD_REFRESH_MS;
if (initialRefreshDelayMs > 0 && lastUpdatedAtRef.current === null && snapshot === null) {
return;
}
if (
lastUpdatedAtRef.current === null ||
Date.now() - lastUpdatedAtRef.current >= staleAfterMs
@ -182,12 +225,22 @@ export function useCodexAccountSnapshot(options: {
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [electronMode, options.enabled, options.includeRateLimits, refresh]);
}, [
electronMode,
initialRefreshDelayMs,
options.enabled,
options.includeRateLimits,
refresh,
snapshot,
]);
useEffect(() => {
if (!electronMode || !options.enabled) {
return;
}
if (initialRefreshDelayMs > 0 && snapshot === null) {
return;
}
const refreshIntervalMs = getRefreshIntervalMs({
loginStatus: snapshot?.login.status,
@ -206,9 +259,11 @@ export function useCodexAccountSnapshot(options: {
};
}, [
electronMode,
initialRefreshDelayMs,
options.enabled,
options.includeRateLimits,
refresh,
snapshot,
snapshot?.login.status,
visible,
]);

View file

@ -1,4 +1,7 @@
export { useCodexAccountSnapshot } from './hooks/useCodexAccountSnapshot';
export {
CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS,
useCodexAccountSnapshot,
} from './hooks/useCodexAccountSnapshot';
export { mergeCodexCliStatusWithSnapshot } from './mergeCodexCliStatusWithSnapshot';
export { mergeCodexProviderStatusWithSnapshot } from './mergeCodexProviderStatusWithSnapshot';
export {

View file

@ -354,10 +354,18 @@ function describeMemberWorkSyncReviewPickupEscalationReason(reason: string): str
return 'The current review request is still waiting for explicit review pickup.';
}
async function resolveOpenCodeRuntimeBinaryForBridgeEnv(): Promise<string | null> {
const resolvedBinaryPath = await resolveVerifiedOpenCodeRuntimeBinaryPath();
async function resolveOpenCodeRuntimeBinaryForBridgeEnv(options?: {
includeShellEnv?: boolean;
}): Promise<string | null> {
const resolvedBinaryPath = await resolveVerifiedOpenCodeRuntimeBinaryPath({
includeShellEnv: options?.includeShellEnv,
});
if (resolvedBinaryPath) return resolvedBinaryPath;
if (options?.includeShellEnv === false) {
return null;
}
try {
const status = await openCodeRuntimeInstallerService?.getStatus();
return status?.installed === true && status.binaryPath ? status.binaryPath : null;
@ -448,15 +456,18 @@ async function createOpenCodeRuntimeAdapterRegistry(
copyOpenCodeLocalMcpLaunchEnv(targetEnv, bridgeEnv);
}
};
const ensureOpenCodeRuntimeBinaryEnv = async (targetEnv: NodeJS.ProcessEnv): Promise<void> => {
const ensureOpenCodeRuntimeBinaryEnv = async (
targetEnv: NodeJS.ProcessEnv,
options: { includeShellEnv?: boolean } = {}
): Promise<void> => {
await ensureOpenCodeBridgeRuntimeBinaryEnv({
targetEnv,
bridgeEnv,
resolveVerifiedOpenCodeRuntimeBinaryPath: resolveOpenCodeRuntimeBinaryForBridgeEnv,
resolveVerifiedOpenCodeRuntimeBinaryPath: () =>
resolveOpenCodeRuntimeBinaryForBridgeEnv({ includeShellEnv: options.includeShellEnv }),
onWarning: (message) => logger.warn(message),
});
};
await ensureOpenCodeRuntimeBinaryEnv(bridgeEnv);
try {
reportProgress('runtime-work-sync', 'Preparing runtime work sync hooks...');
const turnSettledEnv = await buildMemberWorkSyncRuntimeTurnSettledEnvironment({
@ -500,7 +511,7 @@ async function createOpenCodeRuntimeAdapterRegistry(
reportProgress('runtime-bridge', 'Preparing OpenCode bridge...');
const resolveBridgeCommandEnv = async (): Promise<NodeJS.ProcessEnv> => {
const nextEnv = { ...bridgeEnv };
await ensureOpenCodeRuntimeBinaryEnv(nextEnv);
await ensureOpenCodeRuntimeBinaryEnv(nextEnv, { includeShellEnv: true });
if (!useHttpMcpBridge) {
return nextEnv;
}

View file

@ -1030,13 +1030,17 @@ export class CliInstallerService {
providerStatusMode: CliInstallerProviderStatusMode
): Promise<void> {
resetGatherDiag(diag);
const shellEnvStartedAt = Date.now();
await resolveInteractiveShellEnvBestEffort({
timeoutMs: 1_500,
fallbackEnv: process.env,
background: false,
});
diag.shellEnvMs = Date.now() - shellEnvStartedAt;
if (providerStatusMode === 'defer') {
diag.shellEnvMs = 0;
} else {
const shellEnvStartedAt = Date.now();
await resolveInteractiveShellEnvBestEffort({
timeoutMs: 1_500,
fallbackEnv: process.env,
background: false,
});
diag.shellEnvMs = Date.now() - shellEnvStartedAt;
}
const r = ref.current;
const binaryResolveStartedAt = Date.now();

View file

@ -212,8 +212,13 @@ async function probeFirstWorkingOpenCodeBinaryCandidate(
return { ok: false, firstFailure: nextFirstFailure };
}
interface OpenCodeRuntimeBinaryResolveOptions {
shellEnvTimeoutMs?: number;
includeShellEnv?: boolean;
}
async function probeFirstWorkingPathOpenCodeBinary(
options: { shellEnvTimeoutMs?: number } = {}
options: OpenCodeRuntimeBinaryResolveOptions = {}
): Promise<VerifiedOpenCodeBinaryProbe> {
const seenCandidates = new Set<string>();
let firstFailure: { binaryPath: string; error: string } | null = null;
@ -230,6 +235,16 @@ async function probeFirstWorkingPathOpenCodeBinary(
}
firstFailure = cachedProbe.firstFailure;
if (options.includeShellEnv === false) {
return probeFirstWorkingOpenCodeBinaryCandidate(
collectPathOpenCodeBinaryCandidates([], {
includeFallbackPathEntries: true,
}),
seenCandidates,
firstFailure
);
}
const shellEnv = await resolveInteractiveShellEnvBestEffort({
timeoutMs: options.shellEnvTimeoutMs ?? RUNTIME_PATH_SHELL_ENV_TIMEOUT_MS,
fallbackEnv: process.env,
@ -254,14 +269,14 @@ async function probeFirstWorkingPathOpenCodeBinary(
}
async function resolveVerifiedPathOpenCodeBinaryPath(
options: { shellEnvTimeoutMs?: number } = {}
options: OpenCodeRuntimeBinaryResolveOptions = {}
): Promise<string | null> {
const result = await probeFirstWorkingPathOpenCodeBinary(options);
return result.ok ? result.binaryPath : null;
}
export async function resolveVerifiedOpenCodeRuntimeBinaryPath(
options: { shellEnvTimeoutMs?: number } = {}
options: OpenCodeRuntimeBinaryResolveOptions = {}
): Promise<string | null> {
return (
(await resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath()) ??

View file

@ -386,15 +386,6 @@ async function resolveNodePath(options?: McpLaunchSpecResolveOptions): Promise<s
if (_resolvedNodePath) return _resolvedNodePath;
emitProgress(options, 'node-runtime', 'Resolving Node.js runtime for MCP server...');
if (shouldPreferShellNodeProbe()) {
const shellProbe = await probeShellNodeRuntimePath(options);
if (shellProbe.ok) {
_resolvedNodePath = shellProbe.path;
emitProgress(options, 'node-runtime-found', 'Using resolved Node.js runtime...');
return _resolvedNodePath;
}
}
const fastProbe = await probeNodeRuntimePath(buildNodeResolveEnv({}));
if (fastProbe.ok) {
_resolvedNodePath = fastProbe.path;
@ -402,6 +393,9 @@ async function resolveNodePath(options?: McpLaunchSpecResolveOptions): Promise<s
return _resolvedNodePath;
}
if (shouldPreferShellNodeProbe()) {
emitProgress(options, 'node-runtime-shell-fallback', 'Trying login shell Node.js runtime...');
}
const shellProbe = await probeShellNodeRuntimePath(options);
if (shellProbe.ok) {
_resolvedNodePath = shellProbe.path;

View file

@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import {
CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS,
mergeCodexCliStatusWithSnapshot,
useCodexAccountSnapshot,
} from '@features/codex-account/renderer';
@ -123,6 +124,7 @@ export const GlobalProviderStatusHeader = (): React.JSX.Element | null => {
loadingCliStatus?.flavor === 'agent_teams_orchestrator' &&
Boolean(loadingCliStatus?.providers.some((provider) => provider.providerId === 'codex')),
includeRateLimits: false,
initialRefreshDelayMs: CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS,
});
const effectiveCliStatus = useMemo(

View file

@ -10,6 +10,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS,
mergeCodexProviderStatusWithSnapshot,
useCodexAccountSnapshot,
} from '@features/codex-account/renderer';
@ -1358,6 +1359,7 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
loadingCliStatus?.flavor === 'agent_teams_orchestrator' &&
Boolean(loadingCliStatus?.providers.some((provider) => provider.providerId === 'codex')),
includeRateLimits: true,
initialRefreshDelayMs: CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS,
});
const visibleCliProviders = useMemo(
() =>

View file

@ -7,6 +7,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS,
mergeCodexProviderStatusWithSnapshot,
useCodexAccountSnapshot,
} from '@features/codex-account/renderer';
@ -176,6 +177,7 @@ export const ExtensionStoreView = (): React.JSX.Element => {
)
),
includeRateLimits: true,
initialRefreshDelayMs: CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS,
});
const codexSnapshotPending =
codexAccount.loading &&

View file

@ -1,5 +1,6 @@
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useCodexAccountSnapshot } from '../../../../src/features/codex-account/renderer/hooks/useCodexAccountSnapshot';
@ -124,9 +125,146 @@ describe('useCodexAccountSnapshot', () => {
});
});
it('can defer the initial Codex snapshot without starting interval refreshes first', async () => {
vi.useFakeTimers();
const snapshot = createSnapshot();
apiMocks.refreshCodexAccountSnapshot.mockResolvedValue(snapshot);
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
function Harness(): React.ReactElement {
const state = useCodexAccountSnapshot({
enabled: true,
includeRateLimits: true,
initialRefreshDelayMs: 30_000,
});
return React.createElement(
'div',
{ 'data-loading': state.loading ? 'true' : 'false' },
state.snapshot?.managedAccount?.email ?? 'empty'
);
}
await act(async () => {
root.render(React.createElement(Harness));
await Promise.resolve();
});
expect(apiMocks.refreshCodexAccountSnapshot).not.toHaveBeenCalled();
expect(host.firstElementChild?.getAttribute('data-loading')).toBe('false');
await act(async () => {
vi.advanceTimersByTime(20_000);
await Promise.resolve();
});
expect(apiMocks.refreshCodexAccountSnapshot).not.toHaveBeenCalled();
await act(async () => {
vi.advanceTimersByTime(10_000);
await Promise.resolve();
});
expect(apiMocks.refreshCodexAccountSnapshot).toHaveBeenCalledTimes(1);
expect(apiMocks.refreshCodexAccountSnapshot).toHaveBeenCalledWith({
includeRateLimits: true,
});
expect(host.textContent).toContain('belief@example.com');
act(() => {
root.unmount();
});
});
it('clears a deferred initial Codex snapshot timer on unmount', async () => {
vi.useFakeTimers();
apiMocks.refreshCodexAccountSnapshot.mockResolvedValue(createSnapshot());
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
function Harness(): React.ReactElement {
useCodexAccountSnapshot({
enabled: true,
includeRateLimits: true,
initialRefreshDelayMs: 30_000,
});
return React.createElement('div', null, 'mounted');
}
await act(async () => {
root.render(React.createElement(Harness));
await Promise.resolve();
});
act(() => {
root.unmount();
});
await act(async () => {
vi.advanceTimersByTime(30_000);
await Promise.resolve();
});
expect(apiMocks.refreshCodexAccountSnapshot).not.toHaveBeenCalled();
});
it('does not run the deferred initial snapshot after a manual refresh already loaded one', async () => {
vi.useFakeTimers();
Object.defineProperty(document, 'visibilityState', {
configurable: true,
get: () => 'hidden' satisfies DocumentVisibilityState,
});
const snapshot = createSnapshot();
apiMocks.refreshCodexAccountSnapshot.mockResolvedValue(snapshot);
let refreshNow!: () => Promise<void>;
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
function Harness(): React.ReactElement {
const state = useCodexAccountSnapshot({
enabled: true,
includeRateLimits: true,
initialRefreshDelayMs: 30_000,
});
refreshNow = () => state.refresh({ includeRateLimits: true });
return React.createElement('div', null, state.snapshot?.managedAccount?.email ?? 'empty');
}
await act(async () => {
root.render(React.createElement(Harness));
await Promise.resolve();
});
await act(async () => {
await refreshNow();
});
expect(apiMocks.refreshCodexAccountSnapshot).toHaveBeenCalledTimes(1);
await act(async () => {
vi.advanceTimersByTime(30_000);
await Promise.resolve();
});
expect(apiMocks.refreshCodexAccountSnapshot).toHaveBeenCalledTimes(1);
act(() => {
root.unmount();
});
});
it('refreshes rate-limit snapshots more often while visible without flipping loading state during background polls', async () => {
vi.useFakeTimers();
let visibilityState: DocumentVisibilityState = 'visible';
const visibilityState: DocumentVisibilityState = 'visible';
Object.defineProperty(document, 'visibilityState', {
configurable: true,
get: () => visibilityState,

View file

@ -1,7 +1,10 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const { realpathMock } = vi.hoisted(() => ({
const { realpathMock, resolveInteractiveShellEnvBestEffortMock } = vi.hoisted(() => ({
realpathMock: vi.fn(async (value: string) => value),
resolveInteractiveShellEnvBestEffortMock: vi.fn(
async (options?: { fallbackEnv?: NodeJS.ProcessEnv }) => options?.fallbackEnv ?? process.env
),
}));
// Mock dependencies before importing service
@ -83,6 +86,14 @@ vi.mock('@main/utils/cliAuthDiagLog', () => ({
appendCliAuthDiag: vi.fn(() => Promise.resolve(null)),
}));
vi.mock('@main/utils/shellEnv', async (importOriginal) => {
const actual = await importOriginal<typeof import('@main/utils/shellEnv')>();
return {
...actual,
resolveInteractiveShellEnvBestEffort: resolveInteractiveShellEnvBestEffortMock,
};
});
import {
CliInstallerService,
isVersionOlder,
@ -148,6 +159,9 @@ describe('CliInstallerService', () => {
vi.clearAllMocks();
realpathMock.mockReset();
realpathMock.mockImplementation(async (value: string) => value);
resolveInteractiveShellEnvBestEffortMock.mockImplementation(
async (options?: { fallbackEnv?: NodeJS.ProcessEnv }) => options?.fallbackEnv ?? process.env
);
vi.mocked(getConfiguredCliFlavor).mockReturnValue('claude');
vi.mocked(getCliFlavorUiOptions).mockReturnValue({
displayName: 'Claude CLI',
@ -169,6 +183,13 @@ describe('CliInstallerService', () => {
expect(status.installedVersion).toBeNull();
expect(status.binaryPath).toBeNull();
expect(status.updateAvailable).toBe(false);
expect(resolveInteractiveShellEnvBestEffortMock).toHaveBeenCalledWith(
expect.objectContaining({
timeoutMs: 1_500,
fallbackEnv: process.env,
background: false,
})
);
});
it('does not block getStatus on diagnostic file writes', async () => {
@ -356,6 +377,7 @@ describe('CliInstallerService', () => {
.filter((event) => event.type === 'status');
expect(status.installed).toBe(true);
expect(resolveInteractiveShellEnvBestEffortMock).not.toHaveBeenCalled();
expect(status.authStatusChecking).toBe(false);
expect(status.authLoggedIn).toBe(false);
expect(status.providers).toHaveLength(3);

View file

@ -243,6 +243,36 @@ describe('OpenCodeRuntimeInstallerService resolver', () => {
});
});
it('resolves from fast fallback PATH without spawning shell env when shell env is disabled', async () => {
const binaryPath = path.join(tempRoot!, 'merged-cli-path', 'bin', 'opencode');
await mkdir(path.dirname(binaryPath), { recursive: true });
await writeFile(binaryPath, 'binary', { mode: 0o755 });
buildMergedCliPathMock.mockReturnValue(path.dirname(binaryPath));
await expect(
resolveVerifiedOpenCodeRuntimeBinaryPath({ includeShellEnv: false })
).resolves.toBe(binaryPath);
expect(resolveInteractiveShellEnvBestEffortMock).not.toHaveBeenCalled();
expect(execCliMock).toHaveBeenCalledWith(binaryPath, ['--version'], {
timeout: 10_000,
windowsHide: true,
});
});
it('does not spawn shell env for shell-only PATH installs when shell env is disabled', async () => {
const binaryPath = path.join(tempRoot!, 'custom-npm-prefix', 'bin', 'opencode');
await mkdir(path.dirname(binaryPath), { recursive: true });
await writeFile(binaryPath, 'binary', { mode: 0o755 });
resolveInteractiveShellEnvBestEffortMock.mockResolvedValue({
PATH: path.dirname(binaryPath),
});
await expect(
resolveVerifiedOpenCodeRuntimeBinaryPath({ includeShellEnv: false })
).resolves.toBeNull();
expect(resolveInteractiveShellEnvBestEffortMock).not.toHaveBeenCalled();
});
it('returns a verified OpenCode binary from nvm when desktop PATH misses npm globals', async () => {
const olderBinaryPath = path.join(
tempRoot!,

View file

@ -464,6 +464,28 @@ describe('TeamMcpConfigBuilder', () => {
}
});
it('skips strict shell env lookup when fast Node lookup succeeds from a minimal GUI PATH', async () => {
mockBuiltWorkspaceEntryAvailable();
const previousPath = process.env.PATH;
process.env.PATH = ['/usr/bin', '/bin', '/usr/sbin', '/sbin'].join(path.delimiter);
hoisted.execCliMock.mockResolvedValue({ stdout: '/fast/node', stderr: '' });
try {
const builder = new TeamMcpConfigBuilder();
const configPath = await builder.writeConfigFile();
createdPaths.push(configPath);
expect(readGeneratedServer(configPath)?.command).toBe('/fast/node');
expect(hoisted.resolveInteractiveShellEnvMock).not.toHaveBeenCalled();
} finally {
if (previousPath === undefined) {
delete process.env.PATH;
} else {
process.env.PATH = previousPath;
}
}
});
it('falls back to strict shell env lookup when fast Node lookup reports an empty path', async () => {
mockBuiltWorkspaceEntryAvailable();
hoisted.resolveInteractiveShellEnvMock.mockResolvedValue({