mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-24 17:31:04 +00:00
perf(tui): defer EmbeddedTuiBackend import, drop dead warmup helpers (#84701)
* perf(tui): skip plugin-aware config validation on remote TUI startup Cold `openclaw tui` against a remote gateway was synchronously calling loadPluginMetadataSnapshot() via getRuntimeConfig() -> loadConfig() -> validateConfigObjectWithPlugins(), pulling the full plugin metadata snapshot (200k+ file reads) onto the TUI's event loop. The TUI itself never consumes plugin metadata in remote mode; it queries the gateway over RPC. The work was being done purely to validate the config and then thrown away. Thread an opt-in `skipPluginValidation` flag through getRuntimeConfig() and loadConfig() (createConfigIO already supports pluginValidation: "skip"; it just wasn't reachable from the runtime entrypoints). The TUI passes skipPluginValidation: !isLocalMode so: - Remote-mode TUI: no plugin metadata load, no event-loop freeze after first render - Embedded (--local) mode: unchanged; the in-process agent runtime still gets a fully validated config * remove verbose comments * perf(tui): move context cache warmup from module top-level to embedded backend agents/context.ts fired ensureContextWindowCacheLoaded() unconditionally at module-eval time for non-skip-listed CLI commands. The TUI transitively imports this module, so the warmup ran on every TUI startup including remote-mode, cascading into ensureOpenClawModelsJson -> resolveImplicitProviders -> runProviderCatalog and dominating the cold-start freeze (CPU profile showed ~55s of resolveProviderSyntheticAuthWithPlugin, lstat, open, etc.). It also pre-emptively called getRuntimeConfig() without skipPluginValidation, pinning the full snapshot and nullifying the skip flag added on this branch. Remove the top-level side effect and trigger the warmup explicitly from EmbeddedTuiBackend.start(), which only runs when an in-process agent runtime actually needs the cache. * perf(tui): defer EmbeddedTuiBackend import until local mode * refactor(agents): remove dead context-cache warmup helpers
This commit is contained in:
@@ -201,44 +201,6 @@ describe("lookupContextTokens", () => {
|
||||
expect(secondLoadConfigMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("only warms eagerly for real openclaw startup commands that need model metadata", async () => {
|
||||
const { shouldEagerWarmContextWindowCache } = await importContextModule();
|
||||
|
||||
expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "chat"])).toBe(true);
|
||||
expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "chat", "--help"])).toBe(false);
|
||||
expect(
|
||||
shouldEagerWarmContextWindowCache(["node", "openclaw", "matrix", "encryption", "help"]),
|
||||
).toBe(false);
|
||||
expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "help", "matrix"])).toBe(false);
|
||||
expect(
|
||||
shouldEagerWarmContextWindowCache(["node", "openclaw", "browser", "status", "--help"]),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldEagerWarmContextWindowCache([
|
||||
"node",
|
||||
"openclaw",
|
||||
"--profile",
|
||||
"--",
|
||||
"config",
|
||||
"validate",
|
||||
]),
|
||||
).toBe(false);
|
||||
expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "logs", "--limit", "5"])).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
shouldEagerWarmContextWindowCache(["node", "openclaw", "memory", "search", "--json"]),
|
||||
).toBe(false);
|
||||
expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "message", "read"])).toBe(false);
|
||||
expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "status", "--json"])).toBe(false);
|
||||
expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "sessions", "--json"])).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
shouldEagerWarmContextWindowCache(["node", "scripts/test-built-plugin-singleton.mjs"]),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("retries config loading after backoff when an initial load fails", async () => {
|
||||
vi.useFakeTimers();
|
||||
const loadConfigMock = vi
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
// Lazy-load pi-coding-agent model metadata so we can infer context windows when
|
||||
// the agent reports a model id. This includes custom models.json entries.
|
||||
|
||||
import path from "node:path";
|
||||
import { isHelpOrVersionInvocation } from "../cli/argv.js";
|
||||
import { getRuntimeConfig } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { computeBackoff, type BackoffPolicy } from "../infra/backoff.js";
|
||||
import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { resolveDefaultAgentDir } from "./agent-scope.js";
|
||||
import { lookupCachedContextTokens, MODEL_CONTEXT_TOKEN_CACHE } from "./context-cache.js";
|
||||
@@ -108,82 +105,6 @@ function loadModelsConfigRuntime() {
|
||||
return CONTEXT_WINDOW_RUNTIME_STATE.modelsConfigRuntimeLoader.load();
|
||||
}
|
||||
|
||||
function isLikelyOpenClawCliProcess(argv: string[] = process.argv): boolean {
|
||||
const entryBasename = normalizeLowercaseStringOrEmpty(path.basename(argv[1] ?? ""));
|
||||
return (
|
||||
entryBasename === "openclaw" ||
|
||||
entryBasename === "openclaw.mjs" ||
|
||||
entryBasename === "entry.js" ||
|
||||
entryBasename === "entry.mjs"
|
||||
);
|
||||
}
|
||||
|
||||
function getCommandPathFromArgv(argv: string[]): string[] {
|
||||
const args = argv.slice(2);
|
||||
const tokens: string[] = [];
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
if (!arg || arg === FLAG_TERMINATOR) {
|
||||
break;
|
||||
}
|
||||
const consumed = consumeRootOptionToken(args, i);
|
||||
if (consumed > 0) {
|
||||
i += consumed - 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("-")) {
|
||||
continue;
|
||||
}
|
||||
tokens.push(arg);
|
||||
if (tokens.length >= 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
const SKIP_EAGER_WARMUP_PRIMARY_COMMANDS = new Set([
|
||||
"agent",
|
||||
"backup",
|
||||
"browser",
|
||||
"completion",
|
||||
"config",
|
||||
"directory",
|
||||
"doctor",
|
||||
"gateway",
|
||||
"health",
|
||||
"hooks",
|
||||
"logs",
|
||||
"memory",
|
||||
"message",
|
||||
"models",
|
||||
"pairing",
|
||||
"plugins",
|
||||
"secrets",
|
||||
"sessions",
|
||||
"status",
|
||||
"update",
|
||||
"webhooks",
|
||||
]);
|
||||
|
||||
export function shouldEagerWarmContextWindowCache(argv: string[] = process.argv): boolean {
|
||||
// Keep this gate tied to the real OpenClaw CLI entrypoints.
|
||||
//
|
||||
// This module can also land inside shared dist chunks that are imported from
|
||||
// plugin-sdk/library surfaces during smoke tests and plugin loading. If we do
|
||||
// eager warmup for those generic Node script imports, merely importing the
|
||||
// built plugin-sdk can call ensureOpenClawModelsJson(), which cascades into
|
||||
// plugin discovery and breaks dist/source singleton assumptions.
|
||||
if (!isLikelyOpenClawCliProcess(argv)) {
|
||||
return false;
|
||||
}
|
||||
if (isHelpOrVersionInvocation(argv)) {
|
||||
return false;
|
||||
}
|
||||
const [primary] = getCommandPathFromArgv(argv);
|
||||
return Boolean(primary) && !SKIP_EAGER_WARMUP_PRIMARY_COMMANDS.has(primary);
|
||||
}
|
||||
|
||||
function primeConfiguredContextWindows(): OpenClawConfig | undefined {
|
||||
if (CONTEXT_WINDOW_RUNTIME_STATE.configuredConfig) {
|
||||
applyConfiguredContextWindows({
|
||||
|
||||
@@ -28,7 +28,6 @@ import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { getSlashCommands } from "./commands.js";
|
||||
import { ChatLog } from "./components/chat-log.js";
|
||||
import { CustomEditor } from "./components/custom-editor.js";
|
||||
import { EmbeddedTuiBackend } from "./embedded-backend.js";
|
||||
import { GatewayChatClient } from "./gateway-chat.js";
|
||||
import { editorTheme, theme } from "./theme/theme.js";
|
||||
import type { TuiBackend } from "./tui-backend.js";
|
||||
@@ -665,15 +664,19 @@ export async function runTui(opts: RunTuiOptions): Promise<TuiResult> {
|
||||
localBtwRunIds.clear();
|
||||
};
|
||||
|
||||
const client: TuiBackend = opts.backend
|
||||
? opts.backend
|
||||
: opts.local
|
||||
? new EmbeddedTuiBackend()
|
||||
: await GatewayChatClient.connect({
|
||||
url: opts.url,
|
||||
token: opts.token,
|
||||
password: opts.password,
|
||||
});
|
||||
let client: TuiBackend;
|
||||
if (opts.backend) {
|
||||
client = opts.backend;
|
||||
} else if (opts.local) {
|
||||
const { EmbeddedTuiBackend } = await import("./embedded-backend.js");
|
||||
client = new EmbeddedTuiBackend();
|
||||
} else {
|
||||
client = await GatewayChatClient.connect({
|
||||
url: opts.url,
|
||||
token: opts.token,
|
||||
password: opts.password,
|
||||
});
|
||||
}
|
||||
const previousConsoleSubsystemFilter = isLocalMode
|
||||
? loggingState.consoleSubsystemFilter
|
||||
? [...loggingState.consoleSubsystemFilter]
|
||||
|
||||
Reference in New Issue
Block a user