perf(gateway): overlap startup work before ready

Signed-off-by: samzong <samzong.lu@gmail.com>
This commit is contained in:
samzong
2026-05-18 06:45:15 +08:00
committed by Peter Steinberger
parent 69aec10852
commit e7933c9137
2 changed files with 128 additions and 18 deletions
@@ -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<void>((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<void>((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() };
+42 -18
View File
@@ -413,6 +413,7 @@ export async function startGatewaySidecars(params: {
deps: CliDeps;
startChannels: () => Promise<void>;
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<void>((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,
]);