mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-29 21:28:42 +00:00
perf(gateway): cache stable plugin index fingerprints
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
@@ -239,8 +240,9 @@ const installRecordsCache = new Map<string, InstallRecordsCacheEntry>();
|
||||
|
||||
function readFileSignature(filePath: string): string {
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
return `${stat.mtimeMs}:${stat.size}`;
|
||||
const stat = fs.statSync(filePath, { bigint: true });
|
||||
const hash = crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("base64url");
|
||||
return `${stat.mtimeNs}:${stat.ctimeNs}:${stat.size}:${hash}`;
|
||||
} catch {
|
||||
return "missing";
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
writePersistedInstalledPluginIndex,
|
||||
} from "./installed-plugin-index-store.js";
|
||||
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
|
||||
import {
|
||||
loadPluginManifestRegistryForInstalledIndex,
|
||||
resolveInstalledManifestRegistryIndexFingerprint,
|
||||
} from "./manifest-registry-installed.js";
|
||||
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
@@ -70,7 +73,106 @@ function createIndex(rootDir: string): InstalledPluginIndex {
|
||||
};
|
||||
}
|
||||
|
||||
function fileSignature(filePath: string) {
|
||||
const stat = fs.statSync(filePath);
|
||||
return {
|
||||
size: stat.size,
|
||||
mtimeMs: stat.mtimeMs,
|
||||
ctimeMs: stat.ctimeMs,
|
||||
};
|
||||
}
|
||||
|
||||
function createIndexWithFileSignatures(rootDir: string): InstalledPluginIndex {
|
||||
const index = createIndex(rootDir);
|
||||
return {
|
||||
...index,
|
||||
plugins: index.plugins.map((record) => ({
|
||||
...record,
|
||||
manifestFile: fileSignature(record.manifestPath),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function deepFreeze<T>(value: T, seen = new WeakSet<object>()): T {
|
||||
if (!value || typeof value !== "object") {
|
||||
return value;
|
||||
}
|
||||
const object = value as object;
|
||||
if (seen.has(object)) {
|
||||
return value;
|
||||
}
|
||||
seen.add(object);
|
||||
for (const child of Object.values(value)) {
|
||||
deepFreeze(child, seen);
|
||||
}
|
||||
return Object.freeze(value);
|
||||
}
|
||||
|
||||
describe("loadPluginManifestRegistryForInstalledIndex", () => {
|
||||
it("reuses frozen installed-index fingerprints when file signatures are persisted", () => {
|
||||
const rootDir = makeTempDir();
|
||||
writePlugin(rootDir, "installed", "installed-");
|
||||
const index = deepFreeze(createIndexWithFileSignatures(rootDir));
|
||||
const first = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
const manifestPath = path.join(rootDir, "openclaw.plugin.json");
|
||||
const nextMtime = new Date(Date.now() + 5000);
|
||||
fs.utimesSync(manifestPath, nextMtime, nextMtime);
|
||||
const second = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
|
||||
expect(second).toBe(first);
|
||||
});
|
||||
|
||||
it("recomputes installed-index fingerprints for mutable index objects", () => {
|
||||
const rootDir = makeTempDir();
|
||||
writePlugin(rootDir, "installed", "installed-");
|
||||
const index = createIndexWithFileSignatures(rootDir);
|
||||
const first = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
const record = index.plugins[0];
|
||||
if (!record) {
|
||||
throw new Error("expected index record");
|
||||
}
|
||||
record.manifestHash = "changed";
|
||||
const second = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
|
||||
expect(second).not.toBe(first);
|
||||
});
|
||||
|
||||
it("does not cache shallow-frozen installed-index fingerprints with mutable nested records", () => {
|
||||
const rootDir = makeTempDir();
|
||||
writePlugin(rootDir, "installed", "installed-");
|
||||
const index = createIndexWithFileSignatures(rootDir);
|
||||
const record = index.plugins[0];
|
||||
if (!record) {
|
||||
throw new Error("expected index record");
|
||||
}
|
||||
Object.freeze(index.installRecords);
|
||||
Object.freeze(index.diagnostics);
|
||||
Object.freeze(record);
|
||||
Object.freeze(index.plugins);
|
||||
Object.freeze(index);
|
||||
const first = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
|
||||
const agentHarnesses = record.startup.agentHarnesses as string[];
|
||||
agentHarnesses.push("changed");
|
||||
const second = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
|
||||
expect(second).not.toBe(first);
|
||||
});
|
||||
|
||||
it("does not cache frozen installed-index fingerprints that depend on live file state", () => {
|
||||
const rootDir = makeTempDir();
|
||||
writePlugin(rootDir, "installed", "installed-");
|
||||
const index = deepFreeze(createIndex(rootDir));
|
||||
const first = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
|
||||
const manifestPath = path.join(rootDir, "openclaw.plugin.json");
|
||||
const nextMtime = new Date(Date.now() + 5000);
|
||||
fs.utimesSync(manifestPath, nextMtime, nextMtime);
|
||||
const second = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
|
||||
expect(second).not.toBe(first);
|
||||
});
|
||||
|
||||
it("reconstructs installed-index manifest registries when manifest files change", () => {
|
||||
const rootDir = makeTempDir();
|
||||
const manifestPath = path.join(rootDir, "openclaw.plugin.json");
|
||||
|
||||
@@ -7,6 +7,7 @@ import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { normalizeOptionalTrimmedStringList } from "../shared/string-normalization.js";
|
||||
import type { PluginCandidate } from "./discovery.js";
|
||||
import { hashJson } from "./installed-plugin-index-hash.js";
|
||||
import type { InstalledPluginFileSignature } from "./installed-plugin-index-hash.js";
|
||||
import type { InstalledPluginIndex, InstalledPluginIndexRecord } from "./installed-plugin-index.js";
|
||||
import { extractPluginInstallRecordsFromInstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js";
|
||||
@@ -25,6 +26,37 @@ import {
|
||||
type PluginDependencySpecMap,
|
||||
} from "./status-dependencies.js";
|
||||
|
||||
const installedManifestRegistryIndexFingerprintCache = new WeakMap<InstalledPluginIndex, string>();
|
||||
|
||||
function isDeepFrozenJsonLike(value: unknown, seen = new WeakSet<object>()): boolean {
|
||||
if (!value || typeof value !== "object") {
|
||||
return true;
|
||||
}
|
||||
const object = value as object;
|
||||
if (seen.has(object)) {
|
||||
return true;
|
||||
}
|
||||
if (!Object.isFrozen(object)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(object);
|
||||
return Object.values(value).every((entry) => isDeepFrozenJsonLike(entry, seen));
|
||||
}
|
||||
|
||||
function hasPersistedFileSignatures(index: InstalledPluginIndex): boolean {
|
||||
return index.plugins.every(
|
||||
(record) =>
|
||||
record.manifestFile !== undefined &&
|
||||
(record.packageJson === undefined || record.packageJson.fileSignature !== undefined),
|
||||
);
|
||||
}
|
||||
|
||||
function isInstalledManifestRegistryIndexFingerprintCacheable(
|
||||
index: InstalledPluginIndex,
|
||||
): boolean {
|
||||
return hasPersistedFileSignatures(index) && isDeepFrozenJsonLike(index);
|
||||
}
|
||||
|
||||
function isRelativePathInsideOrEqual(relativePath: string): boolean {
|
||||
return (
|
||||
relativePath === "" ||
|
||||
@@ -61,12 +93,19 @@ function safeFileSignature(filePath: string | undefined): string | undefined {
|
||||
}
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
return `${filePath}:${stat.size}:${stat.mtimeMs}`;
|
||||
return formatFileSignature(filePath, stat);
|
||||
} catch {
|
||||
return `${filePath}:missing`;
|
||||
}
|
||||
}
|
||||
|
||||
function formatFileSignature(
|
||||
filePath: string,
|
||||
signature: Pick<InstalledPluginFileSignature, "size" | "mtimeMs">,
|
||||
): string {
|
||||
return `${filePath}:${signature.size}:${signature.mtimeMs}`;
|
||||
}
|
||||
|
||||
function buildInstalledManifestRegistryIndexKey(index: InstalledPluginIndex) {
|
||||
const realpathCache = new Map<string, string>();
|
||||
return {
|
||||
@@ -78,9 +117,12 @@ function buildInstalledManifestRegistryIndexKey(index: InstalledPluginIndex) {
|
||||
installRecords: index.installRecords,
|
||||
diagnostics: index.diagnostics,
|
||||
plugins: index.plugins.map((record) => {
|
||||
const packageJsonFile =
|
||||
record.packageJson?.fileSignature ??
|
||||
safeFileSignature(resolvePackageJsonPath(record, realpathCache));
|
||||
const packageJsonPath = resolvePackageJsonPath(record, realpathCache);
|
||||
const packageJsonFile = record.packageJson?.fileSignature
|
||||
? packageJsonPath
|
||||
? formatFileSignature(packageJsonPath, record.packageJson.fileSignature)
|
||||
: undefined
|
||||
: safeFileSignature(packageJsonPath);
|
||||
return {
|
||||
pluginId: record.pluginId,
|
||||
packageName: record.packageName,
|
||||
@@ -91,7 +133,9 @@ function buildInstalledManifestRegistryIndexKey(index: InstalledPluginIndex) {
|
||||
packageChannel: record.packageChannel,
|
||||
manifestPath: record.manifestPath,
|
||||
manifestHash: record.manifestHash,
|
||||
manifestFile: safeFileSignature(record.manifestPath),
|
||||
manifestFile: record.manifestFile
|
||||
? formatFileSignature(record.manifestPath, record.manifestFile)
|
||||
: safeFileSignature(record.manifestPath),
|
||||
format: record.format,
|
||||
bundleFormat: record.bundleFormat,
|
||||
source: record.source,
|
||||
@@ -116,7 +160,15 @@ function buildInstalledManifestRegistryIndexKey(index: InstalledPluginIndex) {
|
||||
export function resolveInstalledManifestRegistryIndexFingerprint(
|
||||
index: InstalledPluginIndex,
|
||||
): string {
|
||||
return hashJson(buildInstalledManifestRegistryIndexKey(index));
|
||||
const cached = installedManifestRegistryIndexFingerprintCache.get(index);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const fingerprint = hashJson(buildInstalledManifestRegistryIndexKey(index));
|
||||
if (isInstalledManifestRegistryIndexFingerprintCacheable(index)) {
|
||||
installedManifestRegistryIndexFingerprintCache.set(index, fingerprint);
|
||||
}
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
function resolveInstalledPluginRootDir(record: InstalledPluginIndexRecord): string {
|
||||
|
||||
Reference in New Issue
Block a user