From 8152bd7808143d9102bb790f8f84750d1b834683 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Thu, 30 Apr 2026 13:54:58 +0300 Subject: [PATCH] perf: reduce desktop quit risk polling Refresh quit risk only when quitting Use in-process status for Electron local server Avoid repeated scheduled task status scans --- packages/electron/main.mjs | 41 +++++++++---------- packages/web/server/index.js | 6 +++ .../web/server/lib/scheduled-tasks/routes.js | 4 ++ .../web/server/lib/scheduled-tasks/runtime.js | 20 +++++++++ 4 files changed, 50 insertions(+), 21 deletions(-) diff --git a/packages/electron/main.mjs b/packages/electron/main.mjs index 03d5c950..ec7cb608 100644 --- a/packages/electron/main.mjs +++ b/packages/electron/main.mjs @@ -128,7 +128,6 @@ const state = { quitConfirmed: false, quitConfirmationPending: false, installingUpdate: false, - quitRiskPollerStarted: false, pendingUpdate: null, unreachableHosts: new Set(), windowCounter: 1, @@ -138,7 +137,6 @@ const state = { sshLogs: new Map(), }; -const QUIT_RISK_POLL_INTERVAL_MS = 5_000; const quitRisk = { hasActiveTunnel: false, hasRunningScheduledTasks: false, @@ -206,6 +204,8 @@ const performConfirmedQuit = () => { }; const requestQuitWithConfirmation = async () => { + await refreshQuitRiskFlags(); + if (!shouldRequireQuitConfirmation()) { performConfirmedQuit(); return; @@ -247,6 +247,24 @@ const requestQuitWithConfirmation = async () => { }; const refreshQuitRiskFlags = async () => { + if (state.serverHandle && typeof state.serverHandle.getQuitRiskStatus === 'function') { + try { + const status = await state.serverHandle.getQuitRiskStatus(); + const scheduled = status?.scheduledTasks; + if (scheduled && typeof scheduled === 'object') { + const enabledCount = Number(scheduled.enabledScheduledTasksCount ?? 0); + const runningCount = Number(scheduled.runningScheduledTasksCount ?? 0); + quitRisk.enabledScheduledTasksCount = Number.isFinite(enabledCount) ? enabledCount : 0; + quitRisk.runningScheduledTasksCount = Number.isFinite(runningCount) ? runningCount : 0; + quitRisk.hasEnabledScheduledTasks = Boolean(scheduled.hasEnabledScheduledTasks) || quitRisk.enabledScheduledTasksCount > 0; + quitRisk.hasRunningScheduledTasks = Boolean(scheduled.hasRunningScheduledTasks) || quitRisk.runningScheduledTasksCount > 0; + } + quitRisk.hasActiveTunnel = Boolean(status?.tunnel?.active); + return; + } catch { + } + } + const base = typeof state.sidecarUrl === 'string' ? state.sidecarUrl.trim().replace(/\/$/, '') : ''; if (!base) return; @@ -279,24 +297,6 @@ const refreshQuitRiskFlags = async () => { } }; -const startQuitRiskPoller = () => { - if (process.platform !== 'darwin') return; - if (state.quitRiskPollerStarted) return; - state.quitRiskPollerStarted = true; - - const loop = async () => { - while (!state.quitConfirmed && !state.quitRequested) { - await refreshQuitRiskFlags(); - if (state.quitConfirmed || state.quitRequested) break; - await new Promise((resolve) => { - const timer = setTimeout(resolve, QUIT_RISK_POLL_INTERVAL_MS); - if (typeof timer?.unref === 'function') timer.unref(); - }); - } - }; - void loop(); -}; - const settingsFilePath = () => { if (typeof process.env.OPENCHAMBER_DATA_DIR === 'string' && process.env.OPENCHAMBER_DATA_DIR.trim()) { return path.join(process.env.OPENCHAMBER_DATA_DIR.trim(), 'settings.json'); @@ -2399,7 +2399,6 @@ app.whenReady().then(async () => { const { initialUrl, localOrigin, bootOutcome } = await resolveInitialUrl(); await activateMainWindow(initialUrl, localOrigin, bootOutcome); - startQuitRiskPoller(); // Notify renderer on OS wake-from-sleep so the SSE event pipeline can // reconnect immediately instead of waiting for the heartbeat watchdog. diff --git a/packages/web/server/index.js b/packages/web/server/index.js index 1c8e04ef..5612e7f5 100644 --- a/packages/web/server/index.js +++ b/packages/web/server/index.js @@ -1233,6 +1233,12 @@ async function main(options = {}) { getPort: () => tunnelRuntimeContext.getActivePort(), getOpenCodePort: () => openCodePort, getTunnelUrl: () => tunnelService.getPublicUrl(), + getQuitRiskStatus: () => ({ + tunnel: { + active: Boolean(tunnelService.getPublicUrl()), + }, + scheduledTasks: scheduledTasksRuntime.getStatus(), + }), isReady: () => isOpenCodeReady, restartOpenCode: () => restartOpenCode(), stop: (shutdownOptions = {}) => diff --git a/packages/web/server/lib/scheduled-tasks/routes.js b/packages/web/server/lib/scheduled-tasks/routes.js index 5676f0ea..18c64c5a 100644 --- a/packages/web/server/lib/scheduled-tasks/routes.js +++ b/packages/web/server/lib/scheduled-tasks/routes.js @@ -156,6 +156,10 @@ export const registerScheduledTaskRoutes = (app, dependencies) => { app.get('/api/openchamber/scheduled-tasks/status', async (_req, res) => { try { + if (typeof scheduledTasksRuntime.getStatus === 'function') { + return res.json(scheduledTasksRuntime.getStatus()); + } + const settings = await readSettingsFromDiskMigrated(); const projects = sanitizeProjects(settings?.projects || []); diff --git a/packages/web/server/lib/scheduled-tasks/runtime.js b/packages/web/server/lib/scheduled-tasks/runtime.js index e655816c..d94b327f 100644 --- a/packages/web/server/lib/scheduled-tasks/runtime.js +++ b/packages/web/server/lib/scheduled-tasks/runtime.js @@ -739,11 +739,31 @@ export const createScheduledTasksRuntime = (deps) => { queue.length = 0; }; + const getStatus = () => { + let enabledCount = 0; + for (const taskMap of tasksByProject.values()) { + for (const task of taskMap.values()) { + if (task?.enabled) { + enabledCount += 1; + } + } + } + + const runningCount = runningTaskKeys.size; + return { + hasEnabledScheduledTasks: enabledCount > 0, + hasRunningScheduledTasks: runningCount > 0, + enabledScheduledTasksCount: enabledCount, + runningScheduledTasksCount: runningCount, + }; + }; + return { start, stop, syncAllProjects, syncProject, runNow, + getStatus, }; };