From ac6ae7c7cecada84ea314a1b0d0c601af87bdd90 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2026 10:19:53 -0700 Subject: [PATCH] [eric] diagnostics: time the boot + stamp the shipped commit into the log --- electron/main.js | 87 +++++++++++++++++++++++++++++++++++++++++++-- electron/preload.js | 8 +++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/electron/main.js b/electron/main.js index 1fb765ef..54d36681 100644 --- a/electron/main.js +++ b/electron/main.js @@ -35,6 +35,21 @@ const getPort = require('get-port'); const http = require('http'); const affiliateTracking = require('./affiliateTracking'); +// Phase 0 boot instrumentation. Records four ordered milestones as parseable +// lines so the packaged-build timing test (and any future perf-regression +// gate) can read them straight out of backend.log without a separate file. +// Format is load-bearing: `[perf] t=` one per line. +// APP_LAUNCH_T is captured at module load so t=0 is genuinely process start. +const APP_LAUNCH_T = Date.now(); +const _perfSeen = new Set(); +function perfMark(name) { + // One-shot per milestone: first-paint etc. can re-fire on crash-recovery + // window recreation, but the baseline we care about is the cold boot. + if (_perfSeen.has(name)) return; + _perfSeen.add(name); + try { console.log(`[perf] ${name} t=${Date.now() - APP_LAUNCH_T}`); } catch (_) {} +} + // Defender warmup: NSIS runs us with --prewarm right after install so Windows scans the bundled binaries while the user is already watching the installer instead of staring at a slow first launch. if (process.argv.includes('--prewarm') && process.platform === 'win32') { const touchExe = (rel) => { @@ -639,6 +654,16 @@ async function startBackend() { } openBackendLog(); + // app-launch is the first milestone that can reach backend.log, since the + // console tee is installed by openBackendLog() just above. APP_LAUNCH_T + // (module load) remains the t=0 reference, so this t is real elapsed. + perfMark('app-launch'); + // Provenance: name the exact commit + version at the top of every boot trace, + // so a user-submitted backend.log instantly says what shipped. Emitted here + // (not in whenReady) because openBackendLog() above just installed the console + // tee; logging earlier would miss the persistent file. + const _bi = getBuildInfo(); + console.log(`[provenance] OpenSwarm ${app.getVersion()} sha=${_bi.shortSha} channel=${_bi.channel} builtAt=${_bi.builtAt || 'n/a'}`); // Record what we're about to launch and whether the interpreter is even // present. If AV quarantined python.exe or the wrong-arch bundle shipped, // exists=false (or spawn 'error' below) names the cause that otherwise @@ -704,6 +729,7 @@ async function startBackend() { emitSplashStatus('Starting backend…'); await waitForBackend(backendPort, { process: backendProcess }); + perfMark('backend-http-ready'); console.log(`Backend ready on port ${backendPort}`); // Backend writes a per-install auth token file at startup. Read it @@ -881,6 +907,7 @@ function createWindow() { // the window existed (cold-launch via openswarm://). pendingDeepLink may // be a string (legacy) OR a {channel, url} object (v1.0.26+ OAuth claims). mainWindow.webContents.once('did-finish-load', () => { + perfMark('first-paint'); if (pendingDeepLink) { if (typeof pendingDeepLink === 'string') { mainWindow.webContents.send('openswarm:auth-url', pendingDeepLink); @@ -1032,6 +1059,47 @@ function sendToRenderer(channel, ...args) { } } +// Maps a raw electron-updater error to a short, human message. The raw error +// is always logged separately for debugging; users only ever see this. No +// em/en dashes per repo style. +function friendlyUpdateError(err) { + const raw = ((err && err.message) || String(err) || '').toLowerCase(); + // Experimental on, but there is no pre-release to fetch: the provider 404s + // looking for the pre-release channel feed. This is the screenshot case. + if (autoUpdater && autoUpdater.allowPrerelease && + /404|not found|cannot find|no published|latest.*\.yml/.test(raw)) { + return 'No experimental builds available right now. You are on the latest version.'; + } + if (/net::|enotfound|etimedout|econnrefused|getaddrinfo|network/.test(raw)) { + return 'Could not reach the update server. Check your connection and try again.'; + } + return 'Update check failed. Please try again later.'; +} + +// Phase 2 provenance: which exact commit produced this build. The build +// scripts write electron/build-info.json (gitignored, regenerated each build) +// next to main.js, so it ships inside the asar. In dev there is no such file, +// so we fall back to a live `git rev-parse` and tag it dev. Cached after first +// read; never throws (a missing/garbled file just yields 'unknown'). +let _buildInfoCache = null; +function getBuildInfo() { + if (_buildInfoCache) return _buildInfoCache; + let info = { sha: 'unknown', shortSha: 'unknown', builtAt: null, channel: 'unknown' }; + try { + const raw = fs.readFileSync(path.join(__dirname, 'build-info.json'), 'utf8'); + const parsed = JSON.parse(raw); + if (parsed && parsed.sha) info = parsed; + } catch (_) { + // Dev fallback: resolve the working-tree HEAD so `npm start` still reports something useful. + try { + const sha = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: __dirname, timeout: 2000 }).toString().trim(); + if (sha) info = { sha, shortSha: sha.slice(0, 12), builtAt: null, channel: 'dev' }; + } catch (_) { /* not a git checkout either; keep 'unknown' */ } + } + _buildInfoCache = info; + return info; +} + function setupAutoUpdater() { if (!autoUpdater) return; // Silent background updates: download on detect, install on next quit. @@ -1068,9 +1136,15 @@ function setupAutoUpdater() { }); autoUpdater.on('error', (err) => { + // Raw electron-updater errors are verbose (full URL, HTTP status, stack, + // sometimes an HTML body). Keep the raw text in the log for debugging, but + // never show it to the user. The common case is "Experimental updates is on + // but no pre-release exists": the GitHub provider 404s hunting a pre-release + // feed, which is not a real failure, just "nothing newer to install". console.error('Auto-update error:', err); - cachedUpdateStatus = { status: 'error', info: null, error: err?.message || String(err) }; - sendToRenderer('update-error', err?.message || String(err)); + const friendly = friendlyUpdateError(err); + cachedUpdateStatus = { status: 'error', info: null, error: friendly }; + sendToRenderer('update-error', friendly); }); autoUpdater.checkForUpdates().catch((err) => { @@ -1702,7 +1776,16 @@ ipcMain.handle('get-auth-token', async () => { } catch (_) {} return authToken; }); +// Phase 0: renderer fires this once, when it renders the first streamed token +// of the first agent response. Main owns the timing log (backend.log), so the +// renderer reports the event and we stamp it against the same APP_LAUNCH_T as +// the other milestones. Idempotent via perfMark's one-shot guard. +ipcMain.on('perf:first-agent-response', () => perfMark('first-agent-response')); + ipcMain.handle('get-app-version', () => app.getVersion()); +// Phase 2 provenance: the renderer's About panel shows the commit this build +// was cut from, so a screenshot is enough to identify the exact code shipped. +ipcMain.handle('get-build-info', () => getBuildInfo()); ipcMain.handle('get-webview-preload-path', () => { return `file://${path.join(__dirname, 'webview-preload.js')}`; }); diff --git a/electron/preload.js b/electron/preload.js index 95f4d613..eb631e06 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -24,6 +24,14 @@ contextBridge.exposeInMainWorld('openswarm', { getAuthToken: () => ipcRenderer.invoke('get-auth-token'), getAppVersion: () => ipcRenderer.invoke('get-app-version'), + + // Phase 2 provenance: { sha, shortSha, builtAt, channel } for the About panel. + getBuildInfo: () => ipcRenderer.invoke('get-build-info'), + + // Phase 0 boot instrumentation: renderer calls this exactly once, when the + // first streamed token of the first agent response paints. Fire-and-forget + // (send, not invoke) so it never blocks the render path. Main dedupes. + markFirstAgentResponse: () => ipcRenderer.send('perf:first-agent-response'), openExternal: (url) => ipcRenderer.invoke('open-external', url), // Returns the persisted install state (app_install_id, ref, ...).