mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
perf: cache model resolution to avoid repeated plugin-provider loads
On ARM64 devices (e.g. Raspberry Pi 4), resolvePluginProviders takes ~20s on first call. Three bugs cause this cost to be paid repeatedly: 1. ensureOpenClawModelsJson readyCache fingerprint includes models.json mtime. After a write, the stored fingerprint (pre-write mtime) never matches again, forcing every caller to re-run planOpenClawModelsJson. 2. readyCache has one entry per file path. Agents with different configs (e.g. main agent vs active-memory subagent) overwrite each other's entry, so neither benefits from caching. 3. resolveExplicitModelWithRegistry calls shouldSuppressBuiltInModel → resolveProviderPluginsForCatalogHooks on every agent run. The internal cache key includes the full config, so callers with slightly different configs each pay the full provider-load cost. Fixes: - Remove modelsFileMtimeMs from fingerprint (bug 1) - Add noopCache to MODELS_JSON_STATE keyed by (path, mtime) — a noop result is config-agnostic, so any caller can reuse it (bug 2) - Cache resolveExplicitModelWithRegistry by (provider, modelId, agentDir), stable for the lifetime of a gateway session (bug 3) Measured on Raspberry Pi 4 (ARM64): active-memory subagent preprocessing: 66-75s → ~3s (warm) active-memory total elapsed: ~96s → ~14s (warm) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
Peter Steinberger
parent
a0c850d188
commit
e9be25b554
@@ -6,6 +6,14 @@ type ModelsJsonState = {
|
||||
string,
|
||||
Promise<{ fingerprint: string; result: { agentDir: string; wrote: boolean } }>
|
||||
>;
|
||||
/**
|
||||
* Cross-config noop cache: when planOpenClawModelsJson returns "noop" (no write
|
||||
* needed), the result is valid for all callers regardless of config differences,
|
||||
* as long as models.json has not changed (same mtime). This avoids redundant
|
||||
* planOpenClawModelsJson runs when multiple agents with slightly different configs
|
||||
* (e.g. main agent vs subagent) call ensureOpenClawModelsJson concurrently.
|
||||
*/
|
||||
noopCache: Map<string, { mtime: number | null; result: { agentDir: string; wrote: boolean } }>;
|
||||
};
|
||||
|
||||
export const MODELS_JSON_STATE = (() => {
|
||||
@@ -19,12 +27,18 @@ export const MODELS_JSON_STATE = (() => {
|
||||
string,
|
||||
Promise<{ fingerprint: string; result: { agentDir: string; wrote: boolean } }>
|
||||
>(),
|
||||
noopCache: new Map(),
|
||||
};
|
||||
}
|
||||
// Schema migration: add noopCache if missing (e.g. after in-process restart with old state)
|
||||
if (!globalState[MODELS_JSON_STATE_KEY].noopCache) {
|
||||
globalState[MODELS_JSON_STATE_KEY].noopCache = new Map();
|
||||
}
|
||||
return globalState[MODELS_JSON_STATE_KEY];
|
||||
})();
|
||||
|
||||
export function resetModelsJsonReadyCacheForTest(): void {
|
||||
MODELS_JSON_STATE.writeLocks.clear();
|
||||
MODELS_JSON_STATE.readyCache.clear();
|
||||
MODELS_JSON_STATE.noopCache.clear();
|
||||
}
|
||||
|
||||
@@ -71,6 +71,10 @@ async function buildModelsJsonFingerprint(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function modelsJsonReadyCacheKey(targetPath: string, fingerprint: string): string {
|
||||
return `${targetPath}\0${fingerprint}`;
|
||||
}
|
||||
|
||||
async function readExistingModelsFile(pathname: string): Promise<{
|
||||
raw: string;
|
||||
parsed: unknown;
|
||||
@@ -172,7 +176,7 @@ export async function ensureOpenClawModelsJson(
|
||||
getCurrentPluginMetadataSnapshot({
|
||||
config: cfg,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
});
|
||||
});
|
||||
const agentDir = agentDirOverride?.trim() ? agentDirOverride.trim() : resolveOpenClawAgentDir();
|
||||
const targetPath = path.join(agentDir, "models.json");
|
||||
const fingerprint = await buildModelsJsonFingerprint({
|
||||
@@ -188,13 +192,12 @@ export async function ensureOpenClawModelsJson(
|
||||
? { providerDiscoveryTimeoutMs: options.providerDiscoveryTimeoutMs }
|
||||
: {}),
|
||||
});
|
||||
const cached = MODELS_JSON_STATE.readyCache.get(targetPath);
|
||||
const cacheKey = modelsJsonReadyCacheKey(targetPath, fingerprint);
|
||||
const cached = MODELS_JSON_STATE.readyCache.get(cacheKey);
|
||||
if (cached) {
|
||||
const settled = await cached;
|
||||
if (settled.fingerprint === fingerprint) {
|
||||
await ensureModelsFileModeForModelsJson(targetPath);
|
||||
return settled.result;
|
||||
}
|
||||
await ensureModelsFileModeForModelsJson(targetPath);
|
||||
return settled.result;
|
||||
}
|
||||
|
||||
const pending = withModelsJsonWriteLock(targetPath, async () => {
|
||||
@@ -233,13 +236,31 @@ export async function ensureOpenClawModelsJson(
|
||||
await ensureModelsFileModeForModelsJson(targetPath);
|
||||
return { fingerprint, result: { agentDir, wrote: true } };
|
||||
});
|
||||
MODELS_JSON_STATE.readyCache.set(targetPath, pending);
|
||||
MODELS_JSON_STATE.readyCache.set(cacheKey, pending);
|
||||
try {
|
||||
const settled = await pending;
|
||||
const refreshedFingerprint = await buildModelsJsonFingerprint({
|
||||
config: cfg,
|
||||
sourceConfigForSecrets: resolved.sourceConfigForSecrets,
|
||||
agentDir,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
...(pluginMetadataSnapshot ? { pluginMetadataSnapshot } : {}),
|
||||
...(options.providerDiscoveryProviderIds
|
||||
? { providerDiscoveryProviderIds: options.providerDiscoveryProviderIds }
|
||||
: {}),
|
||||
...(options.providerDiscoveryTimeoutMs !== undefined
|
||||
? { providerDiscoveryTimeoutMs: options.providerDiscoveryTimeoutMs }
|
||||
: {}),
|
||||
});
|
||||
const refreshedCacheKey = modelsJsonReadyCacheKey(targetPath, refreshedFingerprint);
|
||||
if (refreshedCacheKey !== cacheKey) {
|
||||
MODELS_JSON_STATE.readyCache.delete(cacheKey);
|
||||
MODELS_JSON_STATE.readyCache.set(refreshedCacheKey, Promise.resolve(settled));
|
||||
}
|
||||
return settled.result;
|
||||
} catch (error) {
|
||||
if (MODELS_JSON_STATE.readyCache.get(targetPath) === pending) {
|
||||
MODELS_JSON_STATE.readyCache.delete(targetPath);
|
||||
if (MODELS_JSON_STATE.readyCache.get(cacheKey) === pending) {
|
||||
MODELS_JSON_STATE.readyCache.delete(cacheKey);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user