From e7933c91371e98f2a199dd34c0bc335c885fe2cc Mon Sep 17 00:00:00 2001 From: samzong Date: Mon, 18 May 2026 06:45:15 +0800 Subject: [PATCH] perf(gateway): overlap startup work before ready Signed-off-by: samzong --- .../server-startup-post-attach.test.ts | 86 +++++++++++++++++++ src/gateway/server-startup-post-attach.ts | 60 +++++++++---- 2 files changed, 128 insertions(+), 18 deletions(-) diff --git a/src/gateway/server-startup-post-attach.test.ts b/src/gateway/server-startup-post-attach.test.ts index ab66de2d0391..48be4fb85bbc 100644 --- a/src/gateway/server-startup-post-attach.test.ts +++ b/src/gateway/server-startup-post-attach.test.ts @@ -383,6 +383,48 @@ describe("startGatewayPostAttachRuntime", () => { expect(events).toEqual(["sidecars", "returned", "sentinel"]); }); + it("starts sidecars while startup logging is still pending", async () => { + const events: string[] = []; + let finishStartupLog: (() => void) | undefined; + const logGatewayStartup = vi.fn( + () => + new Promise((resolve) => { + events.push("startup-log-start"); + finishStartupLog = () => { + events.push("startup-log-end"); + resolve(); + }; + }), + ); + const startGatewaySidecars = vi.fn(async () => { + events.push("sidecars"); + return { pluginServices: null, postReadySidecars: [] }; + }); + + const runtimePromise = startGatewayPostAttachRuntime( + createPostAttachParams(), + createPostAttachRuntimeDeps({ + logGatewayStartup, + refreshLatestUpdateRestartSentinel: vi.fn(async () => null), + startGatewaySidecars, + }), + ); + + await vi.waitFor(() => { + expect(logGatewayStartup).toHaveBeenCalledTimes(1); + expect(startGatewaySidecars).toHaveBeenCalledTimes(1); + }); + expect(events).toEqual(["startup-log-start", "sidecars"]); + + if (!finishStartupLog) { + throw new Error("Expected startup log release callback to be initialized"); + } + finishStartupLog(); + await runtimePromise; + + expect(events).toEqual(["startup-log-start", "sidecars", "startup-log-end"]); + }); + it("skips heavy restart sentinel refresh when no sentinel file exists", async () => { const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-no-sentinel-")); vi.stubEnv("OPENCLAW_STATE_DIR", stateDir); @@ -800,6 +842,50 @@ describe("startGatewayPostAttachRuntime", () => { ); }); + it("starts and reports plugin services before channel startup completion", async () => { + await withEnvAsync( + { OPENCLAW_SKIP_CHANNELS: undefined, OPENCLAW_SKIP_PROVIDERS: undefined }, + async () => { + let releaseChannels: (() => void) | undefined; + const pluginServices = { stop: vi.fn(async () => {}) } as never; + const onPluginServices = vi.fn(); + const onSidecarsReady = vi.fn(); + const startChannels = vi.fn( + () => + new Promise((resolve) => { + releaseChannels = resolve; + }), + ); + hoisted.startPluginServices.mockResolvedValueOnce(pluginServices); + + await startGatewayPostAttachRuntime({ + ...createPostAttachParams({ + deferSidecars: true, + onPluginServices, + onSidecarsReady, + }), + startChannels, + }); + + await vi.waitFor(() => { + expect(startChannels).toHaveBeenCalledTimes(1); + expect(hoisted.startPluginServices).toHaveBeenCalledTimes(1); + expect(onPluginServices).toHaveBeenCalledWith(pluginServices); + }); + expect(onSidecarsReady).not.toHaveBeenCalled(); + + if (!releaseChannels) { + throw new Error("Expected channel startup release callback to be initialized"); + } + releaseChannels(); + await vi.waitFor(() => { + expect(onSidecarsReady).toHaveBeenCalledTimes(1); + }); + expect(onPluginServices).toHaveBeenCalledTimes(1); + }, + ); + }); + it("emits a startup trace span when channel startup is skipped", async () => { const trace = createStartupTraceRecorder(); const logChannels = { info: vi.fn(), error: vi.fn() }; diff --git a/src/gateway/server-startup-post-attach.ts b/src/gateway/server-startup-post-attach.ts index 332041ca2994..394a102eb760 100644 --- a/src/gateway/server-startup-post-attach.ts +++ b/src/gateway/server-startup-post-attach.ts @@ -413,6 +413,7 @@ export async function startGatewaySidecars(params: { deps: CliDeps; startChannels: () => Promise; prewarmPrimaryModel?: typeof prewarmConfiguredPrimaryModel; + onPluginServices?: (pluginServices: PluginServicesHandle | null) => void; log: { warn: (msg: string) => void }; logHooks: { info: (msg: string) => void; @@ -445,6 +446,31 @@ export async function startGatewaySidecars(params: { } }); + const pluginServicesPromise = measureStartup( + params.startupTrace, + "sidecars.plugin-services", + async () => { + try { + const { startPluginServices } = await import("../plugins/services.js"); + return await startPluginServices({ + registry: params.pluginRegistry, + config: params.cfg, + workspaceDir: params.defaultWorkspaceDir, + startupTrace: params.startupTrace, + }); + } catch (err) { + params.log.warn(`plugin services failed to start: ${String(err)}`); + return null; + } + }, + ); + const pluginServicesReportPromise = params.onPluginServices + ? pluginServicesPromise.then((pluginServices) => { + params.onPluginServices?.(pluginServices); + return pluginServices; + }) + : pluginServicesPromise; + const skipChannels = isTruthyEnvValue(process.env.OPENCLAW_SKIP_CHANNELS) || isTruthyEnvValue(process.env.OPENCLAW_SKIP_PROVIDERS); @@ -492,20 +518,7 @@ export async function startGatewaySidecars(params: { }, 250); } - let pluginServices: PluginServicesHandle | null = null; - await measureStartup(params.startupTrace, "sidecars.plugin-services", async () => { - try { - const { startPluginServices } = await import("../plugins/services.js"); - pluginServices = await startPluginServices({ - registry: params.pluginRegistry, - config: params.cfg, - workspaceDir: params.defaultWorkspaceDir, - startupTrace: params.startupTrace, - }); - } catch (err) { - params.log.warn(`plugin services failed to start: ${String(err)}`); - } - }); + const pluginServices = await pluginServicesReportPromise; if (params.cfg.acp?.enabled) { void (async () => { @@ -804,7 +817,7 @@ export async function startGatewayPostAttachRuntime( await params.onStartupPluginsLoaded?.(loaded); } - await measureStartup(params.startupTrace, "post-attach.log", () => + const startupLogPromise = measureStartup(params.startupTrace, "post-attach.log", () => runtimeDeps.logGatewayStartup({ cfg: params.cfgAtStart, bindHost: params.bindHost, @@ -849,6 +862,12 @@ export async function startGatewayPostAttachRuntime( }), ); + let pluginServicesReported = false; + const reportPluginServices = (pluginServices: PluginServicesHandle | null) => { + pluginServicesReported = true; + params.onPluginServices?.(pluginServices); + }; + const sidecarsPromise = params.minimalTestGateway ? Promise.resolve({ pluginServices: null, pluginRegistry, postReadySidecars: [] }) : new Promise((resolve) => setImmediate(resolve)).then(async () => { @@ -865,6 +884,7 @@ export async function startGatewayPostAttachRuntime( logHooks: params.logHooks, logChannels: params.logChannels, startupTrace: params.startupTrace, + onPluginServices: reportPluginServices, }), ); const loaderStatsAfter = getPluginModuleLoaderStats(); @@ -884,7 +904,9 @@ export async function startGatewayPostAttachRuntime( for (const method of STARTUP_UNAVAILABLE_GATEWAY_METHODS) { params.unavailableGatewayMethods.delete(method); } - params.onPluginServices?.(result.pluginServices); + if (!pluginServicesReported) { + reportPluginServices(result.pluginServices); + } params.onPostReadySidecars?.(result.postReadySidecars); params.onSidecarsReady?.(); params.startupTrace?.detail("sidecars.ready", [ @@ -937,7 +959,8 @@ export async function startGatewayPostAttachRuntime( }); if (params.deferSidecars !== true) { - const [stopGatewayUpdateCheck, tailscaleCleanup, sidecarsResult] = await Promise.all([ + const [, stopGatewayUpdateCheck, tailscaleCleanup, sidecarsResult] = await Promise.all([ + startupLogPromise, stopGatewayUpdateCheckPromise, tailscaleCleanupPromise, sidecarsPromise, @@ -949,7 +972,8 @@ export async function startGatewayPostAttachRuntime( }; } - const [stopGatewayUpdateCheck, tailscaleCleanup] = await Promise.all([ + const [, stopGatewayUpdateCheck, tailscaleCleanup] = await Promise.all([ + startupLogPromise, stopGatewayUpdateCheckPromise, tailscaleCleanupPromise, ]);