mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 14:20:35 +08:00
398 lines
12 KiB
JavaScript
398 lines
12 KiB
JavaScript
import { performance } from "node:perf_hooks";
|
|
import path from "node:path";
|
|
import { createRequire } from "node:module";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
import {
|
|
BmeDatabase,
|
|
buildBmeDbName,
|
|
buildGraphFromSnapshot,
|
|
buildSnapshotFromGraph,
|
|
ensureDexieLoaded,
|
|
} 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 projectRootHint = String(process.env.ST_BME_NODE_MODULES_ROOT || "").trim();
|
|
const requireFromProjectRoot = projectRootHint
|
|
? createRequire(path.join(projectRootHint, "package.json"))
|
|
: null;
|
|
const SIZE_PRESETS = [
|
|
{ label: "M", seed: 17, nodeCount: 1200, edgeCount: 3600 },
|
|
{ label: "L", seed: 29, nodeCount: 3600, edgeCount: 10800 },
|
|
{ label: "XL", seed: 43, nodeCount: 7200, edgeCount: 21600 },
|
|
];
|
|
|
|
async function importWithProjectRootFallback(specifier) {
|
|
try {
|
|
return await import(specifier);
|
|
} catch (error) {
|
|
if (!requireFromProjectRoot) {
|
|
throw error;
|
|
}
|
|
const resolved = requireFromProjectRoot.resolve(specifier);
|
|
return await import(pathToFileURL(resolved).href);
|
|
}
|
|
}
|
|
|
|
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 buildBenchSnapshot({ label, seed, nodeCount, edgeCount }) {
|
|
const chatId = `load-bench-${label.toLowerCase()}-${seed}`;
|
|
const graph = buildRuntimeGraph(seed, nodeCount, edgeCount, chatId);
|
|
return {
|
|
chatId,
|
|
snapshot: buildSnapshotFromGraph(graph, {
|
|
chatId,
|
|
revision: 1,
|
|
}),
|
|
};
|
|
}
|
|
|
|
async function setupIndexedDbTestEnv() {
|
|
try {
|
|
await importWithProjectRootFallback("fake-indexeddb/auto");
|
|
} catch {
|
|
// no-op
|
|
}
|
|
|
|
if (!globalThis.Dexie) {
|
|
try {
|
|
const imported = await importWithProjectRootFallback("dexie");
|
|
globalThis.Dexie = imported?.default || imported?.Dexie || imported;
|
|
} catch {
|
|
await import("../../lib/dexie.min.js");
|
|
}
|
|
}
|
|
|
|
await ensureDexieLoaded();
|
|
}
|
|
|
|
async function cleanupDatabase(chatId = "") {
|
|
if (!chatId || typeof globalThis.Dexie?.delete !== "function") return;
|
|
try {
|
|
await globalThis.Dexie.delete(buildBmeDbName(chatId));
|
|
} catch {
|
|
// no-op
|
|
}
|
|
}
|
|
|
|
async function prepareIndexedDb(chatId, snapshot) {
|
|
await cleanupDatabase(chatId);
|
|
const db = new BmeDatabase(chatId, { dexieClass: globalThis.Dexie });
|
|
await db.open();
|
|
await db.importSnapshot(snapshot, {
|
|
mode: "replace",
|
|
preserveRevision: true,
|
|
markSyncDirty: false,
|
|
});
|
|
return db;
|
|
}
|
|
|
|
async function prepareOpfsStore(chatId, snapshot) {
|
|
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(snapshot, {
|
|
mode: "replace",
|
|
preserveRevision: true,
|
|
markSyncDirty: false,
|
|
});
|
|
return store;
|
|
}
|
|
|
|
async function readProbeOrFallback(store) {
|
|
let inspectionSnapshot = null;
|
|
let exportProbeMs = 0;
|
|
let exportSnapshotMs = 0;
|
|
let exportSource = "";
|
|
|
|
if (typeof store.exportSnapshotProbe === "function") {
|
|
const probeStartedAt = performance.now();
|
|
inspectionSnapshot = await store.exportSnapshotProbe({ includeTombstones: false });
|
|
exportProbeMs = performance.now() - probeStartedAt;
|
|
exportSource = "probe";
|
|
}
|
|
|
|
if (!inspectionSnapshot) {
|
|
const exportStartedAt = performance.now();
|
|
inspectionSnapshot = await store.exportSnapshot({ includeTombstones: false });
|
|
exportSnapshotMs = performance.now() - exportStartedAt;
|
|
exportSource = "full-export";
|
|
}
|
|
|
|
return {
|
|
inspectionSnapshot,
|
|
exportProbeMs,
|
|
exportSnapshotMs,
|
|
exportSource,
|
|
};
|
|
}
|
|
|
|
async function measureSuccessPreApply(store, chatId) {
|
|
const startedAt = performance.now();
|
|
const probeResult = await readProbeOrFallback(store);
|
|
let snapshot = probeResult.inspectionSnapshot;
|
|
let exportSnapshotMs = probeResult.exportSnapshotMs;
|
|
let exportSource = probeResult.exportSource;
|
|
|
|
if (snapshot?.__stBmeProbeOnly === true) {
|
|
const exportStartedAt = performance.now();
|
|
snapshot = await store.exportSnapshot({ includeTombstones: false });
|
|
exportSnapshotMs += performance.now() - exportStartedAt;
|
|
exportSource =
|
|
probeResult.exportSource === "probe" ? "probe+full-export" : "full-export";
|
|
}
|
|
|
|
const preApplyMs = performance.now() - startedAt;
|
|
const hydrateStartedAt = performance.now();
|
|
buildGraphFromSnapshot(snapshot, { chatId });
|
|
const hydrateMs = performance.now() - hydrateStartedAt;
|
|
|
|
return {
|
|
preApplyMs,
|
|
exportProbeMs: probeResult.exportProbeMs,
|
|
exportSnapshotMs,
|
|
hydrateMs,
|
|
exportSource,
|
|
};
|
|
}
|
|
|
|
async function measureProbeRejectPreApply(store) {
|
|
const startedAt = performance.now();
|
|
const probeResult = await readProbeOrFallback(store);
|
|
return {
|
|
preApplyMs: performance.now() - startedAt,
|
|
exportProbeMs: probeResult.exportProbeMs,
|
|
exportSnapshotMs: probeResult.exportSnapshotMs,
|
|
exportSource: probeResult.exportSource,
|
|
};
|
|
}
|
|
|
|
async function runPreset(preset) {
|
|
const indexedDbSuccessSamples = [];
|
|
const indexedDbProbeRejectSamples = [];
|
|
const indexedDbProbeSamples = [];
|
|
const indexedDbExportSamples = [];
|
|
const indexedDbHydrateSamples = [];
|
|
const opfsSuccessSamples = [];
|
|
const opfsProbeRejectSamples = [];
|
|
const opfsProbeSamples = [];
|
|
const opfsExportSamples = [];
|
|
const opfsHydrateSamples = [];
|
|
|
|
for (let run = 0; run < RUNS; run += 1) {
|
|
const { chatId, snapshot } = buildBenchSnapshot({
|
|
...preset,
|
|
seed: preset.seed + run * 17,
|
|
});
|
|
|
|
const indexedDbChatId = `${chatId}-indexeddb`;
|
|
const db = await prepareIndexedDb(indexedDbChatId, snapshot);
|
|
const indexedDbSuccess = await measureSuccessPreApply(db, indexedDbChatId);
|
|
const indexedDbProbeReject = await measureProbeRejectPreApply(db);
|
|
indexedDbSuccessSamples.push(indexedDbSuccess.preApplyMs);
|
|
indexedDbProbeRejectSamples.push(indexedDbProbeReject.preApplyMs);
|
|
indexedDbProbeSamples.push(indexedDbSuccess.exportProbeMs);
|
|
indexedDbExportSamples.push(indexedDbSuccess.exportSnapshotMs);
|
|
indexedDbHydrateSamples.push(indexedDbSuccess.hydrateMs);
|
|
await db.close();
|
|
await cleanupDatabase(indexedDbChatId);
|
|
|
|
const opfsChatId = `${chatId}-opfs`;
|
|
const opfsStore = await prepareOpfsStore(opfsChatId, snapshot);
|
|
const opfsSuccess = await measureSuccessPreApply(opfsStore, opfsChatId);
|
|
const opfsProbeReject = await measureProbeRejectPreApply(opfsStore);
|
|
opfsSuccessSamples.push(opfsSuccess.preApplyMs);
|
|
opfsProbeRejectSamples.push(opfsProbeReject.preApplyMs);
|
|
opfsProbeSamples.push(opfsSuccess.exportProbeMs);
|
|
opfsExportSamples.push(opfsSuccess.exportSnapshotMs);
|
|
opfsHydrateSamples.push(opfsSuccess.hydrateMs);
|
|
await opfsStore.close();
|
|
}
|
|
|
|
const result = {
|
|
indexedDbPreApplySuccessMs: summarize(indexedDbSuccessSamples),
|
|
indexedDbProbeRejectMs: summarize(indexedDbProbeRejectSamples),
|
|
indexedDbExportProbeMs: summarize(indexedDbProbeSamples),
|
|
indexedDbExportSnapshotMs: summarize(indexedDbExportSamples),
|
|
indexedDbHydrateMs: summarize(indexedDbHydrateSamples),
|
|
opfsPreApplySuccessMs: summarize(opfsSuccessSamples),
|
|
opfsProbeRejectMs: summarize(opfsProbeRejectSamples),
|
|
opfsExportProbeMs: summarize(opfsProbeSamples),
|
|
opfsExportSnapshotMs: summarize(opfsExportSamples),
|
|
opfsHydrateMs: summarize(opfsHydrateSamples),
|
|
};
|
|
|
|
if (!outputJson) {
|
|
console.log(`\n[ST-BME][load-preapply-bench] ${preset.label}`);
|
|
console.log(
|
|
formatSummary("indexeddb-preapply-success", indexedDbSuccessSamples),
|
|
`probeRejectP95=${result.indexedDbProbeRejectMs.p95.toFixed(2)}ms`,
|
|
`probeP95=${result.indexedDbExportProbeMs.p95.toFixed(2)}ms`,
|
|
`exportP95=${result.indexedDbExportSnapshotMs.p95.toFixed(2)}ms`,
|
|
);
|
|
console.log(
|
|
formatSummary("opfs-preapply-success", opfsSuccessSamples),
|
|
`probeRejectP95=${result.opfsProbeRejectMs.p95.toFixed(2)}ms`,
|
|
`probeP95=${result.opfsExportProbeMs.p95.toFixed(2)}ms`,
|
|
`exportP95=${result.opfsExportSnapshotMs.p95.toFixed(2)}ms`,
|
|
);
|
|
console.log(
|
|
formatSummary("indexeddb-hydrate", indexedDbHydrateSamples),
|
|
formatSummary("opfs-hydrate", opfsHydrateSamples),
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async function main() {
|
|
await setupIndexedDbTestEnv();
|
|
const results = {};
|
|
for (const preset of SIZE_PRESETS) {
|
|
results[preset.label] = await runPreset(preset);
|
|
}
|
|
if (outputJson) {
|
|
console.log(
|
|
JSON.stringify({
|
|
runs: RUNS,
|
|
presets: results,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
await main();
|