mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
perf(test): trim active memory and qa lab hotspots
This commit is contained in:
@@ -3,7 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import plugin from "./index.js";
|
||||
import plugin, { __testing } from "./index.js";
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
@@ -119,10 +119,12 @@ describe("active-memory plugin", () => {
|
||||
runEmbeddedPiAgent.mockResolvedValue({
|
||||
payloads: [{ text: "- lemon pepper wings\n- blue cheese" }],
|
||||
});
|
||||
__testing.resetActiveRecallCacheForTests();
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
if (stateDir) {
|
||||
await fs.rm(stateDir, { recursive: true, force: true });
|
||||
@@ -1022,9 +1024,10 @@ describe("active-memory plugin", () => {
|
||||
});
|
||||
|
||||
it("does not cache timeout results", async () => {
|
||||
__testing.setMinimumTimeoutMsForTests(1);
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
timeoutMs: 250,
|
||||
timeoutMs: 1,
|
||||
logging: true,
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
@@ -1032,12 +1035,15 @@ describe("active-memory plugin", () => {
|
||||
runEmbeddedPiAgent.mockImplementation(async (params: { abortSignal?: AbortSignal }) => {
|
||||
lastAbortSignal = params.abortSignal;
|
||||
return await new Promise((resolve, reject) => {
|
||||
const abortHandler = () => reject(new Error("aborted"));
|
||||
params.abortSignal?.addEventListener("abort", abortHandler, { once: true });
|
||||
setTimeout(() => {
|
||||
const timer = setTimeout(() => {
|
||||
params.abortSignal?.removeEventListener("abort", abortHandler);
|
||||
resolve({ payloads: [] });
|
||||
}, 2_000);
|
||||
const abortHandler = () => {
|
||||
clearTimeout(timer);
|
||||
reject(new Error("aborted"));
|
||||
};
|
||||
params.abortSignal?.addEventListener("abort", abortHandler, { once: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1102,14 +1108,15 @@ describe("active-memory plugin", () => {
|
||||
});
|
||||
|
||||
it("ignores late subagent payloads once the active-memory timeout signal has fired", async () => {
|
||||
__testing.setMinimumTimeoutMsForTests(1);
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
timeoutMs: 250,
|
||||
timeoutMs: 1,
|
||||
logging: true,
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
runEmbeddedPiAgent.mockImplementationOnce(async (params: { timeoutMs?: number }) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, (params.timeoutMs ?? 0) + 25));
|
||||
await new Promise((resolve) => setTimeout(resolve, (params.timeoutMs ?? 0) + 1));
|
||||
return {
|
||||
payloads: [{ text: "late timeout payload that should never become memory context" }],
|
||||
meta: { aborted: true },
|
||||
@@ -1944,45 +1951,42 @@ describe("active-memory plugin", () => {
|
||||
expect(lines.some((line) => line.includes("\r"))).toBe(false);
|
||||
});
|
||||
|
||||
it("caps the active-memory cache size and evicts the oldest entries", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
logging: true,
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
it("caps the active-memory cache size and evicts the oldest entries", () => {
|
||||
const sessionKey = "agent:main:cache-cap";
|
||||
for (let index = 0; index <= 1000; index += 1) {
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: `cache pressure prompt ${index}`, messages: [] },
|
||||
{
|
||||
__testing.setCachedResult(
|
||||
__testing.buildCacheKey({
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:cache-cap",
|
||||
messageProvider: "webchat",
|
||||
sessionKey,
|
||||
query: `cache pressure prompt ${index}`,
|
||||
}),
|
||||
{
|
||||
status: "ok",
|
||||
elapsedMs: 1,
|
||||
rawReply: `memory ${index}`,
|
||||
summary: `memory ${index}`,
|
||||
},
|
||||
15_000,
|
||||
);
|
||||
}
|
||||
|
||||
const callsBeforeReplay = runEmbeddedPiAgent.mock.calls.length;
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "cache pressure prompt 0", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:cache-cap",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(runEmbeddedPiAgent.mock.calls.length).toBe(callsBeforeReplay + 1);
|
||||
const infoLines = vi
|
||||
.mocked(api.logger.info)
|
||||
.mock.calls.map((call: unknown[]) => String(call[0]));
|
||||
expect(
|
||||
infoLines.some(
|
||||
(line: string) => line.includes("cached status=ok") && line.includes("prompt 0"),
|
||||
__testing.getCachedResult(
|
||||
__testing.buildCacheKey({
|
||||
agentId: "main",
|
||||
sessionKey,
|
||||
query: "cache pressure prompt 0",
|
||||
}),
|
||||
),
|
||||
).toBe(false);
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
__testing.getCachedResult(
|
||||
__testing.buildCacheKey({
|
||||
agentId: "main",
|
||||
sessionKey,
|
||||
query: "cache pressure prompt 1",
|
||||
}),
|
||||
),
|
||||
).toMatchObject({ status: "ok", summary: "memory 1" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,8 @@ const DEFAULT_RECENT_USER_CHARS = 220;
|
||||
const DEFAULT_RECENT_ASSISTANT_CHARS = 180;
|
||||
const DEFAULT_CACHE_TTL_MS = 15_000;
|
||||
const DEFAULT_MAX_CACHE_ENTRIES = 1000;
|
||||
const CACHE_SWEEP_INTERVAL_MS = 1000;
|
||||
const DEFAULT_MIN_TIMEOUT_MS = 250;
|
||||
const DEFAULT_QUERY_MODE = "recent" as const;
|
||||
const DEFAULT_QMD_SEARCH_MODE = "search" as const;
|
||||
const DEFAULT_TRANSCRIPT_DIR = "active-memory";
|
||||
@@ -173,6 +175,8 @@ type ActiveMemoryToggleStore = {
|
||||
type AsyncLock = <T>(task: () => Promise<T>) => Promise<T>;
|
||||
|
||||
const toggleStoreLocks = new Map<string, AsyncLock>();
|
||||
let lastActiveRecallCacheSweepAt = 0;
|
||||
let minimumTimeoutMs = DEFAULT_MIN_TIMEOUT_MS;
|
||||
|
||||
function createAsyncLock(): AsyncLock {
|
||||
let lock: Promise<void> = Promise.resolve();
|
||||
@@ -632,7 +636,7 @@ function normalizePluginConfig(pluginConfig: unknown): ResolvedActiveRecallPlugi
|
||||
timeoutMs: clampInt(
|
||||
parseOptionalPositiveInt(raw.timeoutMs, DEFAULT_TIMEOUT_MS),
|
||||
DEFAULT_TIMEOUT_MS,
|
||||
250,
|
||||
minimumTimeoutMs,
|
||||
120_000,
|
||||
),
|
||||
queryMode:
|
||||
@@ -944,12 +948,19 @@ function getCachedResult(cacheKey: string): ActiveRecallResult | undefined {
|
||||
}
|
||||
|
||||
function setCachedResult(cacheKey: string, result: ActiveRecallResult, ttlMs: number): void {
|
||||
sweepExpiredCacheEntries();
|
||||
const now = Date.now();
|
||||
if (
|
||||
activeRecallCache.size >= DEFAULT_MAX_CACHE_ENTRIES ||
|
||||
now - lastActiveRecallCacheSweepAt >= CACHE_SWEEP_INTERVAL_MS
|
||||
) {
|
||||
sweepExpiredCacheEntries(now);
|
||||
lastActiveRecallCacheSweepAt = now;
|
||||
}
|
||||
if (activeRecallCache.has(cacheKey)) {
|
||||
activeRecallCache.delete(cacheKey);
|
||||
}
|
||||
activeRecallCache.set(cacheKey, {
|
||||
expiresAt: Date.now() + ttlMs,
|
||||
expiresAt: now + ttlMs,
|
||||
result,
|
||||
});
|
||||
while (activeRecallCache.size > DEFAULT_MAX_CACHE_ENTRIES) {
|
||||
@@ -2027,3 +2038,17 @@ export default definePluginEntry({
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const __testing = {
|
||||
buildCacheKey,
|
||||
getCachedResult,
|
||||
resetActiveRecallCacheForTests() {
|
||||
activeRecallCache.clear();
|
||||
lastActiveRecallCacheSweepAt = 0;
|
||||
minimumTimeoutMs = DEFAULT_MIN_TIMEOUT_MS;
|
||||
},
|
||||
setMinimumTimeoutMsForTests(value: number) {
|
||||
minimumTimeoutMs = value;
|
||||
},
|
||||
setCachedResult,
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ async function fetchWithRetry(input: string, init?: RequestInit, attempts = 3) {
|
||||
if (attempt === attempts) {
|
||||
throw error;
|
||||
}
|
||||
await sleep(50);
|
||||
await sleep(10);
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
@@ -61,7 +61,7 @@ async function waitForRunnerCatalog(baseUrl: string, timeoutMs = 5_000) {
|
||||
if (bootstrap.runnerCatalog.status !== "loading") {
|
||||
return bootstrap.runnerCatalog;
|
||||
}
|
||||
await sleep(50);
|
||||
await sleep(10);
|
||||
}
|
||||
throw new Error("runner catalog stayed loading");
|
||||
}
|
||||
@@ -75,7 +75,7 @@ async function waitForFile(filePath: string, timeoutMs = 5_000) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
await sleep(50);
|
||||
await sleep(10);
|
||||
}
|
||||
}
|
||||
throw new Error(`file did not appear: ${filePath}`);
|
||||
@@ -417,7 +417,7 @@ describe("qa-lab server", () => {
|
||||
await lab.stop();
|
||||
});
|
||||
|
||||
await sleep(150);
|
||||
await sleep(25);
|
||||
await expect(readFile(markerPath, "utf8")).rejects.toThrow();
|
||||
|
||||
const bootstrapResponse = await fetchWithRetry(`${lab.baseUrl}/api/bootstrap`);
|
||||
@@ -501,7 +501,6 @@ describe("qa-lab server", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
const snapshot = (await (await fetchWithRetry(`${lab.baseUrl}/api/state`)).json()) as {
|
||||
messages: Array<{ direction: string }>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user