mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
299 lines
9.8 KiB
JavaScript
299 lines
9.8 KiB
JavaScript
import { performance } from "node:perf_hooks";
|
|
|
|
import {
|
|
buildPersistDelta,
|
|
resetPersistRecordSerializationCaches,
|
|
} from "../../sync/bme-db.js";
|
|
import {
|
|
getNativeModuleStatus,
|
|
installNativePersistDeltaHook,
|
|
} from "../../vendor/wasm/stbme_core.js";
|
|
|
|
const RUNS = 5;
|
|
|
|
function buildSnapshots(seed = 5, nodeCount = 5000, edgeCount = 12000, churn = 0.1) {
|
|
let state = seed >>> 0;
|
|
const rand = () => {
|
|
state = (state * 1664525 + 1013904223) >>> 0;
|
|
return state / 0xffffffff;
|
|
};
|
|
|
|
const beforeNodes = [];
|
|
for (let i = 0; i < nodeCount; i++) {
|
|
beforeNodes.push({
|
|
id: `n-${i}`,
|
|
type: "event",
|
|
fields: {
|
|
text: `node-${i}`,
|
|
v: Math.floor(rand() * 1000),
|
|
},
|
|
archived: false,
|
|
updatedAt: 1000 + i,
|
|
});
|
|
}
|
|
|
|
const beforeEdges = [];
|
|
for (let i = 0; i < edgeCount; i++) {
|
|
const from = Math.floor(rand() * nodeCount);
|
|
let to = Math.floor(rand() * nodeCount);
|
|
if (to === from) to = (to + 1) % nodeCount;
|
|
beforeEdges.push({
|
|
id: `e-${i}`,
|
|
fromId: `n-${from}`,
|
|
toId: `n-${to}`,
|
|
relation: "related",
|
|
strength: rand(),
|
|
updatedAt: 1000 + i,
|
|
});
|
|
}
|
|
|
|
const afterNodes = beforeNodes.map((node) => ({ ...node, fields: { ...node.fields } }));
|
|
const afterEdges = beforeEdges.map((edge) => ({ ...edge }));
|
|
|
|
const mutateNodeCount = Math.floor(nodeCount * churn);
|
|
for (let i = 0; i < mutateNodeCount; i++) {
|
|
const index = Math.floor(rand() * afterNodes.length);
|
|
afterNodes[index].fields.v = Math.floor(rand() * 5000);
|
|
afterNodes[index].updatedAt += 100;
|
|
}
|
|
|
|
const addNodeCount = Math.max(1, Math.floor(nodeCount * churn * 0.25));
|
|
const baseNodeId = afterNodes.length;
|
|
for (let i = 0; i < addNodeCount; i++) {
|
|
afterNodes.push({
|
|
id: `n-new-${baseNodeId + i}`,
|
|
type: "event",
|
|
fields: { text: `new-${i}`, v: Math.floor(rand() * 3000) },
|
|
archived: false,
|
|
updatedAt: 5000 + i,
|
|
});
|
|
}
|
|
|
|
const removeEdgeCount = Math.max(1, Math.floor(edgeCount * churn * 0.2));
|
|
afterEdges.splice(0, removeEdgeCount);
|
|
|
|
return {
|
|
before: {
|
|
meta: { chatId: "bench-chat", revision: 1, lastModified: 1000 },
|
|
state: { lastProcessedFloor: 1, extractionCount: 1 },
|
|
nodes: beforeNodes,
|
|
edges: beforeEdges,
|
|
tombstones: [],
|
|
},
|
|
after: {
|
|
meta: { chatId: "bench-chat", revision: 2, lastModified: 2000 },
|
|
state: { lastProcessedFloor: 2, extractionCount: 2 },
|
|
nodes: afterNodes,
|
|
edges: afterEdges,
|
|
tombstones: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function summarizeDiagnostics(samples = []) {
|
|
const summary = {
|
|
prepareMs: 0,
|
|
nativeAttemptMs: 0,
|
|
lookupMs: 0,
|
|
jsDiffMs: 0,
|
|
hydrateMs: 0,
|
|
serializationCacheHits: 0,
|
|
serializationCacheMisses: 0,
|
|
preparedRecordSetCacheHits: 0,
|
|
preparedRecordSetCacheMisses: 0,
|
|
};
|
|
if (!samples.length) return summary;
|
|
for (const sample of samples) {
|
|
summary.prepareMs += Number(sample?.prepareMs || 0);
|
|
summary.nativeAttemptMs += Number(sample?.nativeAttemptMs || 0);
|
|
summary.lookupMs += Number(sample?.lookupMs || 0);
|
|
summary.jsDiffMs += Number(sample?.jsDiffMs || 0);
|
|
summary.hydrateMs += Number(sample?.hydrateMs || 0);
|
|
summary.serializationCacheHits += Number(sample?.serializationCacheHits || 0);
|
|
summary.serializationCacheMisses += Number(sample?.serializationCacheMisses || 0);
|
|
summary.preparedRecordSetCacheHits += Number(
|
|
sample?.preparedRecordSetCacheHits || 0,
|
|
);
|
|
summary.preparedRecordSetCacheMisses += Number(
|
|
sample?.preparedRecordSetCacheMisses || 0,
|
|
);
|
|
}
|
|
for (const key of Object.keys(summary)) {
|
|
summary[key] /= samples.length;
|
|
}
|
|
return summary;
|
|
}
|
|
|
|
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 buildModeOptions(mode, onDiagnostics, extraOptions = {}) {
|
|
if (mode === "native-json") {
|
|
return {
|
|
useNativeDelta: true,
|
|
minSnapshotRecords: 0,
|
|
minStructuralDelta: 0,
|
|
minCombinedSerializedChars: 0,
|
|
persistNativeDeltaBridgeMode: "json",
|
|
nativeFailOpen: false,
|
|
onDiagnostics,
|
|
...extraOptions,
|
|
};
|
|
}
|
|
if (mode === "native-hash") {
|
|
return {
|
|
useNativeDelta: true,
|
|
minSnapshotRecords: 0,
|
|
minStructuralDelta: 0,
|
|
minCombinedSerializedChars: 0,
|
|
persistNativeDeltaBridgeMode: "hash",
|
|
nativeFailOpen: false,
|
|
onDiagnostics,
|
|
...extraOptions,
|
|
};
|
|
}
|
|
return {
|
|
useNativeDelta: false,
|
|
onDiagnostics,
|
|
...extraOptions,
|
|
};
|
|
}
|
|
|
|
function runMeasuredPersistDeltaSample(
|
|
snapshots,
|
|
mode,
|
|
{ resetCaches = true, usePreparedRecordSetCache = true } = {},
|
|
) {
|
|
if (resetCaches) {
|
|
resetPersistRecordSerializationCaches();
|
|
}
|
|
let diagnostics = null;
|
|
const startedAt = performance.now();
|
|
const delta = buildPersistDelta(
|
|
snapshots.before,
|
|
snapshots.after,
|
|
buildModeOptions(
|
|
mode,
|
|
(snapshot) => {
|
|
diagnostics = snapshot;
|
|
},
|
|
{ usePreparedRecordSetCache },
|
|
),
|
|
);
|
|
const elapsedMs = performance.now() - startedAt;
|
|
return {
|
|
elapsedMs,
|
|
upsertNodes: delta.upsertNodes.length,
|
|
upsertEdges: delta.upsertEdges.length,
|
|
deleteNodeIds: delta.deleteNodeIds.length,
|
|
deleteEdgeIds: delta.deleteEdgeIds.length,
|
|
prepareMs: diagnostics?.prepareMs,
|
|
nativeAttemptMs: diagnostics?.nativeAttemptMs,
|
|
lookupMs: diagnostics?.lookupMs,
|
|
jsDiffMs: diagnostics?.jsDiffMs,
|
|
hydrateMs: diagnostics?.hydrateMs,
|
|
serializationCacheHits: diagnostics?.serializationCacheHits,
|
|
serializationCacheMisses: diagnostics?.serializationCacheMisses,
|
|
preparedRecordSetCacheHits: diagnostics?.preparedRecordSetCacheHits,
|
|
preparedRecordSetCacheMisses: diagnostics?.preparedRecordSetCacheMisses,
|
|
};
|
|
}
|
|
|
|
function primePersistDeltaCaches(snapshots, mode) {
|
|
buildPersistDelta(
|
|
snapshots.before,
|
|
snapshots.after,
|
|
buildModeOptions(mode, undefined, {
|
|
usePreparedRecordSetCache: true,
|
|
onDiagnostics() {},
|
|
}),
|
|
);
|
|
}
|
|
|
|
function formatTimingSummary(label, samples = []) {
|
|
const timingSummary = summarize(samples.map((sample) => sample.elapsedMs));
|
|
return `${label} avg=${timingSummary.avg.toFixed(2)}ms p95=${timingSummary.p95.toFixed(2)}ms min=${timingSummary.min.toFixed(2)}ms max=${timingSummary.max.toFixed(2)}ms`;
|
|
}
|
|
|
|
function formatStageSummary(label, samples = []) {
|
|
const diagnosticsSummary = summarizeDiagnostics(samples);
|
|
return `${label} prepare=${diagnosticsSummary.prepareMs.toFixed(2)}ms native=${diagnosticsSummary.nativeAttemptMs.toFixed(2)}ms lookup=${diagnosticsSummary.lookupMs.toFixed(2)}ms diff=${diagnosticsSummary.jsDiffMs.toFixed(2)}ms hydrate=${diagnosticsSummary.hydrateMs.toFixed(2)}ms ser-cache=${diagnosticsSummary.serializationCacheHits.toFixed(1)}H/${diagnosticsSummary.serializationCacheMisses.toFixed(1)}M set-cache=${diagnosticsSummary.preparedRecordSetCacheHits.toFixed(1)}H/${diagnosticsSummary.preparedRecordSetCacheMisses.toFixed(1)}M`;
|
|
}
|
|
|
|
async function main() {
|
|
await installNativePersistDeltaHook();
|
|
const nativeStatus = getNativeModuleStatus();
|
|
const coldSamplesByMode = {
|
|
js: [],
|
|
"native-json": [],
|
|
"native-hash": [],
|
|
};
|
|
const warmSamplesByMode = {
|
|
js: [],
|
|
"native-json": [],
|
|
"native-hash": [],
|
|
};
|
|
const modes = ["js", "native-json", "native-hash"];
|
|
for (let run = 0; run < RUNS; run++) {
|
|
const snapshots = buildSnapshots(17 + run, 5000, 12000, 0.12);
|
|
for (const mode of modes) {
|
|
coldSamplesByMode[mode].push(
|
|
runMeasuredPersistDeltaSample(snapshots, mode, {
|
|
resetCaches: true,
|
|
usePreparedRecordSetCache: false,
|
|
}),
|
|
);
|
|
resetPersistRecordSerializationCaches();
|
|
primePersistDeltaCaches(snapshots, mode);
|
|
warmSamplesByMode[mode].push(
|
|
runMeasuredPersistDeltaSample(snapshots, mode, {
|
|
resetCaches: false,
|
|
usePreparedRecordSetCache: true,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
const avgUpserts =
|
|
coldSamplesByMode.js.reduce(
|
|
(acc, sample) => acc + sample.upsertNodes + sample.upsertEdges,
|
|
0,
|
|
) / coldSamplesByMode.js.length;
|
|
const avgDeletes =
|
|
coldSamplesByMode.js.reduce(
|
|
(acc, sample) => acc + sample.deleteNodeIds + sample.deleteEdgeIds,
|
|
0,
|
|
) / coldSamplesByMode.js.length;
|
|
|
|
console.log(
|
|
`[ST-BME][bench] persist-delta native-source=${nativeStatus.source || "unknown"}`,
|
|
);
|
|
console.log(
|
|
`[ST-BME][bench] persist-delta cold runs=${RUNS} | ${formatTimingSummary("js", coldSamplesByMode.js)} | ${formatTimingSummary("native-json", coldSamplesByMode["native-json"])} | ${formatTimingSummary("native-hash", coldSamplesByMode["native-hash"])} | avgUpserts=${avgUpserts.toFixed(1)} avgDeletes=${avgDeletes.toFixed(1)}`,
|
|
);
|
|
console.log(
|
|
`[ST-BME][bench] persist-delta cold stages | ${formatStageSummary("js", coldSamplesByMode.js)} | ${formatStageSummary("native-json", coldSamplesByMode["native-json"])} | ${formatStageSummary("native-hash", coldSamplesByMode["native-hash"])} `,
|
|
);
|
|
console.log(
|
|
`[ST-BME][bench] persist-delta warm runs=${RUNS} | ${formatTimingSummary("js", warmSamplesByMode.js)} | ${formatTimingSummary("native-json", warmSamplesByMode["native-json"])} | ${formatTimingSummary("native-hash", warmSamplesByMode["native-hash"])} `,
|
|
);
|
|
console.log(
|
|
`[ST-BME][bench] persist-delta warm stages | ${formatStageSummary("js", warmSamplesByMode.js)} | ${formatStageSummary("native-json", warmSamplesByMode["native-json"])} | ${formatStageSummary("native-hash", warmSamplesByMode["native-hash"])} `,
|
|
);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error("[ST-BME][bench] persist-delta failed:", error?.message || String(error));
|
|
process.exitCode = 1;
|
|
});
|