From 195be426f1517890de7932858b185b0db05626fe Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Mon, 16 Mar 2026 16:32:17 -0700 Subject: [PATCH] Convert getAppMetrics to async and replace spawnSync with async Bun.spawn spawnSync blocks the event loop while waiting for ps and du, which stalls the SSE metrics stream and other requests. Running these concurrently with async spawn (and Promise.all for du) keeps the server responsive under load. --- src/server/api/system.ts | 51 ++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/src/server/api/system.ts b/src/server/api/system.ts index 5a70175..c1db948 100644 --- a/src/server/api/system.ts +++ b/src/server/api/system.ts @@ -103,7 +103,7 @@ let _appDiskCache: Record = {} let _appDiskLastUpdate = 0 const DISK_CACHE_TTL = 30000 -function getAppMetrics(): Record { +async function getAppMetrics(): Promise> { const apps = allApps() const running = apps.filter(a => a.proc?.pid) const result: Record = {} @@ -117,8 +117,10 @@ function getAppMetrics(): Record { if (pidToName.size > 0) { try { const pids = [...pidToName.keys()].join(',') - const ps = Bun.spawnSync(['ps', '-o', 'pid=,%cpu=,rss=', '-p', pids]) - for (const line of ps.stdout.toString().split('\n')) { + const proc = Bun.spawn(['ps', '-o', 'pid=,%cpu=,rss=', '-p', pids], { stdout: 'pipe', stderr: 'ignore' }) + const output = await new Response(proc.stdout).text() + await proc.exited + for (const line of output.split('\n')) { const parts = line.trim().split(/\s+/) if (parts.length < 3) continue const pid = parseInt(parts[0]!, 10) @@ -135,12 +137,21 @@ function getAppMetrics(): Record { if (now - _appDiskLastUpdate > DISK_CACHE_TTL) { _appDiskLastUpdate = now _appDiskCache = {} - for (const app of apps) { - try { - const du = Bun.spawnSync(['du', '-sk', join(APPS_DIR, app.name)]) - const kb = parseInt(du.stdout.toString().trim().split('\t')[0]!, 10) - if (kb) _appDiskCache[app.name] = kb * 1024 - } catch {} + const duResults = await Promise.all( + apps.map(async app => { + try { + const proc = Bun.spawn(['du', '-sk', join(APPS_DIR, app.name)], { stdout: 'pipe', stderr: 'ignore' }) + const output = await new Response(proc.stdout).text() + await proc.exited + const kb = parseInt(output.trim().split('\t')[0]!, 10) + return { name: app.name, bytes: kb ? kb * 1024 : 0 } + } catch { + return { name: app.name, bytes: 0 } + } + }) + ) + for (const { name, bytes } of duResults) { + if (bytes) _appDiskCache[name] = bytes } } @@ -154,26 +165,30 @@ function getAppMetrics(): Record { } // Get current system metrics -router.get('/metrics', c => { +router.get('/metrics', async c => { const metrics: SystemMetrics = { cpu: getCpuUsage(), ram: getMemoryUsage(), disk: getDiskUsage(), - apps: getAppMetrics(), + apps: await getAppMetrics(), } return c.json(metrics) }) // SSE stream for real-time metrics (updates every 2s) router.sse('/metrics/stream', (send) => { + let queue = Promise.resolve() + const sendMetrics = () => { - const metrics: SystemMetrics = { - cpu: getCpuUsage(), - ram: getMemoryUsage(), - disk: getDiskUsage(), - apps: getAppMetrics(), - } - send(metrics) + queue = queue.then(async () => { + const metrics: SystemMetrics = { + cpu: getCpuUsage(), + ram: getMemoryUsage(), + disk: getDiskUsage(), + apps: await getAppMetrics(), + } + await send(metrics) + }) } // Initial send