From 452cf09c07b414edf333ecb5439527bed9ccbd0f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 11 Aug 2026 03:51:35 -0700 Subject: [PATCH] perf(cli): skip inactive provider cleanup imports (#121997) --- src/agents/provider-local-service.test.ts | 4 ++ src/agents/provider-local-service.ts | 11 ++--- src/agents/provider-runtime-lifecycle.ts | 20 ++++++++++ .../provider-transport-dispatcher-pool.ts | 13 ++++-- src/cli/run-main.exit.test.ts | 40 ++++++++++++++++--- src/cli/run-main.ts | 20 +++++++--- src/infra/net/pinned-dispatcher-pool.test.ts | 9 +++++ 7 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 src/agents/provider-runtime-lifecycle.ts diff --git a/src/agents/provider-local-service.test.ts b/src/agents/provider-local-service.test.ts index e14ebb47aa01..e727e9e34597 100644 --- a/src/agents/provider-local-service.test.ts +++ b/src/agents/provider-local-service.test.ts @@ -23,6 +23,7 @@ import { hasLocalServiceProcessExited, stopManagedProviderLocalServices, } from "./provider-local-service.js"; +import { hasManagedProviderLocalServices } from "./provider-runtime-lifecycle.js"; const ONE_SHOT_HOST_READY_TIMEOUT_MS = 30_000; const ONE_SHOT_HOST_EXIT_TIMEOUT_MS = 5_000; @@ -204,6 +205,7 @@ describe("provider local service", () => { }); it("starts an on-demand local service and stops it after idle", async () => { + expect(hasManagedProviderLocalServices()).toBe(false); const port = await freePort(); const healthUrl = `http://127.0.0.1:${port}/v1/models`; const model = attachModelProviderLocalService( @@ -230,9 +232,11 @@ describe("provider local service", () => { if (!lease) { throw new Error("Expected provider local service lease"); } + expect(hasManagedProviderLocalServices()).toBe(true); expect((await fetch(healthUrl)).ok).toBe(true); lease.release(); await waitForProbeFailure(healthUrl); + expect(hasManagedProviderLocalServices()).toBe(false); }); it("resolves process configuration from the host config", async () => { diff --git a/src/agents/provider-local-service.ts b/src/agents/provider-local-service.ts index 7ec0ccaf00ca..d5810c4bf10e 100644 --- a/src/agents/provider-local-service.ts +++ b/src/agents/provider-local-service.ts @@ -22,6 +22,7 @@ import { signalChildProcessTree, shouldDetachChildForProcessTree, } from "../process/child-process-tree.js"; +import { setManagedProviderLocalServicesActive } from "./provider-runtime-lifecycle.js"; import { unwrapHeadersInitSentinelsForProviderEgress } from "./provider-secret-egress.js"; const log = createSubsystemLogger("provider-local-service"); @@ -228,6 +229,7 @@ export async function ensureProviderLocalService( installExitHandler(); const managed = services.get(key) ?? { active: 0 }; services.set(key, managed); + setManagedProviderLocalServicesActive(true); clearIdleTimer(managed); managed.active += 1; @@ -294,6 +296,7 @@ export function stopManagedProviderLocalServices(): void { stopManagedService(key, managed, "host-shutdown"); } services.clear(); + setManagedProviderLocalServicesActive(false); } /** Return bounded local-service state for focused lifecycle tests. */ @@ -312,11 +315,7 @@ function validateLocalServiceConfig(service: ModelProviderLocalServiceConfig, pr } function resolveHealthUrl(service: ModelProviderLocalServiceConfig, baseUrl: string): string { - const configured = service.healthUrl?.trim(); - if (configured) { - return configured; - } - return `${baseUrl.replace(/\/+$/, "")}/models`; + return service.healthUrl?.trim() || `${baseUrl.replace(/\/+$/, "")}/models`; } function localServiceKey( @@ -583,6 +582,7 @@ function scheduleIdleStop( if (!managed.process) { if (!managed.starting) { services.delete(key); + setManagedProviderLocalServicesActive(services.size > 0); } return; } @@ -613,6 +613,7 @@ function stopManagedService(key: string, managed: ManagedLocalService, reason: s managed.process = undefined; managed.lastExit = undefined; services.delete(key); + setManagedProviderLocalServicesActive(services.size > 0); if (child) { drainLocalServiceOutput(child); } diff --git a/src/agents/provider-runtime-lifecycle.ts b/src/agents/provider-runtime-lifecycle.ts new file mode 100644 index 000000000000..21764276be83 --- /dev/null +++ b/src/agents/provider-runtime-lifecycle.ts @@ -0,0 +1,20 @@ +// Provider owners record lifecycle facts here so short-lived CLI teardown can +// avoid loading heavyweight provider modules when they never created resources. +let managedProviderLocalServicesActive = false; +let providerTransportDispatcherPoolActive = false; + +export function setManagedProviderLocalServicesActive(active: boolean): void { + managedProviderLocalServicesActive = active; +} + +export function hasManagedProviderLocalServices(): boolean { + return managedProviderLocalServicesActive; +} + +export function setProviderTransportDispatcherPoolActive(active: boolean): void { + providerTransportDispatcherPoolActive = active; +} + +export function hasProviderTransportDispatcherPool(): boolean { + return providerTransportDispatcherPoolActive; +} diff --git a/src/agents/provider-transport-dispatcher-pool.ts b/src/agents/provider-transport-dispatcher-pool.ts index f52a1ed3939f..7deb99539be9 100644 --- a/src/agents/provider-transport-dispatcher-pool.ts +++ b/src/agents/provider-transport-dispatcher-pool.ts @@ -1,4 +1,5 @@ import { PinnedDispatcherPool } from "../infra/net/pinned-dispatcher-pool.js"; +import { setProviderTransportDispatcherPoolActive } from "./provider-runtime-lifecycle.js"; const PROVIDER_DISPATCHER_POOL_MAX_ENTRIES = 16; const PROVIDER_DISPATCHER_POOL_IDLE_TTL_MS = 60_000; @@ -7,10 +8,13 @@ let activePool: PinnedDispatcherPool | undefined; /** Returns the current process-lifecycle provider dispatcher pool generation. */ export function getProviderTransportDispatcherPool(): PinnedDispatcherPool { - activePool ??= new PinnedDispatcherPool({ - maxEntries: PROVIDER_DISPATCHER_POOL_MAX_ENTRIES, - idleTtlMs: PROVIDER_DISPATCHER_POOL_IDLE_TTL_MS, - }); + if (!activePool) { + activePool = new PinnedDispatcherPool({ + maxEntries: PROVIDER_DISPATCHER_POOL_MAX_ENTRIES, + idleTtlMs: PROVIDER_DISPATCHER_POOL_IDLE_TTL_MS, + }); + setProviderTransportDispatcherPoolActive(true); + } return activePool; } @@ -21,6 +25,7 @@ export async function closeProviderTransportDispatcherPool(): Promise { await pool.closeAll(); if (activePool === pool) { activePool = undefined; + setProviderTransportDispatcherPoolActive(false); } } } diff --git a/src/cli/run-main.exit.test.ts b/src/cli/run-main.exit.test.ts index bc3796152ab8..0f5c356dd204 100644 --- a/src/cli/run-main.exit.test.ts +++ b/src/cli/run-main.exit.test.ts @@ -58,6 +58,9 @@ const closeActiveMemorySearchManagersMock = vi.hoisted(() => vi.fn(async () => { const hasMemoryRuntimeMock = vi.hoisted(() => vi.fn(() => false)); const listRegisteredAgentHarnessesMock = vi.hoisted(() => vi.fn((): unknown[] => [])); const disposeRegisteredAgentHarnessesMock = vi.hoisted(() => vi.fn(async () => {})); +const hasManagedProviderLocalServicesMock = vi.hoisted(() => vi.fn(() => false)); +const hasProviderTransportDispatcherPoolMock = vi.hoisted(() => vi.fn(() => false)); +const providerCleanupModuleImportState = vi.hoisted(() => ({ local: 0, transport: 0 })); const stopManagedProviderLocalServicesMock = vi.hoisted(() => vi.fn()); const closeProviderTransportDispatcherPoolMock = vi.hoisted(() => vi.fn(async () => {})); const getActiveMcpLoopbackRuntimeMock = vi.hoisted(() => @@ -320,13 +323,20 @@ vi.mock("../agents/harness/registry.js", () => ({ disposeRegisteredAgentHarnesses: disposeRegisteredAgentHarnessesMock, })); -vi.mock("../agents/provider-local-service.js", () => ({ - stopManagedProviderLocalServices: stopManagedProviderLocalServicesMock, +vi.mock("../agents/provider-runtime-lifecycle.js", () => ({ + hasManagedProviderLocalServices: hasManagedProviderLocalServicesMock, + hasProviderTransportDispatcherPool: hasProviderTransportDispatcherPoolMock, })); -vi.mock("../agents/provider-transport-dispatcher-pool.js", () => ({ - closeProviderTransportDispatcherPool: closeProviderTransportDispatcherPoolMock, -})); +vi.mock("../agents/provider-local-service.js", () => { + providerCleanupModuleImportState.local += 1; + return { stopManagedProviderLocalServices: stopManagedProviderLocalServicesMock }; +}); + +vi.mock("../agents/provider-transport-dispatcher-pool.js", () => { + providerCleanupModuleImportState.transport += 1; + return { closeProviderTransportDispatcherPool: closeProviderTransportDispatcherPoolMock }; +}); vi.mock("../gateway/mcp-http.loopback-runtime.js", () => ({ getActiveMcpLoopbackRuntime: getActiveMcpLoopbackRuntimeMock, @@ -570,6 +580,8 @@ describe("runCli exit behavior", () => { }); hasMemoryRuntimeMock.mockReturnValue(false); listRegisteredAgentHarnessesMock.mockReturnValue([]); + hasManagedProviderLocalServicesMock.mockReturnValue(false); + hasProviderTransportDispatcherPoolMock.mockReturnValue(false); outputPrecomputedBrowserHelpTextMock.mockReturnValue(false); outputPrecomputedNodesHelpTextMock.mockReturnValue(false); outputPrecomputedRootHelpTextMock.mockReturnValue(false); @@ -595,6 +607,22 @@ describe("runCli exit behavior", () => { loggingState.forceConsoleToStderr = false; }); + it("does not load inactive provider cleanup modules for cold help", async () => { + tryRouteCliMock.mockResolvedValueOnce(false); + const parseAsync = vi.fn().mockResolvedValueOnce(undefined); + buildProgramMock.mockReturnValueOnce({ + commands: [{ name: () => "nodes", aliases: () => [] }], + parseAsync, + }); + + await withEnvAsync({ OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH: "1" }, async () => { + await runCli(["node", "openclaw", "nodes", "--help"]); + }); + + expect(parseAsync).toHaveBeenCalledWith(["node", "openclaw", "nodes", "--help"]); + expect(providerCleanupModuleImportState).toEqual({ local: 0, transport: 0 }); + }); + it("does not import dotenv for gateway forms without a workspace file", async () => { existsSyncOverride.value = () => false; expect(dotenvModuleImportState.count).toBe(0); @@ -673,6 +701,8 @@ describe("runCli exit behavior", () => { it("completes asynchronous teardown before returning to the outer entrypoint", async () => { const order: string[] = []; listRegisteredAgentHarnessesMock.mockReturnValueOnce([{ harness: { id: "copilot" } }]); + hasManagedProviderLocalServicesMock.mockReturnValueOnce(true); + hasProviderTransportDispatcherPoolMock.mockReturnValueOnce(true); disposeRegisteredAgentHarnessesMock.mockImplementationOnce(async () => { order.push("harnesses"); }); diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index d17e0be3a179..1f2d81959db1 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -274,14 +274,22 @@ async function closeCliResources(): Promise { } }, async () => { - const { stopManagedProviderLocalServices } = - await import("../agents/provider-local-service.js"); - stopManagedProviderLocalServices(); + const { hasManagedProviderLocalServices } = + await import("../agents/provider-runtime-lifecycle.js"); + if (hasManagedProviderLocalServices()) { + const { stopManagedProviderLocalServices } = + await import("../agents/provider-local-service.js"); + stopManagedProviderLocalServices(); + } }, async () => { - const { closeProviderTransportDispatcherPool } = - await import("../agents/provider-transport-dispatcher-pool.js"); - await closeProviderTransportDispatcherPool(); + const { hasProviderTransportDispatcherPool } = + await import("../agents/provider-runtime-lifecycle.js"); + if (hasProviderTransportDispatcherPool()) { + const { closeProviderTransportDispatcherPool } = + await import("../agents/provider-transport-dispatcher-pool.js"); + await closeProviderTransportDispatcherPool(); + } }, async () => { const { getActiveMcpLoopbackRuntime } = diff --git a/src/infra/net/pinned-dispatcher-pool.test.ts b/src/infra/net/pinned-dispatcher-pool.test.ts index 4a521e82f7c1..871d07133f0d 100644 --- a/src/infra/net/pinned-dispatcher-pool.test.ts +++ b/src/infra/net/pinned-dispatcher-pool.test.ts @@ -1,5 +1,6 @@ import type { Dispatcher } from "undici"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { hasProviderTransportDispatcherPool } from "../../agents/provider-runtime-lifecycle.js"; import { closeProviderTransportDispatcherPool, getProviderTransportDispatcherPool, @@ -21,6 +22,14 @@ describe("PinnedDispatcherPool", () => { await closeProviderTransportDispatcherPool(); }); + it("records dispatcher-pool activity only for an allocated generation", async () => { + expect(hasProviderTransportDispatcherPool()).toBe(false); + getProviderTransportDispatcherPool(); + expect(hasProviderTransportDispatcherPool()).toBe(true); + await closeProviderTransportDispatcherPool(); + expect(hasProviderTransportDispatcherPool()).toBe(false); + }); + it("reuses an exact live key and closes it only at lifecycle shutdown", async () => { const pool = new PinnedDispatcherPool({ maxEntries: 2, idleTtlMs: 60_000 }); const created = createDispatcher();