perf(test): trim active memory and qa lab hotspots

This commit is contained in:
Peter Steinberger
2026-04-20 15:03:38 +01:00
parent f6360da116
commit beff874340
3 changed files with 75 additions and 47 deletions

View File

@@ -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" });
});
});

View File

@@ -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,
};

View File

@@ -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 }>;
};