perf(browser): precompute browser help

This commit is contained in:
Peter Steinberger 2026-04-25 13:07:01 +01:00
parent 3064ea78ab
commit c977643460
No known key found for this signature in database
8 changed files with 212 additions and 17 deletions

View file

@ -55,6 +55,7 @@ Docs: https://docs.openclaw.ai
- Subagents: fall back to direct completion delivery when the parent announce turn finishes without a visible payload, so child results still reach channel-backed requester sessions.
- Browser/Playwright: ignore benign already-handled route races during guarded navigation so browser-page tasks no longer fail when Playwright tears down a route mid-flight. (#68708) Thanks @Steady-ai.
- Browser/CLI: lazy-load browser command groups and plugin runtime services so `openclaw browser --help` can render without loading the full browser automation stack. Fixes #65400. (#65460, #66640) Thanks @pandego and @Tianworld.
- Browser/CLI: serve precomputed `openclaw browser --help` text from CLI startup metadata, avoiding the full plugin/config startup path for the common help invocation.
- Browser/downloads: seed managed Chrome profiles with OpenClaw download prefs and capture unmanaged click-triggered downloads under the guarded downloads directory, while explicit download waiters still own their target file. (#64558) Thanks @Pearcekieser.
- Browser/Chrome: stop passing redundant `--disable-setuid-sandbox` when `browser.noSandbox` is enabled; `--no-sandbox` remains the effective sandbox opt-out. (#67939) Thanks @sebykrueger.
- Browser/client: stop telling agents to permanently avoid the browser after transient timeout or cancellation failures; keep the no-retry hint for persistent unavailable/rate-limit cases. (#46505) Thanks @jriff.

View file

@ -128,13 +128,18 @@ const buildMissingEntryErrorMessage = async () => {
const isBareRootHelpInvocation = (argv) =>
argv.length === 3 && (argv[2] === "--help" || argv[2] === "-h");
const loadPrecomputedRootHelpText = () => {
const isBrowserHelpInvocation = (argv) =>
argv.length === 4 && argv[2] === "browser" && (argv[3] === "--help" || argv[3] === "-h");
const isHelpFastPathDisabled = () =>
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1";
const loadPrecomputedHelpText = (key) => {
try {
const raw = readFileSync(new URL("./dist/cli-startup-metadata.json", import.meta.url), "utf8");
const parsed = JSON.parse(raw);
return typeof parsed?.rootHelpText === "string" && parsed.rootHelpText.length > 0
? parsed.rootHelpText
: null;
const value = parsed?.[key];
return typeof value === "string" && value.length > 0 ? value : null;
} catch {
return null;
}
@ -144,7 +149,7 @@ const tryOutputBareRootHelp = async () => {
if (!isBareRootHelpInvocation(process.argv)) {
return false;
}
const precomputed = loadPrecomputedRootHelpText();
const precomputed = loadPrecomputedHelpText("rootHelpText");
if (precomputed) {
process.stdout.write(precomputed);
return true;
@ -166,7 +171,21 @@ const tryOutputBareRootHelp = async () => {
return false;
};
if (await tryOutputBareRootHelp()) {
const tryOutputBrowserHelp = () => {
if (!isBrowserHelpInvocation(process.argv)) {
return false;
}
const precomputed = loadPrecomputedHelpText("browserHelpText");
if (!precomputed) {
return false;
}
process.stdout.write(precomputed);
return true;
};
if (!isHelpFastPathDisabled() && (await tryOutputBareRootHelp())) {
// OK
} else if (!isHelpFastPathDisabled() && tryOutputBrowserHelp()) {
// OK
} else {
await installProcessWarningFilter();

View file

@ -26,6 +26,7 @@ const distDir = path.join(rootDir, "dist");
const outputPath = path.join(distDir, "cli-startup-metadata.json");
const extensionsDir = path.join(rootDir, "extensions");
const ROOT_HELP_RENDER_TIMEOUT_MS = 120_000;
const BROWSER_HELP_RENDER_TIMEOUT_MS = 120_000;
const CORE_CHANNEL_ORDER = [
"telegram",
"whatsapp",
@ -70,6 +71,29 @@ function resolveRootHelpBundleIdentity(
};
}
function updateHashFromFiles(hash: ReturnType<typeof createHash>, files: string[]) {
for (const file of files.toSorted()) {
hash.update(`${path.relative(rootDir, file)}\0`);
hash.update(readFileSync(file));
hash.update("\0");
}
}
function resolveBrowserHelpSourceSignature(): string {
const hash = createHash("sha1");
const browserCliDir = path.join(rootDir, "extensions/browser/src/cli");
const browserCliFiles = readdirSync(browserCliDir)
.filter((entry) => entry.endsWith(".ts"))
.map((entry) => path.join(browserCliDir, entry));
updateHashFromFiles(hash, browserCliFiles);
updateHashFromFiles(hash, [
path.join(rootDir, "src/cli/program/help.ts"),
path.join(rootDir, "src/cli/program/context.ts"),
path.join(rootDir, "src/cli/banner.ts"),
]);
return hash.digest("hex");
}
export function readBundledChannelCatalog(
extensionsDirOverride: string = extensionsDir,
): BundledChannelCatalog {
@ -245,6 +269,53 @@ function renderSourceRootHelpText(
return result.stdout ?? "";
}
function renderSourceBrowserHelpText(
renderContext: RootHelpRenderContext = createIsolatedRootHelpRenderContext(),
): string {
const browserCliUrl = pathToFileURL(
path.join(rootDir, "extensions/browser/src/cli/browser-cli.ts"),
).href;
const helpUrl = pathToFileURL(path.join(rootDir, "src/cli/program/help.ts")).href;
const contextUrl = pathToFileURL(path.join(rootDir, "src/cli/program/context.ts")).href;
const inlineModule = [
`const { Command } = await import("commander");`,
`const { registerBrowserCli } = await import(${JSON.stringify(browserCliUrl)});`,
`const { configureProgramHelp } = await import(${JSON.stringify(helpUrl)});`,
`const { createProgramContext } = await import(${JSON.stringify(contextUrl)});`,
`const program = new Command();`,
`configureProgramHelp(program, createProgramContext());`,
`registerBrowserCli(program, ["node", "openclaw", "browser", "--help"]);`,
`const browser = program.commands.find((cmd) => cmd.name() === "browser");`,
`if (!browser) throw new Error("Browser command was not registered.");`,
`browser.outputHelp();`,
"process.exit(0);",
].join("\n");
const result = spawnSync(
process.execPath,
["--import", "tsx", "--input-type=module", "--eval", inlineModule],
{
cwd: rootDir,
encoding: "utf8",
env: {
...renderContext.env,
OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH: "1",
},
timeout: BROWSER_HELP_RENDER_TIMEOUT_MS,
},
);
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
const stderr = result.stderr?.trim();
throw new Error(
"Failed to render source browser help" +
(stderr ? `: ${stderr}` : result.signal ? `: terminated by ${result.signal}` : ""),
);
}
return result.stdout ?? "";
}
export async function writeCliStartupMetadata(options?: {
distDir?: string;
outputPath?: string;
@ -255,6 +326,7 @@ export async function writeCliStartupMetadata(options?: {
const resolvedExtensionsDir = options?.extensionsDir ?? extensionsDir;
const channelCatalog = readBundledChannelCatalog(resolvedExtensionsDir);
const bundleIdentity = resolveRootHelpBundleIdentity(resolvedDistDir);
const browserHelpSourceSignature = resolveBrowserHelpSourceSignature();
const bundledPluginsDir = path.join(resolvedDistDir, "extensions");
const renderContext = createIsolatedRootHelpRenderContext(
existsSync(bundledPluginsDir) ? bundledPluginsDir : resolvedExtensionsDir,
@ -264,12 +336,17 @@ export async function writeCliStartupMetadata(options?: {
try {
const existing = JSON.parse(readFileSync(resolvedOutputPath, "utf8")) as {
rootHelpBundleSignature?: unknown;
browserHelpSourceSignature?: unknown;
channelCatalogSignature?: unknown;
browserHelpText?: unknown;
};
if (
bundleIdentity &&
existing.rootHelpBundleSignature === bundleIdentity.signature &&
existing.channelCatalogSignature === channelCatalog.signature
existing.browserHelpSourceSignature === browserHelpSourceSignature &&
existing.channelCatalogSignature === channelCatalog.signature &&
typeof existing.browserHelpText === "string" &&
existing.browserHelpText.length > 0
) {
return;
}
@ -283,6 +360,7 @@ export async function writeCliStartupMetadata(options?: {
} catch {
rootHelpText = renderSourceRootHelpText(renderContext);
}
const browserHelpText = renderSourceBrowserHelpText(renderContext);
mkdirSync(resolvedDistDir, { recursive: true });
writeFileSync(
@ -293,6 +371,8 @@ export async function writeCliStartupMetadata(options?: {
channelOptions,
channelCatalogSignature: channelCatalog.signature,
rootHelpBundleSignature: bundleIdentity?.signature ?? null,
browserHelpSourceSignature,
browserHelpText,
rootHelpText,
},
null,

View file

@ -3,10 +3,15 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
let precomputedRootHelpText: string | null | undefined;
let precomputedBrowserHelpText: string | null | undefined;
export function loadPrecomputedRootHelpText(): string | null {
if (precomputedRootHelpText !== undefined) {
return precomputedRootHelpText;
function loadPrecomputedHelpText(
key: "rootHelpText" | "browserHelpText",
cache: string | null | undefined,
setCache: (value: string | null) => void,
): string | null {
if (cache !== undefined) {
return cache;
}
try {
const metadataPath = path.resolve(
@ -15,18 +20,31 @@ export function loadPrecomputedRootHelpText(): string | null {
"cli-startup-metadata.json",
);
const raw = fs.readFileSync(metadataPath, "utf8");
const parsed = JSON.parse(raw) as { rootHelpText?: unknown };
if (typeof parsed.rootHelpText === "string" && parsed.rootHelpText.length > 0) {
precomputedRootHelpText = parsed.rootHelpText;
return precomputedRootHelpText;
const parsed = JSON.parse(raw) as Record<string, unknown>;
const value = parsed[key];
if (typeof value === "string" && value.length > 0) {
setCache(value);
return value;
}
} catch {
// Fall back to live root-help rendering.
// Fall back to live help rendering.
}
precomputedRootHelpText = null;
setCache(null);
return null;
}
export function loadPrecomputedRootHelpText(): string | null {
return loadPrecomputedHelpText("rootHelpText", precomputedRootHelpText, (value) => {
precomputedRootHelpText = value;
});
}
export function loadPrecomputedBrowserHelpText(): string | null {
return loadPrecomputedHelpText("browserHelpText", precomputedBrowserHelpText, (value) => {
precomputedBrowserHelpText = value;
});
}
export function outputPrecomputedRootHelpText(): boolean {
const rootHelpText = loadPrecomputedRootHelpText();
if (!rootHelpText) {
@ -36,8 +54,18 @@ export function outputPrecomputedRootHelpText(): boolean {
return true;
}
export function outputPrecomputedBrowserHelpText(): boolean {
const browserHelpText = loadPrecomputedBrowserHelpText();
if (!browserHelpText) {
return false;
}
process.stdout.write(browserHelpText);
return true;
}
export const __testing = {
resetPrecomputedRootHelpTextForTests(): void {
precomputedRootHelpText = undefined;
precomputedBrowserHelpText = undefined;
},
};

View file

@ -14,6 +14,7 @@ const ensureTaskRegistryReadyMock = vi.hoisted(() => vi.fn());
const startTaskRegistryMaintenanceMock = vi.hoisted(() => vi.fn());
const outputRootHelpMock = vi.hoisted(() => vi.fn());
const outputPrecomputedRootHelpTextMock = vi.hoisted(() => vi.fn(() => false));
const outputPrecomputedBrowserHelpTextMock = vi.hoisted(() => vi.fn(() => false));
const buildProgramMock = vi.hoisted(() => vi.fn());
const getProgramContextMock = vi.hoisted(() => vi.fn(() => null));
const registerCoreCliByNameMock = vi.hoisted(() => vi.fn());
@ -71,6 +72,7 @@ vi.mock("./program/root-help.js", () => ({
}));
vi.mock("./root-help-metadata.js", () => ({
outputPrecomputedBrowserHelpText: outputPrecomputedBrowserHelpTextMock,
outputPrecomputedRootHelpText: outputPrecomputedRootHelpTextMock,
}));
@ -98,8 +100,10 @@ describe("runCli exit behavior", () => {
beforeEach(() => {
vi.clearAllMocks();
hasMemoryRuntimeMock.mockReturnValue(false);
outputPrecomputedBrowserHelpTextMock.mockReturnValue(false);
outputPrecomputedRootHelpTextMock.mockReturnValue(false);
getProgramContextMock.mockReturnValue(null);
delete process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH;
});
it("does not force process.exit after successful routed command", async () => {
@ -119,6 +123,29 @@ describe("runCli exit behavior", () => {
exitSpy.mockRestore();
});
it("renders browser help from startup metadata without building the full program", async () => {
outputPrecomputedBrowserHelpTextMock.mockReturnValueOnce(true);
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
throw new Error(`unexpected process.exit(${String(code)})`);
}) as typeof process.exit);
await runCli(["node", "openclaw", "browser", "--help"]);
expect(maybeRunCliInContainerMock).toHaveBeenCalledWith([
"node",
"openclaw",
"browser",
"--help",
]);
expect(tryRouteCliMock).not.toHaveBeenCalled();
expect(outputPrecomputedBrowserHelpTextMock).toHaveBeenCalledTimes(1);
expect(outputRootHelpMock).not.toHaveBeenCalled();
expect(buildProgramMock).not.toHaveBeenCalled();
expect(closeActiveMemorySearchManagersMock).not.toHaveBeenCalled();
expect(exitSpy).not.toHaveBeenCalled();
exitSpy.mockRestore();
});
it("renders root help without building the full program", async () => {
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
throw new Error(`unexpected process.exit(${String(code)})`);

View file

@ -6,6 +6,7 @@ import {
shouldEnsureCliPath,
shouldStartCrestodianForBareRoot,
shouldStartCrestodianForModernOnboard,
shouldUseBrowserHelpFastPath,
shouldUseRootHelpFastPath,
} from "./run-main.js";
@ -131,6 +132,20 @@ describe("shouldUseRootHelpFastPath", () => {
});
});
describe("shouldUseBrowserHelpFastPath", () => {
it("uses the fast path for browser command help only", () => {
expect(shouldUseBrowserHelpFastPath(["node", "openclaw", "browser", "--help"])).toBe(true);
expect(shouldUseBrowserHelpFastPath(["node", "openclaw", "browser", "-h"])).toBe(true);
expect(
shouldUseBrowserHelpFastPath(["node", "openclaw", "--profile", "work", "browser", "-h"]),
).toBe(true);
expect(shouldUseBrowserHelpFastPath(["node", "openclaw", "browser", "status", "--help"])).toBe(
false,
);
expect(shouldUseBrowserHelpFastPath(["node", "openclaw", "status", "--help"])).toBe(false);
});
});
describe("resolveMissingPluginCommandMessage", () => {
it("explains plugins.allow misses for a bundled plugin command", () => {
expect(

View file

@ -68,7 +68,22 @@ export function shouldEnsureCliPath(argv: string[]): boolean {
}
export function shouldUseRootHelpFastPath(argv: string[]): boolean {
return resolveCliArgvInvocation(argv).isRootHelpInvocation;
return (
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH !== "1" &&
resolveCliArgvInvocation(argv).isRootHelpInvocation
);
}
export function shouldUseBrowserHelpFastPath(argv: string[]): boolean {
if (process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1") {
return false;
}
const invocation = resolveCliArgvInvocation(argv);
return (
invocation.commandPath.length === 1 &&
invocation.commandPath[0] === "browser" &&
invocation.hasHelpOrVersion
);
}
export function shouldStartCrestodianForBareRoot(argv: string[]): boolean {
@ -217,6 +232,13 @@ export async function runCli(argv: string[] = process.argv) {
return;
}
if (shouldUseBrowserHelpFastPath(normalizedArgv)) {
const { outputPrecomputedBrowserHelpText } = await import("./root-help-metadata.js");
if (outputPrecomputedBrowserHelpText()) {
return;
}
}
if (shouldStartCrestodianForBareRoot(normalizedArgv)) {
if (!process.stdin.isTTY || !process.stdout.isTTY) {
console.error(

View file

@ -32,10 +32,13 @@ describe("write-cli-startup-metadata", () => {
await writeCliStartupMetadata({ distDir, outputPath, extensionsDir });
const written = JSON.parse(readFileSync(outputPath, "utf8")) as {
browserHelpText: string;
channelOptions: string[];
rootHelpText: string;
};
expect(written.channelOptions).toContain("matrix");
expect(written.browserHelpText).toContain("Usage:");
expect(written.browserHelpText).toContain("openclaw browser");
expect(written.rootHelpText).toContain("Usage:");
expect(written.rootHelpText).toContain("openclaw");
});