mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(gateway): bound cloud workspace hash memo (#124373)
This commit is contained in:
committed by
GitHub
parent
f38a2f51b3
commit
e6d2b1bbac
@@ -96,6 +96,7 @@ function createAcceptedWorkspacePublisherFactory(
|
||||
contentHashCount: 0,
|
||||
contentHashDurationMs: 0,
|
||||
memoHitCount: 0,
|
||||
memoTruncatedCount: 0,
|
||||
totalDurationMs: 0,
|
||||
},
|
||||
})}\n`,
|
||||
|
||||
@@ -145,17 +145,20 @@ describe("workspace hash memo", () => {
|
||||
contentHashCount: 7,
|
||||
contentHashDurationMs: 11,
|
||||
memoHitCount: 13,
|
||||
memoTruncatedCount: 17,
|
||||
totalDurationMs: 17,
|
||||
});
|
||||
recordRemoteWorkspaceHashMetrics(aggregate, {
|
||||
contentHashCount: 19,
|
||||
contentHashDurationMs: 23,
|
||||
memoHitCount: 29,
|
||||
memoTruncatedCount: 31,
|
||||
totalDurationMs: 31,
|
||||
});
|
||||
expect(aggregate).toMatchObject({
|
||||
remoteContentHashCount: 26,
|
||||
remoteMemoHitCount: 42,
|
||||
remoteMemoTruncatedCount: 48,
|
||||
remoteHashDurationMs: 34,
|
||||
remoteManifestDurationMs: 48,
|
||||
});
|
||||
@@ -179,6 +182,7 @@ describe("workspace hash memo", () => {
|
||||
contentHashCount: MAX_RECONCILIATION_ENTRIES,
|
||||
contentHashDurationMs: Number.MAX_SAFE_INTEGER,
|
||||
memoHitCount: MAX_RECONCILIATION_ENTRIES,
|
||||
memoTruncatedCount: MAX_RECONCILIATION_ENTRIES,
|
||||
totalDurationMs: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
})}\n`,
|
||||
@@ -233,4 +237,58 @@ describe("workspace hash memo", () => {
|
||||
expect(nextReconcile.manifestRef).toBe(replaced.manifestRef);
|
||||
expect(nextReconcile.metrics).toMatchObject({ contentHashCount: 1, memoHitCount: 0 });
|
||||
});
|
||||
|
||||
it("bounds the remote memo to the largest files and reports truncation", async () => {
|
||||
const root = tempDirs.make("openclaw-remote-manifest-memo-cap-");
|
||||
const home = path.join(root, "home");
|
||||
const workspace = path.join(root, "workspace");
|
||||
await Promise.all([fs.mkdir(home), fs.mkdir(workspace)]);
|
||||
await Promise.all([
|
||||
fs.writeFile(path.join(workspace, "small.txt"), "1"),
|
||||
fs.writeFile(path.join(workspace, "medium.txt"), "22"),
|
||||
fs.writeFile(path.join(workspace, "large.txt"), "333"),
|
||||
]);
|
||||
const limitDeclaration = `const MAX_RECONCILIATION_ENTRIES = ${MAX_RECONCILIATION_ENTRIES};`;
|
||||
const limitedScript = REMOTE_WORKSPACE_MANIFEST_JS.replace(
|
||||
limitDeclaration,
|
||||
"const MAX_RECONCILIATION_ENTRIES = 2;",
|
||||
);
|
||||
expect(limitedScript).not.toBe(REMOTE_WORKSPACE_MANIFEST_JS);
|
||||
const env = { ...process.env, HOME: home };
|
||||
type MemoResponse = {
|
||||
manifestRef: string;
|
||||
memo: [string, string][];
|
||||
metrics: { contentHashCount: number; memoHitCount: number; memoTruncatedCount: number };
|
||||
};
|
||||
const capture = async (memo: [string, string][]): Promise<MemoResponse> => {
|
||||
const result = await runCommandWithTimeout(
|
||||
[process.execPath, "-e", limitedScript, workspace, "", "memo-v1"],
|
||||
{ timeoutMs: 10_000, baseEnv: env, input: JSON.stringify(memo) },
|
||||
);
|
||||
expect(result).toMatchObject({ code: 0, stderr: "" });
|
||||
return JSON.parse(result.stdout) as MemoResponse;
|
||||
};
|
||||
|
||||
const first = await capture([]);
|
||||
expect(first.memo).toHaveLength(2);
|
||||
expect(
|
||||
first.memo
|
||||
.map(([identity]) => Number(identity.split(":")[3]))
|
||||
.toSorted((left, right) => left - right),
|
||||
).toEqual([2, 3]);
|
||||
expect(first.metrics).toMatchObject({
|
||||
contentHashCount: 3,
|
||||
memoHitCount: 0,
|
||||
memoTruncatedCount: 1,
|
||||
});
|
||||
|
||||
const unchanged = await capture(first.memo);
|
||||
expect(unchanged.manifestRef).toBe(first.manifestRef);
|
||||
expect(unchanged.memo).toEqual(first.memo);
|
||||
expect(unchanged.metrics).toMatchObject({
|
||||
contentHashCount: 1,
|
||||
memoHitCount: 2,
|
||||
memoTruncatedCount: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,13 +13,17 @@ export type WorkspaceReconcileMetrics = {
|
||||
remoteManifestCalls: number;
|
||||
remoteContentHashCount: number;
|
||||
remoteMemoHitCount: number;
|
||||
remoteMemoTruncatedCount: number;
|
||||
remoteHashDurationMs: number;
|
||||
remoteManifestDurationMs: number;
|
||||
remoteManifestWallDurationMs: number;
|
||||
localReconciliationDurationMs: number;
|
||||
};
|
||||
|
||||
type RemoteWorkspaceHashMetrics = WorkspaceHashMetrics & { totalDurationMs: number };
|
||||
type RemoteWorkspaceHashMetrics = WorkspaceHashMetrics & {
|
||||
memoTruncatedCount: number;
|
||||
totalDurationMs: number;
|
||||
};
|
||||
|
||||
export const MAX_WORKSPACE_HASH_MEMO_BYTES = 8 * 1024 * 1024;
|
||||
|
||||
@@ -40,6 +44,7 @@ export function createWorkspaceReconcileMetrics(): WorkspaceReconcileMetrics {
|
||||
remoteManifestCalls: 0,
|
||||
remoteContentHashCount: 0,
|
||||
remoteMemoHitCount: 0,
|
||||
remoteMemoTruncatedCount: 0,
|
||||
remoteHashDurationMs: 0,
|
||||
remoteManifestDurationMs: 0,
|
||||
remoteManifestWallDurationMs: 0,
|
||||
@@ -87,6 +92,7 @@ export function recordRemoteWorkspaceHashMetrics(
|
||||
): void {
|
||||
aggregate.remoteContentHashCount += metrics.contentHashCount;
|
||||
aggregate.remoteMemoHitCount += metrics.memoHitCount;
|
||||
aggregate.remoteMemoTruncatedCount += metrics.memoTruncatedCount;
|
||||
aggregate.remoteHashDurationMs += metrics.contentHashDurationMs;
|
||||
aggregate.remoteManifestDurationMs += metrics.totalDurationMs;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ const remoteWorkspaceManifestEnvelopeSchema = z
|
||||
contentHashCount: z.number().finite().nonnegative(),
|
||||
contentHashDurationMs: z.number().finite().nonnegative(),
|
||||
memoHitCount: z.number().finite().nonnegative(),
|
||||
memoTruncatedCount: z.number().finite().nonnegative(),
|
||||
totalDurationMs: z.number().finite().nonnegative(),
|
||||
})
|
||||
.strict(),
|
||||
|
||||
@@ -98,11 +98,19 @@ const entriesByPath = new Map();
|
||||
let inventoryPathBytes = 0;
|
||||
let eligibleBytes = 0;
|
||||
const usedHashMemo = new Map();
|
||||
const metrics = { contentHashCount: 0, contentHashDurationMs: 0, memoHitCount: 0 };
|
||||
const metrics = {
|
||||
contentHashCount: 0,
|
||||
contentHashDurationMs: 0,
|
||||
memoHitCount: 0,
|
||||
memoTruncatedCount: 0,
|
||||
};
|
||||
const startedAt = performance.now();
|
||||
function fail(message) {
|
||||
throw new Error(message);
|
||||
}
|
||||
function compareHashMemoIdentity(left, right) {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
function readHashMemo() {
|
||||
if (!memoMode) return new Map();
|
||||
const raw = fs.readFileSync(0, "utf8");
|
||||
@@ -373,7 +381,7 @@ async function hashFiles(entries) {
|
||||
entry.mode = Number(after.mode & 0o777n);
|
||||
entry.size = Number(after.size);
|
||||
entry.sha256 = sha256;
|
||||
usedHashMemo.set(identity, sha256);
|
||||
usedHashMemo.set(identity, { sha256, size: Number(after.size) });
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
@@ -440,14 +448,23 @@ async function main() {
|
||||
const manifest = serializeManifest(baseCommit, entries);
|
||||
const digest = publishManifest(manifestRoot, manifest);
|
||||
const manifestRef = "sha256:" + digest;
|
||||
const measured = { ...metrics, totalDurationMs: performance.now() - startedAt };
|
||||
if (memoMode) {
|
||||
// Largest files preserve the most expensive hashes. Identity tie-breaking and
|
||||
// final ordering keep the bounded cache deterministic across captures.
|
||||
const memo = [...usedHashMemo]
|
||||
.sort(
|
||||
(left, right) =>
|
||||
right[1].size - left[1].size || compareHashMemoIdentity(left[0], right[0]),
|
||||
)
|
||||
.slice(0, MAX_RECONCILIATION_ENTRIES)
|
||||
.map(([identity, value]) => [identity, value.sha256])
|
||||
.sort((left, right) => compareHashMemoIdentity(left[0], right[0]));
|
||||
metrics.memoTruncatedCount = usedHashMemo.size - memo.length;
|
||||
const measured = { ...metrics, totalDurationMs: performance.now() - startedAt };
|
||||
process.stdout.write(JSON.stringify({
|
||||
version: 1,
|
||||
manifestRef,
|
||||
memo: [...usedHashMemo].sort((left, right) =>
|
||||
left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0,
|
||||
),
|
||||
memo,
|
||||
metrics: measured,
|
||||
}) + "\n");
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user