From 18cf2e65e8c4b173b1d614f52153e905b8a87ff4 Mon Sep 17 00:00:00 2001 From: Anso Date: Tue, 28 Apr 2026 00:33:17 -0400 Subject: [PATCH] perf(backend): parallelize independent startup initializers (#816) The boot path awaited SelfUpdateService.initialize, DockerEventManager.start, and TrivyService.initialize one at a time even though none of them depend on each other. Group them into a single Promise.all so total cold-start time is the slowest one rather than the sum. Also convert the inner `docker compose version` probe in SelfUpdateService.initialize from execFileSync to execFileAsync. Without that, the synchronous spawn would block the event loop for up to 5 seconds and silently serialize the other two members of the parallel block, defeating the parallelization win for in-container deployments. The synchronous service starts (Monitor, AutoHeal, ImageUpdate, Scheduler, Mfa) are grouped together up front. They schedule timers whose first ticks fire 5+ seconds out, so they safely run alongside the awaited block. --- backend/src/bootstrap/startup.ts | 24 ++++++++++++----------- backend/src/services/SelfUpdateService.ts | 9 ++++++--- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/backend/src/bootstrap/startup.ts b/backend/src/bootstrap/startup.ts index a55c7eeb..19812cda 100644 --- a/backend/src/bootstrap/startup.ts +++ b/backend/src/bootstrap/startup.ts @@ -29,27 +29,29 @@ export async function startServer(server: Server): Promise { console.error('Migration failed:', error); } + // Synchronous starts: schedule background timers and continue. None of + // these fire their first tick for at least a few seconds, so they + // safely run alongside the async initializers below. LicenseService.getInstance().initialize(); - - await SelfUpdateService.getInstance().initialize(); - MonitorService.getInstance().start(); AutoHealService.getInstance().start(); - - await DockerEventManager.getInstance().start(); - - await TrivyService.getInstance().initialize(); - ImageUpdateService.getInstance().start(); - SchedulerService.getInstance().start(); + MfaService.getInstance().start(); + // Async initializers are independent of each other; run in parallel + // so total boot time is the slowest one rather than the sum. + await Promise.all([ + SelfUpdateService.getInstance().initialize(), + DockerEventManager.getInstance().start(), + TrivyService.getInstance().initialize(), + ]); + + // Fire-and-forget housekeeping; logged but never awaited. sweepStaleGitTempDirs().catch((err) => { console.warn('[GitSource] Temp dir sweep failed:', (err as Error).message); }); - MfaService.getInstance().start(); - const isPilotAgent = process.env.SENCHO_MODE === 'pilot'; const listenHost = isPilotAgent ? '127.0.0.1' : undefined; diff --git a/backend/src/services/SelfUpdateService.ts b/backend/src/services/SelfUpdateService.ts index d2a19c9a..fd34cd89 100644 --- a/backend/src/services/SelfUpdateService.ts +++ b/backend/src/services/SelfUpdateService.ts @@ -1,4 +1,4 @@ -import { execFileSync, execFile } from 'child_process'; +import { execFile } from 'child_process'; import { promisify } from 'util'; import * as fs from 'fs'; import DockerController from './DockerController'; @@ -63,9 +63,12 @@ class SelfUpdateService { return; } - // Verify docker compose CLI is available inside the container + // Verify docker compose CLI is available inside the container. + // execFileAsync (not Sync) so the parallel boot-task block in + // bootstrap/startup.ts can actually run TrivyService and DockerEventManager + // concurrently instead of waiting on a 5s blocking spawn here. try { - execFileSync('docker', ['compose', 'version'], { stdio: 'pipe', timeout: 5000 }); + await execFileAsync('docker', ['compose', 'version'], { timeout: 5000 }); } catch { console.log('[SelfUpdate] docker compose CLI not available in container'); disableCapability('self-update');