diff --git a/packages/app/src/app/index.tsx b/packages/app/src/app/index.tsx index 3502d86bf..9333559e4 100644 --- a/packages/app/src/app/index.tsx +++ b/packages/app/src/app/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useSyncExternalStore, useState } from 'react' +import { useEffect, useRef, useSyncExternalStore, useState } from 'react' import { usePathname, useRouter } from 'expo-router' import { useDaemonRegistry } from '@/contexts/daemon-registry-context' import { shouldUseManagedDesktopDaemon } from '@/desktop/managed-runtime/managed-runtime' @@ -13,6 +13,7 @@ import { } from './index-startup' const STARTUP_TIMEOUT_MS = 30_000 +const APP_START_TIME = performance.now() function useAnyHostOnline(serverIds: string[]): string | null { const runtime = getHostRuntimeStore() @@ -60,6 +61,17 @@ export default function Index() { const [hasTimedOut, setHasTimedOut] = useState(false) const isDesktopStartupRace = shouldUseManagedDesktopDaemon() const onlineServerId = useAnyHostOnline(daemons.map((daemon) => daemon.serverId)) + const startupLogged = useRef(false) + + useEffect(() => { + if (onlineServerId && !startupLogged.current) { + startupLogged.current = true + console.info('[Startup] host online', { + serverId: onlineServerId, + elapsedMs: Math.round(performance.now() - APP_START_TIME), + }) + } + }, [onlineServerId]) useEffect(() => { const timer = setTimeout(() => { @@ -80,6 +92,10 @@ export default function Index() { if (pathname !== '/' && pathname !== '') { return } + console.info('[Startup] navigating to host route', { + serverId: onlineServerId, + elapsedMs: Math.round(performance.now() - APP_START_TIME), + }) router.replace(buildHostRootRoute(onlineServerId) as any) }, [onlineServerId, pathname, registryLoading, router]) diff --git a/packages/app/src/contexts/daemon-registry-context.tsx b/packages/app/src/contexts/daemon-registry-context.tsx index 36d04e22a..4d64c6bbf 100644 --- a/packages/app/src/contexts/daemon-registry-context.tsx +++ b/packages/app/src/contexts/daemon-registry-context.tsx @@ -483,15 +483,20 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) { let cancelled = false const bootstrapDesktopStartup = async () => { + const t0 = performance.now() + const elapsed = () => Math.round(performance.now() - t0) try { const isE2E = await AsyncStorage.getItem(E2E_STORAGE_KEY) if (cancelled || isE2E) { return } + console.info('[DaemonRegistry] desktop bootstrap: starting', { elapsedMs: elapsed() }) await Promise.allSettled([ (async () => { + console.info('[DaemonRegistry] desktop bootstrap: startManagedDaemon', { elapsedMs: elapsed() }) const daemon = await startManagedDaemon() + console.info('[DaemonRegistry] desktop bootstrap: managed daemon ready', { elapsedMs: elapsed(), serverId: daemon.serverId }) if (cancelled || !daemon.serverId) { return } @@ -506,15 +511,18 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) { label: daemon.hostname ?? undefined, connection, }) + console.info('[DaemonRegistry] desktop bootstrap: host connection upserted', { elapsedMs: elapsed() }) })().catch((managedBootstrapError) => { if (!cancelled) { console.warn( '[DaemonRegistry] Failed to bootstrap desktop daemon connection', + { elapsedMs: elapsed() }, managedBootstrapError ) } }), (async () => { + console.info('[DaemonRegistry] desktop bootstrap: probing localhost', { elapsedMs: elapsed() }) const { serverId, hostname } = await probeConnection( { id: `bootstrap:${DEFAULT_LOCALHOST_ENDPOINT}`, @@ -523,6 +531,7 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) { }, { timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS } ) + console.info('[DaemonRegistry] desktop bootstrap: localhost probe succeeded', { elapsedMs: elapsed(), serverId }) if (cancelled) { return } @@ -532,8 +541,9 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) { endpoint: DEFAULT_LOCALHOST_ENDPOINT, label: hostname ?? undefined, }) + console.info('[DaemonRegistry] desktop bootstrap: direct connection upserted', { elapsedMs: elapsed() }) })().catch(() => { - // Best-effort startup race only; ignore if localhost isn't reachable. + console.info('[DaemonRegistry] desktop bootstrap: localhost probe failed', { elapsedMs: elapsed() }) }), ]) } catch (bootstrapError) { diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts index 84c4ddb8d..4c3e66cf7 100644 --- a/packages/app/src/runtime/host-runtime.ts +++ b/packages/app/src/runtime/host-runtime.ts @@ -143,7 +143,9 @@ export type HostRuntimeStartOptions = { autoProbe?: boolean; }; -const PROBE_INTERVAL_MS = 10_000; +const PROBE_TICK_MS = 2_000; +const PROBE_STEADY_MS = 10_000; +const PROBE_MAX_BACKOFF_MS = 30_000; const ADAPTIVE_SWITCH_THRESHOLD_MS = 40; const ADAPTIVE_SWITCH_CONSECUTIVE_PROBES = 3; const DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT = 200; @@ -419,19 +421,19 @@ function findConnectionById( return host.connections.find((connection) => connection.id === connectionId) ?? null; } -function selectInitialConnectionId(host: HostProfile): string | null { - if (host.connections.length === 0) { - return null; +function probeIntervalForConnection( + firstSeenAt: number, + isActiveOnline: boolean, + now: number +): number { + if (isActiveOnline) { + return PROBE_STEADY_MS; } - if (host.preferredConnectionId) { - const preferred = host.connections.find( - (connection) => connection.id === host.preferredConnectionId - ); - if (preferred) { - return preferred.id; - } - } - return host.connections[0]?.id ?? null; + const age = now - firstSeenAt; + if (age < 10_000) return 2_000; + if (age < 30_000) return 5_000; + if (age < 60_000) return PROBE_STEADY_MS; + return PROBE_MAX_BACKOFF_MS; } function createDefaultDeps(): HostRuntimeControllerDeps { @@ -507,6 +509,8 @@ export class HostRuntimeController { private unsubscribeClientStatus: (() => void) | null = null; private probeIntervalHandle: ReturnType | null = null; private started = false; + private connectionFirstSeenAt = new Map(); + private connectionLastProbedAt = new Map(); private switchCandidateConnectionId: string | null = null; private switchCandidateHitCount = 0; private clientIdPromise: Promise | null = null; @@ -555,19 +559,12 @@ export class HostRuntimeController { return; } this.started = true; + this.trackConnectionFirstSeen(); await this.runProbeCycleNow(); - - if (!this.snapshot.activeConnectionId) { - const fallbackConnectionId = selectInitialConnectionId(this.host); - if (fallbackConnectionId) { - await this.switchToConnection({ connectionId: fallbackConnectionId }); - } - } - - if (options?.autoProbe !== false && PROBE_INTERVAL_MS > 0) { + if (options?.autoProbe !== false) { this.probeIntervalHandle = setInterval(() => { void this.runProbeCycleNow(); - }, PROBE_INTERVAL_MS); + }, PROBE_TICK_MS); } } @@ -597,20 +594,8 @@ export class HostRuntimeController { async updateHost(host: HostProfile): Promise { this.host = host; - const activeConnectionId = this.snapshot.activeConnectionId; - const activeConnection = findConnectionById(host, activeConnectionId); - if (!activeConnection) { - await this.runProbeCycleNow(); - if (this.snapshot.activeConnectionId) { - return; - } - const fallbackConnectionId = selectInitialConnectionId(host); - if (!fallbackConnectionId) { - await this.stop(); - return; - } - await this.switchToConnection({ connectionId: fallbackConnectionId }); - } + this.trackConnectionFirstSeen(); + await this.runProbeCycleNow(); } ensureConnected(): void { @@ -674,9 +659,29 @@ export class HostRuntimeController { return; } - const probeByConnectionId = new Map(); + const now = performance.now(); + const isOnline = this.snapshot.connectionStatus === "online"; + const activeConnectionId = this.snapshot.activeConnectionId; + + const connectionsToProbe = this.host.connections.filter((connection) => { + const lastProbed = this.connectionLastProbedAt.get(connection.id); + if (lastProbed == null) { + return true; + } + const firstSeen = this.connectionFirstSeenAt.get(connection.id) ?? now; + const isActiveOnline = isOnline && connection.id === activeConnectionId; + const interval = probeIntervalForConnection(firstSeen, isActiveOnline, now); + return now - lastProbed >= interval; + }); + + if (connectionsToProbe.length === 0) { + return; + } + + const probeByConnectionId = new Map(this.snapshot.probeByConnectionId); await Promise.all( - this.host.connections.map(async (connection) => { + connectionsToProbe.map(async (connection) => { + this.connectionLastProbedAt.set(connection.id, performance.now()); try { const latencyMs = await this.deps.measureLatency({ host: this.host, @@ -700,12 +705,12 @@ export class HostRuntimeController { } this.updateSnapshot({ probeByConnectionId }); - const activeConnectionId = this.snapshot.activeConnectionId; - const activeProbe = activeConnectionId - ? probeByConnectionId.get(activeConnectionId) + const currentActiveConnectionId = this.snapshot.activeConnectionId; + const activeProbe = currentActiveConnectionId + ? probeByConnectionId.get(currentActiveConnectionId) : null; - if (!activeConnectionId || !findConnectionById(this.host, activeConnectionId)) { + if (!currentActiveConnectionId || !findConnectionById(this.host, currentActiveConnectionId)) { const nextConnectionId = selectBestConnection({ candidates: buildConnectionCandidates(this.host), probeByConnectionId, @@ -724,7 +729,7 @@ export class HostRuntimeController { candidates: buildConnectionCandidates(this.host), probeByConnectionId, }); - if (nextConnectionId && nextConnectionId !== activeConnectionId) { + if (nextConnectionId && nextConnectionId !== currentActiveConnectionId) { await this.switchToConnection({ connectionId: nextConnectionId, expectedProbeVersion: requestVersion, @@ -745,7 +750,7 @@ export class HostRuntimeController { .sort((left, right) => left.latencyMs - right.latencyMs); const fastest = available[0] ?? null; - if (!fastest || fastest.connectionId === activeConnectionId) { + if (!fastest || fastest.connectionId === currentActiveConnectionId) { this.switchCandidateConnectionId = null; this.switchCandidateHitCount = 0; return; @@ -839,6 +844,22 @@ export class HostRuntimeController { }); } + private trackConnectionFirstSeen(): void { + const now = performance.now(); + const currentIds = new Set(this.host.connections.map((c) => c.id)); + for (const id of this.connectionFirstSeenAt.keys()) { + if (!currentIds.has(id)) { + this.connectionFirstSeenAt.delete(id); + this.connectionLastProbedAt.delete(id); + } + } + for (const connection of this.host.connections) { + if (!this.connectionFirstSeenAt.has(connection.id)) { + this.connectionFirstSeenAt.set(connection.id, now); + } + } + } + private isCurrentSwitchRequest(version: number): boolean { return version === this.switchRequestVersion; } diff --git a/packages/cli/src/commands/daemon/local-daemon.ts b/packages/cli/src/commands/daemon/local-daemon.ts index e9d79bf4e..b1bb33aa8 100644 --- a/packages/cli/src/commands/daemon/local-daemon.ts +++ b/packages/cli/src/commands/daemon/local-daemon.ts @@ -20,7 +20,7 @@ export interface LocalDaemonPidInfo { startedAt?: string hostname?: string uid?: number - sockPath?: string + listen?: string } export interface LocalDaemonState { @@ -164,7 +164,7 @@ function readPidFile(pidPath: string): LocalDaemonPidInfo | null { startedAt: typeof parsed.startedAt === 'string' ? parsed.startedAt : undefined, hostname: typeof parsed.hostname === 'string' ? parsed.hostname : undefined, uid: typeof parsed.uid === 'number' ? parsed.uid : undefined, - sockPath: typeof parsed.sockPath === 'string' ? parsed.sockPath : undefined, + listen: typeof parsed.listen === 'string' ? parsed.listen : typeof parsed.sockPath === 'string' ? parsed.sockPath : undefined, } } catch { return null @@ -322,7 +322,7 @@ export function resolveLocalDaemonState(options: { home?: string } = {}): LocalD const logPath = path.join(home, DAEMON_LOG_FILENAME) const pidInfo = existsSync(pidPath) ? readPidFile(pidPath) : null const running = pidInfo ? isProcessRunning(pidInfo.pid) : false - const listen = pidInfo?.sockPath ?? config.listen + const listen = pidInfo?.listen ?? config.listen return { home, diff --git a/packages/cli/src/utils/client.ts b/packages/cli/src/utils/client.ts index 4789c486b..98a311a29 100644 --- a/packages/cli/src/utils/client.ts +++ b/packages/cli/src/utils/client.ts @@ -75,8 +75,8 @@ function readPidSocketTarget(paseoHome: string): string | null { } try { - const parsed = JSON.parse(readFileSync(pidPath, 'utf-8')) as { sockPath?: unknown } - return typeof parsed.sockPath === 'string' ? parsed.sockPath : null + const parsed = JSON.parse(readFileSync(pidPath, 'utf-8')) as { listen?: unknown; sockPath?: unknown } + return typeof parsed.listen === 'string' ? parsed.listen : typeof parsed.sockPath === 'string' ? parsed.sockPath : null } catch { return null } diff --git a/packages/cli/tests/28-client-ipc-targets.test.ts b/packages/cli/tests/28-client-ipc-targets.test.ts index 38d9589ae..b990cb392 100644 --- a/packages/cli/tests/28-client-ipc-targets.test.ts +++ b/packages/cli/tests/28-client-ipc-targets.test.ts @@ -48,7 +48,7 @@ console.log('=== CLI IPC Target Helpers ===\n') mkdirSync(paseoHome, { recursive: true }) writeFileSync( path.join(paseoHome, 'paseo.pid'), - JSON.stringify({ pid: process.pid, sockPath: '/tmp/paseo-from-pid.sock' }) + JSON.stringify({ pid: process.pid, listen: '/tmp/paseo-from-pid.sock' }) ) assert.deepStrictEqual(resolveDefaultDaemonHosts({ PASEO_HOME: paseoHome }), [ 'unix:///tmp/paseo-from-pid.sock', @@ -93,7 +93,7 @@ console.log('=== CLI IPC Target Helpers ===\n') mkdirSync(paseoHome, { recursive: true }) writeFileSync( path.join(paseoHome, 'paseo.pid'), - JSON.stringify({ pid: process.pid, sockPath: '/tmp/paseo-priority.sock' }) + JSON.stringify({ pid: process.pid, listen: '/tmp/paseo-priority.sock' }) ) assert.deepStrictEqual( resolveDefaultDaemonHosts({ diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index aa7fa03e5..fec05eaec 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -536,8 +536,9 @@ pub fn run() { garbage_collect_attachment_files ]) .setup(|app| { + let setup_start = std::time::Instant::now(); log::info!( - "[app] Paseo Desktop v{} starting", + "[app] Paseo Desktop v{} setup starting", app.package_info().version ); @@ -645,6 +646,7 @@ pub fn run() { } app.set_menu(menu)?; + log::info!("[app] setup complete ({}ms)", setup_start.elapsed().as_millis()); let window = app.get_webview_window("main").unwrap(); let window_clone = window.clone(); diff --git a/packages/desktop/src-tauri/src/runtime_manager.rs b/packages/desktop/src-tauri/src/runtime_manager.rs index 7b239e7bb..0bfdab16b 100644 --- a/packages/desktop/src-tauri/src/runtime_manager.rs +++ b/packages/desktop/src-tauri/src/runtime_manager.rs @@ -574,8 +574,10 @@ fn managed_daemon_status_internal(app: &AppHandle) -> Result Result { + let t0 = std::time::Instant::now(); log::info!("[daemon] starting managed daemon"); let status = ensure_runtime_ready_internal(app)?; + log::info!("[daemon] runtime ready ({}ms)", t0.elapsed().as_millis()); let runtime_root = PathBuf::from(&status.runtime_root); let manifest = load_runtime_manifest(&runtime_root)?; let output = cli_command(&runtime_root, &manifest, &["start"])? @@ -599,20 +601,21 @@ fn start_managed_daemon_internal(app: &AppHandle) -> Result { const logger = rootLogger.child({ module: "bootstrap" }); + const bootstrapStart = performance.now(); + const elapsed = () => `${(performance.now() - bootstrapStart).toFixed(0)}ms`; const daemonVersion = resolveDaemonVersion(import.meta.url); const pidLockMode = config.pidLock?.mode ?? "self"; const pidLockOwnerPid = config.pidLock?.ownerPid; @@ -177,6 +179,7 @@ export async function createPaseoDaemon( await acquirePidLock(config.paseoHome, config.listen, { ownerPid: pidLockOwnerPid, }); + logger.info({ elapsed: elapsed() }, "PID lock acquired"); } try { @@ -328,6 +331,7 @@ export async function createPaseoDaemon( agentStorage ); await agentStorage.initialize(); + logger.info({ elapsed: elapsed() }, "Agent storage initialized"); await bootstrapWorkspaceRegistries({ paseoHome: config.paseoHome, agentStorage, @@ -335,8 +339,10 @@ export async function createPaseoDaemon( workspaceRegistry, logger, }); + logger.info({ elapsed: elapsed() }, "Workspace registries bootstrapped"); const persistedRecords = await agentStorage.list(); logger.info( + { elapsed: elapsed() }, `Agent registry loaded (${persistedRecords.length} record${persistedRecords.length === 1 ? "" : "s"}); agents will initialize on demand` ); logger.info( @@ -518,6 +524,7 @@ export async function createPaseoDaemon( openaiConfig: config.openai, speechConfig: config.speech, }); + logger.info({ elapsed: elapsed() }, "Speech runtime initialized"); wsServer = new VoiceAssistantWebSocketServer( httpServer, @@ -567,6 +574,8 @@ export async function createPaseoDaemon( wsServer?.publishSpeechReadiness(snapshot); }); + logger.info({ elapsed: elapsed() }, "Bootstrap complete, ready to start listening"); + const start = async () => { // Start main HTTP server await new Promise((resolve, reject) => { @@ -584,11 +593,11 @@ export async function createPaseoDaemon( if (listenTarget.type === "tcp") { logger.info( - { host: listenTarget.host, port: listenTarget.port }, + { host: listenTarget.host, port: listenTarget.port, elapsed: elapsed() }, `Server listening on http://${listenTarget.host}:${listenTarget.port}` ); } else { - logger.info({ path: listenTarget.path }, `Server listening on ${listenTarget.path}`); + logger.info({ path: listenTarget.path, elapsed: elapsed() }, `Server listening on ${listenTarget.path}`); } if (relayEnabled) { diff --git a/packages/server/src/server/pid-lock.ts b/packages/server/src/server/pid-lock.ts index 4f0a6cde0..155144b6b 100644 --- a/packages/server/src/server/pid-lock.ts +++ b/packages/server/src/server/pid-lock.ts @@ -8,7 +8,7 @@ export interface PidLockInfo { startedAt: string; hostname: string; uid: number; - sockPath: string; + listen: string; } export class PidLockError extends Error { @@ -43,7 +43,7 @@ function resolveOwnerPid(ownerPid?: number): number { export async function acquirePidLock( paseoHome: string, - sockPath: string, + listen: string, options?: { ownerPid?: number } ): Promise { const pidPath = getPidFilePath(paseoHome); @@ -85,7 +85,7 @@ export async function acquirePidLock( startedAt: new Date().toISOString(), hostname: hostname(), uid: process.getuid?.() ?? 0, - sockPath, + listen, }; let fd;