Files
ST-Bionic-Memory-Ecology/tests/perf/persist-load-bench.mjs
2026-04-22 20:09:48 +08:00

420 lines
14 KiB
JavaScript

import { performance } from "node:perf_hooks";
import {
buildGraphFromSnapshot,
buildPersistDelta,
buildSnapshotFromGraph,
} from "../../sync/bme-db.js";
import {
BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY,
OpfsGraphStore,
} from "../../sync/bme-opfs-store.js";
import { createMemoryOpfsRoot } from "../helpers/memory-opfs.mjs";
const RUNS = 4;
const outputJson = process.argv.includes("--json");
const useNativeHydrate = process.argv.includes("--native-hydrate");
const nativeHydrateThresholdArg = process.argv.find((entry) =>
String(entry || "").startsWith("--native-hydrate-threshold="),
);
const nativeHydrateThresholdRecords = nativeHydrateThresholdArg
? Math.max(
0,
Math.floor(
Number(String(nativeHydrateThresholdArg).split("=").slice(1).join("=") || 0) || 0,
),
)
: undefined;
const SIZE_PRESETS = [
{ label: "M", seed: 17, nodeCount: 1200, edgeCount: 3600, churn: 0.08 },
{ label: "L", seed: 29, nodeCount: 3600, edgeCount: 10800, churn: 0.1 },
{ label: "XL", seed: 43, nodeCount: 7200, edgeCount: 21600, churn: 0.12 },
];
let nativeHydratePreloadStatus = useNativeHydrate ? "pending" : "not-requested";
let nativeHydratePreloadError = "";
function summarize(values = []) {
if (!values.length) {
return { avg: 0, p95: 0, min: 0, max: 0 };
}
const sorted = [...values].sort((a, b) => a - b);
const sum = sorted.reduce((acc, value) => acc + value, 0);
const p95Index = Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95));
return {
avg: sum / sorted.length,
p95: sorted[p95Index],
min: sorted[0],
max: sorted[sorted.length - 1],
};
}
function formatSummary(label, values = []) {
const summary = summarize(values);
return `${label} avg=${summary.avg.toFixed(2)}ms p95=${summary.p95.toFixed(2)}ms min=${summary.min.toFixed(2)}ms max=${summary.max.toFixed(2)}ms`;
}
function createRandom(seed = 1) {
let state = seed >>> 0;
return () => {
state = (state * 1664525 + 1013904223) >>> 0;
return state / 0xffffffff;
};
}
function buildRuntimeGraph(seed = 1, nodeCount = 100, edgeCount = 200, chatId = "bench-chat") {
const rand = createRandom(seed);
const nodes = [];
const edges = [];
for (let index = 0; index < nodeCount; index += 1) {
nodes.push({
id: `node-${index}`,
type: "event",
updatedAt: 1000 + index,
archived: false,
sourceFloor: index,
fields: {
title: `Node ${index}`,
text: `node-${index}-${Math.floor(rand() * 100000)}`,
},
});
}
for (let index = 0; index < edgeCount; index += 1) {
const fromIndex = Math.floor(rand() * nodeCount);
let toIndex = Math.floor(rand() * nodeCount);
if (toIndex === fromIndex) {
toIndex = (toIndex + 1) % nodeCount;
}
edges.push({
id: `edge-${index}`,
fromId: `node-${fromIndex}`,
toId: `node-${toIndex}`,
relation: "related",
strength: rand(),
updatedAt: 2000 + index,
});
}
return {
version: 1,
nodes,
edges,
historyState: {
chatId,
lastProcessedAssistantFloor: Math.max(0, Math.floor(nodeCount / 12)),
extractionCount: Math.max(1, Math.floor(nodeCount / 40)),
processedMessageHashes: {},
processedMessageHashVersion: 1,
processedMessageHashesNeedRefresh: false,
recentRecallOwnerKeys: [],
activeRecallOwnerKey: "",
activeRegion: "",
activeRegionSource: "",
activeStorySegmentId: "",
activeStoryTimeLabel: "",
activeStoryTimeSource: "",
lastBatchStatus: null,
lastMutationSource: "bench",
lastExtractedRegion: "",
lastExtractedStorySegmentId: "",
activeCharacterPovOwner: "",
activeUserPovOwner: "",
},
vectorIndexState: {
chatId,
collectionId: "",
hashToNodeId: {},
nodeToHash: {},
replayRequiredNodeIds: [],
dirty: false,
dirtyReason: "",
pendingRepairFromFloor: null,
lastIntegrityIssue: null,
lastStats: {
nodesIndexed: 0,
updatedAt: 0,
},
},
knowledgeState: {
owners: {},
activeOwnerKey: "",
},
regionState: {
activeRegion: "",
knownRegions: {},
manualActiveRegion: "",
},
timelineState: {
activeSegmentId: "",
manualActiveSegmentId: "",
segments: [],
},
summaryState: {
updatedAt: 0,
entries: [],
},
batchJournal: [],
maintenanceJournal: [],
lastRecallResult: null,
lastProcessedSeq: Math.max(0, Math.floor(nodeCount / 12)),
};
}
function mutateRuntimeGraph(baseGraph, seed = 1, churn = 0.1) {
const rand = createRandom(seed);
const nextGraph = structuredClone(baseGraph);
const mutateNodeCount = Math.max(1, Math.floor(nextGraph.nodes.length * churn));
const mutateEdgeCount = Math.max(1, Math.floor(nextGraph.edges.length * churn * 0.5));
for (let index = 0; index < mutateNodeCount; index += 1) {
const nodeIndex = Math.floor(rand() * nextGraph.nodes.length);
const node = nextGraph.nodes[nodeIndex];
node.updatedAt += 100 + index;
node.fields.text = `${node.fields.text}-mut-${index}`;
}
for (let index = 0; index < mutateEdgeCount; index += 1) {
const edgeIndex = Math.floor(rand() * nextGraph.edges.length);
const edge = nextGraph.edges[edgeIndex];
edge.updatedAt += 80 + index;
edge.strength = rand();
}
const addNodeCount = Math.max(1, Math.floor(nextGraph.nodes.length * churn * 0.12));
const baseNodeId = nextGraph.nodes.length;
for (let index = 0; index < addNodeCount; index += 1) {
nextGraph.nodes.push({
id: `node-new-${baseNodeId + index}`,
type: "event",
updatedAt: 5000 + index,
archived: false,
sourceFloor: baseNodeId + index,
fields: {
title: `Node new ${index}`,
text: `new-node-${index}`,
},
});
}
const deleteEdgeCount = Math.max(1, Math.floor(nextGraph.edges.length * churn * 0.08));
nextGraph.edges.splice(0, deleteEdgeCount);
nextGraph.historyState.lastProcessedAssistantFloor += 1;
nextGraph.historyState.extractionCount += 1;
nextGraph.lastProcessedSeq = nextGraph.historyState.lastProcessedAssistantFloor;
nextGraph.summaryState.updatedAt += 1;
return nextGraph;
}
function buildBenchPair({ label, seed, nodeCount, edgeCount, churn }) {
const chatId = `bench-${label.toLowerCase()}`;
const beforeGraph = buildRuntimeGraph(seed, nodeCount, edgeCount, chatId);
const afterGraph = mutateRuntimeGraph(beforeGraph, seed + 101, churn);
return {
label,
chatId,
beforeGraph,
afterGraph,
};
}
function measureSnapshotBuild(graph, options) {
let diagnostics = null;
const startedAt = performance.now();
const snapshot = buildSnapshotFromGraph(graph, {
...options,
onDiagnostics(snapshotValue) {
diagnostics = snapshotValue;
},
});
return {
elapsedMs: performance.now() - startedAt,
snapshot,
diagnostics,
};
}
function measureHydrate(snapshot, chatId) {
let diagnostics = null;
const startedAt = performance.now();
buildGraphFromSnapshot(snapshot, {
chatId,
useNativeHydrate,
loadNativeHydrateThresholdRecords: nativeHydrateThresholdRecords,
nativeFailOpen: true,
onDiagnostics(snapshotValue) {
diagnostics = snapshotValue;
},
});
return {
elapsedMs: performance.now() - startedAt,
diagnostics,
};
}
async function measureOpfsCommit(baseSnapshot, afterSnapshot, delta, chatId) {
const rootDirectory = createMemoryOpfsRoot();
const store = new OpfsGraphStore(chatId, {
rootDirectoryFactory: async () => rootDirectory,
storeMode: BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY,
});
await store.open();
await store.importSnapshot(baseSnapshot, {
mode: "replace",
preserveRevision: true,
markSyncDirty: false,
});
const startedAt = performance.now();
const result = await store.commitDelta(delta, {
reason: "bench-commit",
requestedRevision: Number(afterSnapshot?.meta?.revision || 0),
markSyncDirty: true,
committedSnapshot: afterSnapshot,
});
const elapsedMs = performance.now() - startedAt;
await store.close();
return {
elapsedMs,
diagnostics: result?.diagnostics || {},
};
}
async function runPreset(preset) {
const snapshotBuildSamples = [];
const hydrateSamples = [];
const opfsCommitSamples = [];
const snapshotNodesSamples = [];
const hydrateRuntimeMetaSamples = [];
const hydrateNodesSamples = [];
const hydrateEdgesSamples = [];
const hydrateStateSamples = [];
const hydrateNormalizeSamples = [];
const hydrateIntegritySamples = [];
const hydrateNativeRecordsSamples = [];
const walFileWriteSamples = [];
const manifestFileWriteSamples = [];
let hydrateNativeUsedRuns = 0;
for (let run = 0; run < RUNS; run += 1) {
const pair = buildBenchPair({
...preset,
seed: preset.seed + run * 17,
});
const beforeSnapshotResult = measureSnapshotBuild(pair.beforeGraph, {
chatId: pair.chatId,
revision: 1,
});
const afterSnapshotResult = measureSnapshotBuild(pair.afterGraph, {
chatId: pair.chatId,
revision: 2,
baseSnapshot: beforeSnapshotResult.snapshot,
});
const delta = buildPersistDelta(
beforeSnapshotResult.snapshot,
afterSnapshotResult.snapshot,
{ useNativeDelta: false },
);
const hydrateResult = measureHydrate(afterSnapshotResult.snapshot, pair.chatId);
const opfsCommitResult = await measureOpfsCommit(
beforeSnapshotResult.snapshot,
afterSnapshotResult.snapshot,
delta,
pair.chatId,
);
snapshotBuildSamples.push(afterSnapshotResult.elapsedMs);
hydrateSamples.push(hydrateResult.elapsedMs);
opfsCommitSamples.push(opfsCommitResult.elapsedMs);
snapshotNodesSamples.push(Number(afterSnapshotResult.diagnostics?.nodesMs || 0));
hydrateRuntimeMetaSamples.push(Number(hydrateResult.diagnostics?.runtimeMetaMs || 0));
hydrateNodesSamples.push(Number(hydrateResult.diagnostics?.nodesMs || 0));
hydrateEdgesSamples.push(Number(hydrateResult.diagnostics?.edgesMs || 0));
hydrateStateSamples.push(Number(hydrateResult.diagnostics?.stateMs || 0));
hydrateNormalizeSamples.push(Number(hydrateResult.diagnostics?.normalizeMs || 0));
hydrateIntegritySamples.push(Number(hydrateResult.diagnostics?.integrityMs || 0));
hydrateNativeRecordsSamples.push(
Number(hydrateResult.diagnostics?.nativeRecordsMs || 0),
);
if (hydrateResult.diagnostics?.nativeUsed === true) {
hydrateNativeUsedRuns += 1;
}
walFileWriteSamples.push(Number(opfsCommitResult.diagnostics?.walFileWriteMs || 0));
manifestFileWriteSamples.push(
Number(opfsCommitResult.diagnostics?.manifestFileWriteMs || 0),
);
}
const result = {
snapshotBuildMs: summarize(snapshotBuildSamples),
snapshotNodesMs: summarize(snapshotNodesSamples),
hydrateMs: summarize(hydrateSamples),
hydrateNodesMs: summarize(hydrateNodesSamples),
hydrateEdgesMs: summarize(hydrateEdgesSamples),
hydrateStateMs: summarize(hydrateStateSamples),
hydrateNormalizeMs: summarize(hydrateNormalizeSamples),
hydrateIntegrityMs: summarize(hydrateIntegritySamples),
hydrateNativeRecordsMs: summarize(hydrateNativeRecordsSamples),
hydrateNativeUsedRuns,
nativeHydrateRequested: useNativeHydrate,
nativeHydrateThresholdRecords:
nativeHydrateThresholdRecords == null ? null : nativeHydrateThresholdRecords,
hydrateRuntimeMetaMs: summarize(hydrateRuntimeMetaSamples),
opfsCommitMs: summarize(opfsCommitSamples),
opfsWalFileWriteMs: summarize(walFileWriteSamples),
opfsManifestFileWriteMs: summarize(manifestFileWriteSamples),
};
if (!outputJson) {
console.log(`\n[ST-BME][persist-load-bench] ${preset.label}`);
console.log(
formatSummary("snapshot-build", snapshotBuildSamples),
`nodesPhaseP95=${result.snapshotNodesMs.p95.toFixed(2)}ms`,
);
console.log(
formatSummary("hydrate", hydrateSamples),
`nodesP95=${result.hydrateNodesMs.p95.toFixed(2)}ms`,
`edgesP95=${result.hydrateEdgesMs.p95.toFixed(2)}ms`,
`normalizeP95=${result.hydrateNormalizeMs.p95.toFixed(2)}ms`,
`integrityP95=${result.hydrateIntegrityMs.p95.toFixed(2)}ms`,
`nativeRecordsP95=${result.hydrateNativeRecordsMs.p95.toFixed(2)}ms`,
`nativeUsed=${result.hydrateNativeUsedRuns}/${RUNS}`,
`runtimeMetaP95=${result.hydrateRuntimeMetaMs.p95.toFixed(2)}ms`,
);
console.log(
formatSummary("opfs-commit", opfsCommitSamples),
`walFileP95=${result.opfsWalFileWriteMs.p95.toFixed(2)}ms`,
`manifestFileP95=${result.opfsManifestFileWriteMs.p95.toFixed(2)}ms`,
);
}
return result;
}
async function main() {
if (useNativeHydrate) {
try {
const nativeModule = await import("../../vendor/wasm/stbme_core.js");
const nativeStatus = await nativeModule?.installNativeHydrateHook?.();
nativeHydratePreloadStatus = nativeStatus?.loaded ? "loaded" : "not-loaded";
nativeHydratePreloadError = String(nativeStatus?.error || "");
} catch (error) {
nativeHydratePreloadStatus = "failed";
nativeHydratePreloadError = error?.message || String(error);
console.warn(
"[ST-BME][persist-load-bench] native hydrate preload failed, fallback to JS hydrate:",
error,
);
}
}
const presets = {};
for (const preset of SIZE_PRESETS) {
presets[preset.label] = await runPreset(preset);
}
const payload = {
runs: RUNS,
nativeHydrateRequested: useNativeHydrate,
nativeHydratePreloadStatus,
nativeHydratePreloadError,
nativeHydrateThresholdRecords:
nativeHydrateThresholdRecords == null ? null : nativeHydrateThresholdRecords,
presets,
};
if (outputJson) {
console.log(JSON.stringify(payload));
}
}
await main();