perf: fast-path gateway foreground startup

This commit is contained in:
Peter Steinberger
2026-04-28 02:04:57 +01:00
parent 6b7886e024
commit 955f0a692a
5 changed files with 145 additions and 4 deletions

View File

@@ -10,6 +10,7 @@ import {
defineImportedProgramCommandGroupSpecs,
type CommandGroupDescriptorSpec,
} from "./command-group-descriptors.js";
import { removeCommandByName } from "./command-tree.js";
import { loadPrivateQaCliModule } from "./private-qa-cli.js";
import {
registerCommandGroupByName,
@@ -26,6 +27,28 @@ export { getSubCliCommandsWithSubcommands };
type SubCliRegistrar = (program: Command) => Promise<void> | void;
function shouldRegisterGatewayRunOnly(name: string, argv: string[]): boolean {
if (name !== "gateway") {
return false;
}
const invocation = resolveCliArgvInvocation(argv);
if (invocation.hasHelpOrVersion || invocation.commandPath[0] !== "gateway") {
return false;
}
return invocation.commandPath.length === 1 || invocation.commandPath[1] === "run";
}
async function registerGatewayRunOnly(program: Command): Promise<void> {
const { addGatewayRunCommand } = await import("../gateway-cli/run.js");
removeCommandByName(program, "gateway");
const gateway = addGatewayRunCommand(
program.command("gateway").description("Run, inspect, and query the WebSocket Gateway"),
);
addGatewayRunCommand(
gateway.command("run").description("Run the WebSocket Gateway (foreground)"),
);
}
async function registerSubCliWithPluginCommands(
program: Command,
registerSubCli: () => Promise<void>,
@@ -241,7 +264,15 @@ export function getSubCliEntries(): ReadonlyArray<SubCliDescriptor> {
return getSubCliEntryDescriptors();
}
export async function registerSubCliByName(program: Command, name: string): Promise<boolean> {
export async function registerSubCliByName(
program: Command,
name: string,
argv: string[] = process.argv,
): Promise<boolean> {
if (shouldRegisterGatewayRunOnly(name, argv)) {
await registerGatewayRunOnly(program);
return true;
}
return registerCommandGroupByName(program, resolveSubCliCommandGroups(), name);
}

View File

@@ -47,8 +47,25 @@ const { registerPluginsCli, registerPluginCliCommandsFromValidatedConfig } = vi.
}),
registerPluginCliCommandsFromValidatedConfig: vi.fn(async () => null),
}));
const { addGatewayRunCommand, gatewayRunAction, registerGatewayCli } = vi.hoisted(() => {
const runAction = vi.fn();
return {
addGatewayRunCommand: vi.fn((command: Command) =>
command.option("--force", "force", false).action(runAction),
),
gatewayRunAction: runAction,
registerGatewayCli: vi.fn((program: Command) => {
program
.command("gateway")
.command("call")
.action(() => undefined);
}),
};
});
vi.mock("../acp-cli.js", () => ({ registerAcpCli }));
vi.mock("../gateway-cli.js", () => ({ registerGatewayCli }));
vi.mock("../gateway-cli/run.js", () => ({ addGatewayRunCommand }));
vi.mock("../nodes-cli.js", () => ({ registerNodesCli }));
vi.mock("../capability-cli.js", () => ({ registerCapabilityCli }));
vi.mock("../plugins-cli.js", () => ({ registerPluginsCli }));
@@ -93,6 +110,9 @@ describe("registerSubCliCommands", () => {
inferAction.mockClear();
registerPluginsCli.mockClear();
registerPluginCliCommandsFromValidatedConfig.mockClear();
addGatewayRunCommand.mockClear();
gatewayRunAction.mockClear();
registerGatewayCli.mockClear();
});
afterEach(() => {
@@ -174,6 +194,30 @@ describe("registerSubCliCommands", () => {
expect(acpAction).toHaveBeenCalledTimes(1);
});
it("registers only the gateway run surface for gateway startup", async () => {
const argv = ["node", "openclaw", "gateway", "--force"];
process.argv = argv;
const program = new Command().name("openclaw");
await registerSubCliByName(program, "gateway", argv);
expect(addGatewayRunCommand).toHaveBeenCalledTimes(2);
expect(registerGatewayCli).not.toHaveBeenCalled();
await program.parseAsync(["gateway", "--force"], { from: "user" });
expect(gatewayRunAction).toHaveBeenCalledTimes(1);
});
it("keeps the full gateway CLI for non-run gateway subcommands", async () => {
const argv = ["node", "openclaw", "gateway", "call", "health"];
process.argv = argv;
const program = new Command().name("openclaw");
await registerSubCliByName(program, "gateway", argv);
expect(addGatewayRunCommand).not.toHaveBeenCalled();
expect(registerGatewayCli).toHaveBeenCalledTimes(1);
});
it.each([
["plugins update", ["plugins", "update", "lossless-claw"]],
["plugins update --all", ["plugins", "update", "--all"]],

View File

@@ -46,8 +46,12 @@ export function getSubCliEntries(): ReadonlyArray<SubCliDescriptor> {
return getSubCliEntryDescriptors();
}
export async function registerSubCliByName(program: Command, name: string): Promise<boolean> {
if (await registerSubCliByNameCore(program, name)) {
export async function registerSubCliByName(
program: Command,
name: string,
argv: string[] = process.argv,
): Promise<boolean> {
if (await registerSubCliByNameCore(program, name, argv)) {
return true;
}
return registerCommandGroupByName(program, resolveSubCliCommandGroups(), name);

View File

@@ -9,6 +9,7 @@ import {
shouldUseBrowserHelpFastPath,
shouldUseRootHelpFastPath,
} from "./run-main-policy.js";
import { isGatewayRunFastPathArgv } from "./run-main.js";
const memoryWikiCommandAliasRegistry: PluginManifestCommandAliasRegistry = {
plugins: [
@@ -28,6 +29,17 @@ const memoryCoreCommandAliasRegistry: PluginManifestCommandAliasRegistry = {
],
};
describe("isGatewayRunFastPathArgv", () => {
it("matches only plain gateway foreground starts without root options or help", () => {
expect(isGatewayRunFastPathArgv(["node", "openclaw", "gateway"])).toBe(true);
expect(isGatewayRunFastPathArgv(["node", "openclaw", "gateway", "--force"])).toBe(true);
expect(isGatewayRunFastPathArgv(["node", "openclaw", "gateway", "run"])).toBe(true);
expect(isGatewayRunFastPathArgv(["node", "openclaw", "gateway", "call", "health"])).toBe(false);
expect(isGatewayRunFastPathArgv(["node", "openclaw", "gateway", "--help"])).toBe(false);
expect(isGatewayRunFastPathArgv(["node", "openclaw", "--no-color", "gateway"])).toBe(false);
});
});
describe("rewriteUpdateFlagArgv", () => {
it("leaves argv unchanged when --update is absent", () => {
const argv = ["node", "entry.js", "status"];

View File

@@ -72,6 +72,52 @@ function createGatewayCliMainStartupTrace(argv: string[]) {
};
}
export function isGatewayRunFastPathArgv(argv: string[]): boolean {
if (argv[2] !== "gateway") {
return false;
}
const invocation = resolveCliArgvInvocation(argv);
if (invocation.hasHelpOrVersion || invocation.commandPath[0] !== "gateway") {
return false;
}
return invocation.commandPath.length === 1 || invocation.commandPath[1] === "run";
}
async function tryRunGatewayRunFastPath(
argv: string[],
startupTrace: ReturnType<typeof createGatewayCliMainStartupTrace>,
): Promise<boolean> {
if (!isGatewayRunFastPathArgv(argv)) {
return false;
}
const [{ Command }, { addGatewayRunCommand }] = await startupTrace.measure(
"gateway-run-imports",
() => Promise.all([import("commander"), import("./gateway-cli/run.js")]),
);
const program = new Command();
program.name("openclaw");
program.enablePositionalOptions();
program.exitOverride((err) => {
process.exitCode = typeof err.exitCode === "number" ? err.exitCode : 1;
throw err;
});
const gateway = addGatewayRunCommand(
program.command("gateway").description("Run, inspect, and query the WebSocket Gateway"),
);
addGatewayRunCommand(
gateway.command("run").description("Run the WebSocket Gateway (foreground)"),
);
try {
await startupTrace.measure("gateway-run-parse", () => program.parseAsync(argv));
} catch (error) {
if (!isCommanderParseExit(error)) {
throw error;
}
process.exitCode = error.exitCode;
}
return true;
}
async function closeCliMemoryManagers(): Promise<void> {
const { hasMemoryRuntime } = await import("../plugins/memory-state.js");
if (!hasMemoryRuntime()) {
@@ -255,6 +301,10 @@ export async function runCli(argv: string[] = process.argv) {
await startupTrace.measure("proxy-dispatcher", () => ensureCliEnvProxyDispatcher());
maybeWarnAboutDebugProxyCoverage();
if (await tryRunGatewayRunFastPath(normalizedArgv, startupTrace)) {
return;
}
const { tryRouteCli } = await startupTrace.measure("route-import", () => import("./route.js"));
if (await startupTrace.measure("route", () => tryRouteCli(normalizedArgv))) {
return;
@@ -339,7 +389,7 @@ export async function runCli(argv: string[] = process.argv) {
await registerCoreCliByName(program, ctx, primary, parseArgv);
}
const { registerSubCliByName } = await import("./program/register.subclis.js");
await registerSubCliByName(program, primary);
await registerSubCliByName(program, primary, parseArgv);
});
}