mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
perf: optimize persist delta gating and diagnostics
This commit is contained in:
@@ -67,6 +67,16 @@ assert.equal(defaultSettings.worldInfoFilterMode, "default");
|
||||
assert.equal(defaultSettings.worldInfoFilterCustomKeywords, "");
|
||||
assert.equal("maintenanceAutoMinNewNodes" in defaultSettings, false);
|
||||
assert.equal(defaultSettings.embeddingTransportMode, "direct");
|
||||
assert.equal(defaultSettings.graphUseNativeLayout, false);
|
||||
assert.equal(defaultSettings.graphNativeLayoutThresholdNodes, 280);
|
||||
assert.equal(defaultSettings.graphNativeLayoutThresholdEdges, 1600);
|
||||
assert.equal(defaultSettings.graphNativeLayoutWorkerTimeoutMs, 260);
|
||||
assert.equal(defaultSettings.persistUseNativeDelta, false);
|
||||
assert.equal(defaultSettings.persistNativeDeltaThresholdRecords, 20000);
|
||||
assert.equal(defaultSettings.persistNativeDeltaThresholdStructuralDelta, 600);
|
||||
assert.equal(defaultSettings.persistNativeDeltaThresholdSerializedChars, 4000000);
|
||||
assert.equal(defaultSettings.nativeEngineFailOpen, true);
|
||||
assert.equal(defaultSettings.graphNativeForceDisable, false);
|
||||
assert.equal(defaultSettings.taskProfilesVersion, 3);
|
||||
assert.ok(defaultSettings.taskProfiles);
|
||||
assert.ok(defaultSettings.taskProfiles.extract);
|
||||
|
||||
76
tests/graph-layout-solver.mjs
Normal file
76
tests/graph-layout-solver.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { solveLayoutWithJs } from "../ui/graph-layout-solver.js";
|
||||
|
||||
const payload = {
|
||||
nodes: [
|
||||
{
|
||||
x: 20,
|
||||
y: 20,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
pinned: false,
|
||||
radius: 8,
|
||||
regionKey: "objective",
|
||||
regionRect: { x: 0, y: 0, w: 200, h: 160 },
|
||||
},
|
||||
{
|
||||
x: 80,
|
||||
y: 30,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
pinned: false,
|
||||
radius: 9,
|
||||
regionKey: "objective",
|
||||
regionRect: { x: 0, y: 0, w: 200, h: 160 },
|
||||
},
|
||||
{
|
||||
x: 100,
|
||||
y: 80,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
pinned: true,
|
||||
radius: 10,
|
||||
regionKey: "objective",
|
||||
regionRect: { x: 0, y: 0, w: 200, h: 160 },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ from: 0, to: 1, strength: 0.8 },
|
||||
{ from: 1, to: 2, strength: 0.6 },
|
||||
],
|
||||
config: {
|
||||
iterations: 32,
|
||||
repulsion: 2200,
|
||||
springK: 0.05,
|
||||
damping: 0.87,
|
||||
centerGravity: 0.02,
|
||||
minGap: 10,
|
||||
speedCap: 3,
|
||||
},
|
||||
};
|
||||
|
||||
const resultA = solveLayoutWithJs(payload);
|
||||
assert.equal(resultA.ok, true);
|
||||
assert.ok(resultA.positions instanceof Float32Array);
|
||||
assert.equal(resultA.positions.length, payload.nodes.length * 2);
|
||||
assert.equal(resultA.diagnostics.solver, "js-worker");
|
||||
assert.equal(resultA.diagnostics.nodeCount, 3);
|
||||
|
||||
const resultB = solveLayoutWithJs(payload);
|
||||
assert.deepEqual(Array.from(resultA.positions), Array.from(resultB.positions));
|
||||
|
||||
for (let index = 0; index < payload.nodes.length; index++) {
|
||||
const x = resultA.positions[index * 2];
|
||||
const y = resultA.positions[index * 2 + 1];
|
||||
assert.ok(Number.isFinite(x));
|
||||
assert.ok(Number.isFinite(y));
|
||||
assert.ok(x >= 0 && x <= 200);
|
||||
assert.ok(y >= 0 && y <= 160);
|
||||
}
|
||||
|
||||
const emptyResult = solveLayoutWithJs({ nodes: [], edges: [] });
|
||||
assert.equal(emptyResult.ok, true);
|
||||
assert.equal(emptyResult.positions.length, 0);
|
||||
|
||||
console.log("graph-layout-solver tests passed");
|
||||
83
tests/graph-native-bridge.mjs
Normal file
83
tests/graph-native-bridge.mjs
Normal file
@@ -0,0 +1,83 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
GraphNativeLayoutBridge,
|
||||
normalizeGraphNativeRuntimeOptions,
|
||||
} from "../ui/graph-native-bridge.js";
|
||||
|
||||
const normalized = normalizeGraphNativeRuntimeOptions({
|
||||
graphUseNativeLayout: "true",
|
||||
graphNativeLayoutThresholdNodes: "333.7",
|
||||
graphNativeLayoutThresholdEdges: -100,
|
||||
graphNativeLayoutWorkerTimeoutMs: 10,
|
||||
nativeEngineFailOpen: "false",
|
||||
graphNativeForceDisable: 1,
|
||||
});
|
||||
|
||||
assert.equal(normalized.graphUseNativeLayout, true);
|
||||
assert.equal(normalized.graphNativeLayoutThresholdNodes, 333);
|
||||
assert.equal(normalized.graphNativeLayoutThresholdEdges, 1);
|
||||
assert.equal(normalized.graphNativeLayoutWorkerTimeoutMs, 40);
|
||||
assert.equal(normalized.nativeEngineFailOpen, false);
|
||||
assert.equal(normalized.graphNativeForceDisable, true);
|
||||
|
||||
const runBridge = new GraphNativeLayoutBridge({
|
||||
graphUseNativeLayout: true,
|
||||
graphNativeLayoutThresholdNodes: 100,
|
||||
graphNativeLayoutThresholdEdges: 200,
|
||||
});
|
||||
|
||||
assert.equal(runBridge.shouldRunForGraph(99, 199), false);
|
||||
assert.equal(runBridge.shouldRunForGraph(100, 10), true);
|
||||
assert.equal(runBridge.shouldRunForGraph(10, 200), true);
|
||||
|
||||
runBridge._ensureWorker = () => null;
|
||||
runBridge._workerBootError = "forced-unavailable";
|
||||
const failOpenResult = await runBridge.solveLayout({ nodes: [] });
|
||||
assert.equal(failOpenResult.ok, false);
|
||||
assert.equal(failOpenResult.skipped, true);
|
||||
assert.equal(failOpenResult.reason, "worker-unavailable");
|
||||
assert.equal(failOpenResult.error, "forced-unavailable");
|
||||
|
||||
const strictBridge = new GraphNativeLayoutBridge({
|
||||
graphUseNativeLayout: true,
|
||||
nativeEngineFailOpen: false,
|
||||
});
|
||||
strictBridge._ensureWorker = () => null;
|
||||
strictBridge._workerBootError = "forced-hard-failure";
|
||||
let strictThrew = false;
|
||||
try {
|
||||
await strictBridge.solveLayout({ nodes: [] });
|
||||
} catch (error) {
|
||||
strictThrew = String(error?.message || "") === "forced-hard-failure";
|
||||
}
|
||||
assert.equal(strictThrew, true);
|
||||
|
||||
const cancelBridge = new GraphNativeLayoutBridge({ graphUseNativeLayout: true });
|
||||
let postMessages = [];
|
||||
cancelBridge._worker = {
|
||||
postMessage(message) {
|
||||
postMessages.push(message);
|
||||
},
|
||||
};
|
||||
let canceledResolveCount = 0;
|
||||
cancelBridge._pendingJobs.set(11, {
|
||||
timer: setTimeout(() => {}, 5000),
|
||||
resolve(result) {
|
||||
canceledResolveCount += 1;
|
||||
assert.equal(result.reason, "manual-cancel");
|
||||
},
|
||||
});
|
||||
cancelBridge.cancelPending("manual-cancel");
|
||||
assert.equal(canceledResolveCount, 1);
|
||||
assert.equal(cancelBridge._pendingJobs.size, 0);
|
||||
assert.deepEqual(postMessages, [
|
||||
{ type: "cancel-layout", jobId: 11, reason: "manual-cancel" },
|
||||
]);
|
||||
|
||||
const disposedBridge = new GraphNativeLayoutBridge({ graphUseNativeLayout: true });
|
||||
disposedBridge.dispose();
|
||||
const disposedResult = await disposedBridge.solveLayout({ nodes: [] });
|
||||
assert.equal(disposedResult.reason, "bridge-disposed");
|
||||
|
||||
console.log("graph-native-bridge tests passed");
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
buildGraphFromSnapshot,
|
||||
buildPersistDelta,
|
||||
buildSnapshotFromGraph,
|
||||
evaluatePersistNativeDeltaGate,
|
||||
} from "../sync/bme-db.js";
|
||||
import { onMessageReceivedController } from "../host/event-binding.js";
|
||||
import {
|
||||
@@ -880,6 +881,7 @@ async function createGraphPersistenceHarness({
|
||||
buildGraphFromSnapshot,
|
||||
buildPersistDelta,
|
||||
buildSnapshotFromGraph,
|
||||
evaluatePersistNativeDeltaGate,
|
||||
buildBmeDbName,
|
||||
scheduleUpload() {
|
||||
if (runtimeContext.__scheduleUploadShouldThrow) {
|
||||
@@ -2619,6 +2621,12 @@ result = {
|
||||
7,
|
||||
"附属步骤失败时,IndexedDB 主写仍应视为成功",
|
||||
);
|
||||
const persistDeltaDiagnostics = harness.api.getGraphPersistenceState().persistDelta;
|
||||
assert.equal(Boolean(persistDeltaDiagnostics), true);
|
||||
assert.equal(persistDeltaDiagnostics.status, "committed");
|
||||
assert.equal(persistDeltaDiagnostics.path, "js");
|
||||
assert.equal(persistDeltaDiagnostics.requestedNative, false);
|
||||
assert.equal(Number.isFinite(Number(persistDeltaDiagnostics.buildMs)), true);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
112
tests/native-layout-parity.mjs
Normal file
112
tests/native-layout-parity.mjs
Normal file
@@ -0,0 +1,112 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { solveLayoutWithJs } from "../ui/graph-layout-solver.js";
|
||||
import {
|
||||
getNativeModuleStatus,
|
||||
solveLayout,
|
||||
} from "../vendor/wasm/stbme_core.js";
|
||||
|
||||
const originalLoader = globalThis.__stBmeLoadRustWasmLayout;
|
||||
|
||||
function buildPayload(seed = 42, nodeCount = 180) {
|
||||
let state = seed;
|
||||
const rand = () => {
|
||||
state = (state * 1664525 + 1013904223) >>> 0;
|
||||
return state / 0xffffffff;
|
||||
};
|
||||
|
||||
const regionRect = { x: 0, y: 0, w: 980, h: 620 };
|
||||
const nodes = [];
|
||||
for (let i = 0; i < nodeCount; i++) {
|
||||
nodes.push({
|
||||
x: regionRect.x + rand() * regionRect.w,
|
||||
y: regionRect.y + rand() * regionRect.h,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
pinned: false,
|
||||
radius: 6 + rand() * 8,
|
||||
regionKey: "objective",
|
||||
regionRect,
|
||||
});
|
||||
}
|
||||
|
||||
const edges = [];
|
||||
const edgeTarget = Math.max(nodeCount * 4, 1);
|
||||
for (let i = 0; i < edgeTarget; i++) {
|
||||
const from = Math.floor(rand() * nodeCount);
|
||||
let to = Math.floor(rand() * nodeCount);
|
||||
if (to === from) to = (to + 1) % nodeCount;
|
||||
edges.push({
|
||||
from,
|
||||
to,
|
||||
strength: 0.25 + rand() * 0.75,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
config: {
|
||||
iterations: 52,
|
||||
repulsion: 2600,
|
||||
springK: 0.05,
|
||||
damping: 0.87,
|
||||
centerGravity: 0.015,
|
||||
minGap: 11,
|
||||
speedCap: 3.2,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function meanAbsDiff(a = new Float32Array(0), b = new Float32Array(0)) {
|
||||
const len = Math.min(a.length, b.length);
|
||||
if (len === 0) return 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < len; i++) {
|
||||
sum += Math.abs(a[i] - b[i]);
|
||||
}
|
||||
return sum / len;
|
||||
}
|
||||
|
||||
try {
|
||||
globalThis.__stBmeLoadRustWasmLayout = async () => ({
|
||||
solve_layout(payload) {
|
||||
const jsResult = solveLayoutWithJs(payload);
|
||||
return {
|
||||
ok: true,
|
||||
positions: Array.from(jsResult.positions),
|
||||
diagnostics: {
|
||||
solver: "mock-rust-wasm",
|
||||
nodeCount: jsResult.diagnostics.nodeCount,
|
||||
edgeCount: jsResult.diagnostics.edgeCount,
|
||||
iterations: jsResult.diagnostics.iterations,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const payload = buildPayload();
|
||||
const jsResult = solveLayoutWithJs(payload);
|
||||
const nativeResult = await solveLayout(payload);
|
||||
|
||||
assert.equal(jsResult.ok, true);
|
||||
assert.equal(nativeResult.ok, true);
|
||||
assert.ok(nativeResult.positions instanceof Float32Array);
|
||||
assert.equal(jsResult.positions.length, nativeResult.positions.length);
|
||||
|
||||
const mad = meanAbsDiff(jsResult.positions, nativeResult.positions);
|
||||
const nativeStatus = getNativeModuleStatus();
|
||||
const threshold = nativeStatus.source === "wasm-pack-artifact" ? 2e-4 : 1e-6;
|
||||
assert.ok(
|
||||
mad <= threshold,
|
||||
`mean abs diff too high: ${mad} (source=${nativeStatus.source || "unknown"}, threshold=${threshold})`,
|
||||
);
|
||||
} finally {
|
||||
if (typeof originalLoader === "function") {
|
||||
globalThis.__stBmeLoadRustWasmLayout = originalLoader;
|
||||
} else {
|
||||
delete globalThis.__stBmeLoadRustWasmLayout;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("native-layout-parity tests passed");
|
||||
116
tests/native-layout-wrapper.mjs
Normal file
116
tests/native-layout-wrapper.mjs
Normal file
@@ -0,0 +1,116 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
function importFreshWrapper(tag = "default") {
|
||||
return import(`../vendor/wasm/stbme_core.js?test=${Date.now()}-${tag}`);
|
||||
}
|
||||
|
||||
const originalLoader = globalThis.__stBmeLoadRustWasmLayout;
|
||||
|
||||
try {
|
||||
globalThis.__stBmeDisableWasmPackArtifacts = true;
|
||||
globalThis.__stBmeLoadRustWasmLayout = async () => ({
|
||||
solve_layout(payload = {}) {
|
||||
const nodeCount = Array.isArray(payload?.nodes) ? payload.nodes.length : 0;
|
||||
return {
|
||||
ok: true,
|
||||
positions: [1, 2, 3, 4],
|
||||
diagnostics: {
|
||||
solver: "mock-loader",
|
||||
nodeCount,
|
||||
edgeCount: 0,
|
||||
iterations: 1,
|
||||
},
|
||||
};
|
||||
},
|
||||
build_persist_delta_compact(payload = {}) {
|
||||
return {
|
||||
upsertNodeIds: Array.isArray(payload?.afterNodes?.ids)
|
||||
? payload.afterNodes.ids.slice(0, 1)
|
||||
: [],
|
||||
upsertEdgeIds: [],
|
||||
deleteNodeIds: [],
|
||||
deleteEdgeIds: [],
|
||||
upsertTombstoneIds: [],
|
||||
};
|
||||
},
|
||||
build_persist_delta(payload = {}) {
|
||||
return {
|
||||
upsertNodes: [{ id: "persist-native-node", marker: payload?.afterSnapshot?.meta?.chatId || "" }],
|
||||
upsertEdges: [],
|
||||
deleteNodeIds: [],
|
||||
deleteEdgeIds: [],
|
||||
tombstones: [],
|
||||
runtimeMetaPatch: { native: true },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = await importFreshWrapper("global-loader");
|
||||
const result = await wrapper.solveLayout({ nodes: [{}, {}], edges: [] });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.usedNative, true);
|
||||
assert.ok(result.positions instanceof Float32Array);
|
||||
assert.deepEqual(Array.from(result.positions), [1, 2, 3, 4]);
|
||||
assert.equal(result.diagnostics.solver, "mock-loader");
|
||||
|
||||
const status = wrapper.getNativeModuleStatus();
|
||||
assert.equal(status.loaded, true);
|
||||
assert.equal(status.source, "global-loader");
|
||||
|
||||
const installStatus = await wrapper.installNativePersistDeltaHook();
|
||||
assert.equal(installStatus.loaded, true);
|
||||
assert.equal(typeof globalThis.__stBmeNativeBuildPersistDelta, "function");
|
||||
const compactDeltaResult = globalThis.__stBmeNativeBuildPersistDelta(
|
||||
{ meta: { chatId: "before-chat" }, nodes: [], edges: [], tombstones: [], state: {} },
|
||||
{ meta: { chatId: "after-chat" }, nodes: [], edges: [], tombstones: [], state: {} },
|
||||
{
|
||||
nowMs: 123,
|
||||
preparedDeltaInput: {
|
||||
afterNodes: { ids: ["persist-native-node"], serialized: ["{}"] },
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.deepEqual(compactDeltaResult.upsertNodeIds, ["persist-native-node"]);
|
||||
|
||||
const deltaResult = globalThis.__stBmeNativeBuildPersistDelta(
|
||||
{ meta: { chatId: "before-chat" }, nodes: [], edges: [], tombstones: [], state: {} },
|
||||
{ meta: { chatId: "after-chat" }, nodes: [], edges: [], tombstones: [], state: {} },
|
||||
{ nowMs: 123 },
|
||||
);
|
||||
assert.deepEqual(deltaResult.upsertNodes, [{ id: "persist-native-node", marker: "after-chat" }]);
|
||||
assert.equal(deltaResult.runtimeMetaPatch.native, true);
|
||||
|
||||
delete globalThis.__stBmeLoadRustWasmLayout;
|
||||
delete globalThis.__stBmeNativeBuildPersistDelta;
|
||||
delete globalThis.__stBmeDisableWasmPackArtifacts;
|
||||
|
||||
const wrapperNoLoader = await importFreshWrapper("no-loader");
|
||||
let rejected = false;
|
||||
try {
|
||||
const resultNoLoader = await wrapperNoLoader.solveLayout({
|
||||
nodes: [{ x: 0, y: 0, vx: 0, vy: 0, pinned: false, radius: 8, regionKey: "objective", regionRect: { x: 0, y: 0, w: 200, h: 120 } }],
|
||||
edges: [],
|
||||
});
|
||||
assert.equal(resultNoLoader.ok, true);
|
||||
assert.equal(resultNoLoader.usedNative, true);
|
||||
const statusNoLoader = wrapperNoLoader.getNativeModuleStatus();
|
||||
assert.equal(statusNoLoader.loaded, true);
|
||||
assert.equal(statusNoLoader.source, "wasm-pack-artifact");
|
||||
} catch (error) {
|
||||
rejected = true;
|
||||
assert.match(
|
||||
String(error?.message || ""),
|
||||
/Rust\/WASM artifact is not initialized|native module unavailable|wasm-pack load error/i,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (typeof originalLoader === "function") {
|
||||
globalThis.__stBmeLoadRustWasmLayout = originalLoader;
|
||||
} else {
|
||||
delete globalThis.__stBmeLoadRustWasmLayout;
|
||||
}
|
||||
delete globalThis.__stBmeDisableWasmPackArtifacts;
|
||||
delete globalThis.__stBmeNativeBuildPersistDelta;
|
||||
}
|
||||
|
||||
console.log("native-layout-wrapper tests passed");
|
||||
187
tests/native-persist-delta-hook.mjs
Normal file
187
tests/native-persist-delta-hook.mjs
Normal file
@@ -0,0 +1,187 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
buildPersistDelta,
|
||||
evaluatePersistNativeDeltaGate,
|
||||
resolvePersistNativeDeltaGateOptions,
|
||||
shouldUseNativePersistDeltaForSnapshots,
|
||||
} from "../sync/bme-db.js";
|
||||
|
||||
const beforeSnapshot = {
|
||||
meta: { chatId: "chat-native", revision: 1, lastModified: 1 },
|
||||
state: { lastProcessedFloor: 1, extractionCount: 1 },
|
||||
nodes: [{ id: "n1", type: "event", fields: { text: "before" }, updatedAt: 1 }],
|
||||
edges: [],
|
||||
tombstones: [],
|
||||
};
|
||||
|
||||
const afterSnapshot = {
|
||||
meta: { chatId: "chat-native", revision: 2, lastModified: 2 },
|
||||
state: { lastProcessedFloor: 2, extractionCount: 2 },
|
||||
nodes: [{ id: "n1", type: "event", fields: { text: "after" }, updatedAt: 2 }],
|
||||
edges: [],
|
||||
tombstones: [],
|
||||
};
|
||||
|
||||
let fallbackDiagnostics = null;
|
||||
const fallbackDelta = buildPersistDelta(beforeSnapshot, afterSnapshot, {
|
||||
onDiagnostics(snapshot) {
|
||||
fallbackDiagnostics = snapshot;
|
||||
},
|
||||
});
|
||||
assert.equal(fallbackDelta.upsertNodes.length, 1);
|
||||
assert.equal(fallbackDelta.deleteNodeIds.length, 0);
|
||||
assert.equal(fallbackDiagnostics.path, "js");
|
||||
assert.equal(fallbackDiagnostics.requestedNative, false);
|
||||
assert.equal(fallbackDiagnostics.usedNative, false);
|
||||
assert.equal(Number.isFinite(fallbackDiagnostics.buildMs), true);
|
||||
|
||||
const defaultGate = resolvePersistNativeDeltaGateOptions({});
|
||||
assert.equal(defaultGate.minSnapshotRecords, 20000);
|
||||
assert.equal(defaultGate.minStructuralDelta, 600);
|
||||
assert.equal(defaultGate.minCombinedSerializedChars, 4000000);
|
||||
assert.equal(
|
||||
shouldUseNativePersistDeltaForSnapshots(beforeSnapshot, afterSnapshot, defaultGate),
|
||||
false,
|
||||
);
|
||||
const payloadGate = evaluatePersistNativeDeltaGate(beforeSnapshot, afterSnapshot, {
|
||||
minSnapshotRecords: 0,
|
||||
minStructuralDelta: 0,
|
||||
minCombinedSerializedChars: 200,
|
||||
measuredCombinedSerializedChars: 120,
|
||||
});
|
||||
assert.equal(payloadGate.allowed, false);
|
||||
assert.deepEqual(payloadGate.reasons, ["below-serialized-chars-threshold"]);
|
||||
assert.equal(
|
||||
shouldUseNativePersistDeltaForSnapshots(beforeSnapshot, afterSnapshot, {
|
||||
minSnapshotRecords: 1,
|
||||
minStructuralDelta: 0,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
const largeBeforeSnapshot = {
|
||||
nodes: new Array(20500).fill(0),
|
||||
edges: new Array(200).fill(0),
|
||||
tombstones: [],
|
||||
};
|
||||
const largeAfterSnapshot = {
|
||||
nodes: new Array(21120).fill(0),
|
||||
edges: new Array(200).fill(0),
|
||||
tombstones: [],
|
||||
};
|
||||
assert.equal(
|
||||
shouldUseNativePersistDeltaForSnapshots(
|
||||
largeBeforeSnapshot,
|
||||
largeAfterSnapshot,
|
||||
defaultGate,
|
||||
),
|
||||
true,
|
||||
);
|
||||
|
||||
const originalNativeBuilder = globalThis.__stBmeNativeBuildPersistDelta;
|
||||
|
||||
globalThis.__stBmeNativeBuildPersistDelta = () => ({
|
||||
upsertNodes: [{ id: "native-node" }],
|
||||
upsertEdges: [{ id: "native-edge" }],
|
||||
deleteNodeIds: ["native-delete-node"],
|
||||
deleteEdgeIds: ["native-delete-edge"],
|
||||
tombstones: [{ id: "node:native-delete-node", kind: "node", targetId: "native-delete-node" }],
|
||||
runtimeMetaPatch: { native: true },
|
||||
});
|
||||
|
||||
let nativeDiagnostics = null;
|
||||
const nativeDelta = buildPersistDelta(beforeSnapshot, afterSnapshot, {
|
||||
useNativeDelta: true,
|
||||
minSnapshotRecords: 0,
|
||||
minStructuralDelta: 0,
|
||||
minCombinedSerializedChars: 0,
|
||||
runtimeMetaPatch: { jsPatch: true },
|
||||
onDiagnostics(snapshot) {
|
||||
nativeDiagnostics = snapshot;
|
||||
},
|
||||
});
|
||||
assert.deepEqual(nativeDelta.upsertNodes, [{ id: "native-node" }]);
|
||||
assert.deepEqual(nativeDelta.deleteNodeIds, ["native-delete-node"]);
|
||||
assert.equal(nativeDelta.runtimeMetaPatch.native, true);
|
||||
assert.equal(nativeDelta.runtimeMetaPatch.jsPatch, true);
|
||||
assert.equal(nativeDiagnostics.path, "native-full");
|
||||
assert.equal(nativeDiagnostics.requestedNative, true);
|
||||
assert.equal(nativeDiagnostics.usedNative, true);
|
||||
|
||||
let payloadGateDiagnostics = null;
|
||||
let payloadGateBuilderCalled = false;
|
||||
globalThis.__stBmeNativeBuildPersistDelta = () => {
|
||||
payloadGateBuilderCalled = true;
|
||||
return { upsertNodes: [] };
|
||||
};
|
||||
const payloadGatedDelta = buildPersistDelta(beforeSnapshot, afterSnapshot, {
|
||||
useNativeDelta: true,
|
||||
minSnapshotRecords: 0,
|
||||
minStructuralDelta: 0,
|
||||
minCombinedSerializedChars: 1000,
|
||||
onDiagnostics(snapshot) {
|
||||
payloadGateDiagnostics = snapshot;
|
||||
},
|
||||
});
|
||||
assert.equal(payloadGateBuilderCalled, false);
|
||||
assert.equal(payloadGatedDelta.upsertNodes.length, 1);
|
||||
assert.equal(payloadGateDiagnostics.path, "js");
|
||||
assert.equal(payloadGateDiagnostics.nativeAttemptStatus, "gated-out");
|
||||
assert.equal(payloadGateDiagnostics.gateAllowed, false);
|
||||
assert.deepEqual(payloadGateDiagnostics.gateReasons, ["below-serialized-chars-threshold"]);
|
||||
|
||||
globalThis.__stBmeNativeBuildPersistDelta = (_before, _after, options = {}) => {
|
||||
assert.equal(Boolean(options?.preparedDeltaInput), true);
|
||||
return {
|
||||
upsertNodeIds: ["n1"],
|
||||
upsertEdgeIds: [],
|
||||
deleteNodeIds: [],
|
||||
deleteEdgeIds: [],
|
||||
upsertTombstoneIds: [],
|
||||
};
|
||||
};
|
||||
|
||||
let compactDiagnostics = null;
|
||||
const compactNativeDelta = buildPersistDelta(beforeSnapshot, afterSnapshot, {
|
||||
useNativeDelta: true,
|
||||
minSnapshotRecords: 0,
|
||||
minStructuralDelta: 0,
|
||||
minCombinedSerializedChars: 0,
|
||||
runtimeMetaPatch: { compact: true },
|
||||
onDiagnostics(snapshot) {
|
||||
compactDiagnostics = snapshot;
|
||||
},
|
||||
});
|
||||
assert.deepEqual(compactNativeDelta.upsertNodes, [
|
||||
{ id: "n1", type: "event", fields: { text: "after" }, updatedAt: 2 },
|
||||
]);
|
||||
assert.deepEqual(compactNativeDelta.upsertEdges, []);
|
||||
assert.deepEqual(compactNativeDelta.deleteNodeIds, []);
|
||||
assert.equal(compactNativeDelta.runtimeMetaPatch.compact, true);
|
||||
assert.equal(compactNativeDelta.runtimeMetaPatch.chatId, "chat-native");
|
||||
assert.equal(compactDiagnostics.path, "native-compact");
|
||||
assert.equal(compactDiagnostics.usedNative, true);
|
||||
|
||||
delete globalThis.__stBmeNativeBuildPersistDelta;
|
||||
|
||||
let threwUnavailable = false;
|
||||
try {
|
||||
buildPersistDelta(beforeSnapshot, afterSnapshot, {
|
||||
useNativeDelta: true,
|
||||
minSnapshotRecords: 0,
|
||||
minStructuralDelta: 0,
|
||||
minCombinedSerializedChars: 0,
|
||||
nativeFailOpen: false,
|
||||
});
|
||||
} catch (error) {
|
||||
threwUnavailable =
|
||||
String(error?.message || "") === "native-persist-delta-builder-unavailable";
|
||||
}
|
||||
assert.equal(threwUnavailable, true);
|
||||
|
||||
if (typeof originalNativeBuilder === "function") {
|
||||
globalThis.__stBmeNativeBuildPersistDelta = originalNativeBuilder;
|
||||
}
|
||||
|
||||
console.log("native-persist-delta-hook tests passed");
|
||||
158
tests/perf/graph-layout-bench.mjs
Normal file
158
tests/perf/graph-layout-bench.mjs
Normal file
@@ -0,0 +1,158 @@
|
||||
import { performance } from "node:perf_hooks";
|
||||
|
||||
import { solveLayoutWithJs } from "../../ui/graph-layout-solver.js";
|
||||
import {
|
||||
getNativeModuleStatus,
|
||||
solveLayout as solveNativeLayout,
|
||||
} from "../../vendor/wasm/stbme_core.js";
|
||||
|
||||
const SCALES = [
|
||||
{ nodes: 600, edgeMultiplier: 3 },
|
||||
{ nodes: 1200, edgeMultiplier: 4 },
|
||||
{ nodes: 2000, edgeMultiplier: 4 },
|
||||
];
|
||||
|
||||
const RUNS = 3;
|
||||
|
||||
function buildPayload(seed = 7, nodeCount = 600, edgeMultiplier = 4) {
|
||||
let state = seed >>> 0;
|
||||
const rand = () => {
|
||||
state = (state * 1664525 + 1013904223) >>> 0;
|
||||
return state / 0xffffffff;
|
||||
};
|
||||
|
||||
const regionRect = { x: 0, y: 0, w: 1280, h: 780 };
|
||||
const nodes = new Array(nodeCount).fill(null).map(() => ({
|
||||
x: regionRect.x + rand() * regionRect.w,
|
||||
y: regionRect.y + rand() * regionRect.h,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
pinned: false,
|
||||
radius: 5.5 + rand() * 8,
|
||||
regionKey: "objective",
|
||||
regionRect,
|
||||
}));
|
||||
|
||||
const edgeCount = Math.max(1, Math.floor(nodeCount * edgeMultiplier));
|
||||
const edges = [];
|
||||
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;
|
||||
edges.push({
|
||||
from,
|
||||
to,
|
||||
strength: 0.25 + rand() * 0.75,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
config: {
|
||||
iterations: 56,
|
||||
repulsion: 2600,
|
||||
springK: 0.05,
|
||||
damping: 0.87,
|
||||
centerGravity: 0.015,
|
||||
minGap: 11,
|
||||
speedCap: 3.2,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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],
|
||||
};
|
||||
}
|
||||
|
||||
async function runNative(payload) {
|
||||
const start = performance.now();
|
||||
const result = await solveNativeLayout(payload);
|
||||
const elapsed = performance.now() - start;
|
||||
return { elapsed, result };
|
||||
}
|
||||
|
||||
function runJs(payload) {
|
||||
const start = performance.now();
|
||||
const result = solveLayoutWithJs(payload);
|
||||
const elapsed = performance.now() - start;
|
||||
return { elapsed, result };
|
||||
}
|
||||
|
||||
async function warmUp() {
|
||||
const payload = buildPayload(12345, 320, 3);
|
||||
runJs(payload);
|
||||
await runNative(payload);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const originalLoader = globalThis.__stBmeLoadRustWasmLayout;
|
||||
if (typeof originalLoader !== "function") {
|
||||
globalThis.__stBmeLoadRustWasmLayout = async () => ({
|
||||
solve_layout(payload) {
|
||||
const jsResult = solveLayoutWithJs(payload);
|
||||
return {
|
||||
ok: true,
|
||||
positions: Array.from(jsResult.positions),
|
||||
diagnostics: {
|
||||
solver: "mock-rust-wasm",
|
||||
nodeCount: jsResult.diagnostics.nodeCount,
|
||||
edgeCount: jsResult.diagnostics.edgeCount,
|
||||
iterations: jsResult.diagnostics.iterations,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await warmUp();
|
||||
const nativeStatus = getNativeModuleStatus();
|
||||
console.log(`[ST-BME][bench] graph-layout runs=${RUNS}`);
|
||||
console.log(
|
||||
`[ST-BME][bench] graph-layout native-source=${nativeStatus.source || "unknown"}`,
|
||||
);
|
||||
for (const scale of SCALES) {
|
||||
const jsTimes = [];
|
||||
const nativeTimes = [];
|
||||
for (let run = 0; run < RUNS; run++) {
|
||||
const payload = buildPayload(
|
||||
scale.nodes * 31 + run,
|
||||
scale.nodes,
|
||||
scale.edgeMultiplier,
|
||||
);
|
||||
const js = runJs(payload);
|
||||
jsTimes.push(js.elapsed);
|
||||
|
||||
const native = await runNative(payload);
|
||||
nativeTimes.push(native.elapsed);
|
||||
}
|
||||
|
||||
const jsSummary = summarize(jsTimes);
|
||||
const nativeSummary = summarize(nativeTimes);
|
||||
console.log(
|
||||
`[ST-BME][bench] nodes=${scale.nodes} edges≈${Math.floor(scale.nodes * scale.edgeMultiplier)} | js avg=${jsSummary.avg.toFixed(2)}ms p95=${jsSummary.p95.toFixed(2)}ms | native avg=${nativeSummary.avg.toFixed(2)}ms p95=${nativeSummary.p95.toFixed(2)}ms`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (typeof originalLoader === "function") {
|
||||
globalThis.__stBmeLoadRustWasmLayout = originalLoader;
|
||||
} else {
|
||||
delete globalThis.__stBmeLoadRustWasmLayout;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("[ST-BME][bench] graph-layout failed:", error?.message || String(error));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
161
tests/perf/persist-delta-bench.mjs
Normal file
161
tests/perf/persist-delta-bench.mjs
Normal file
@@ -0,0 +1,161 @@
|
||||
import { performance } from "node:perf_hooks";
|
||||
|
||||
import { buildPersistDelta } 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 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],
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await installNativePersistDeltaHook();
|
||||
const nativeStatus = getNativeModuleStatus();
|
||||
const jsSamples = [];
|
||||
const nativeSamples = [];
|
||||
for (let run = 0; run < RUNS; run++) {
|
||||
const snapshots = buildSnapshots(17 + run, 5000, 12000, 0.12);
|
||||
const jsStartedAt = performance.now();
|
||||
const jsDelta = buildPersistDelta(snapshots.before, snapshots.after, {
|
||||
useNativeDelta: false,
|
||||
});
|
||||
const jsElapsedMs = performance.now() - jsStartedAt;
|
||||
jsSamples.push({
|
||||
elapsedMs: jsElapsedMs,
|
||||
upsertNodes: jsDelta.upsertNodes.length,
|
||||
upsertEdges: jsDelta.upsertEdges.length,
|
||||
deleteNodeIds: jsDelta.deleteNodeIds.length,
|
||||
deleteEdgeIds: jsDelta.deleteEdgeIds.length,
|
||||
});
|
||||
|
||||
const nativeStartedAt = performance.now();
|
||||
const nativeDelta = buildPersistDelta(snapshots.before, snapshots.after, {
|
||||
useNativeDelta: true,
|
||||
minSnapshotRecords: 0,
|
||||
minStructuralDelta: 0,
|
||||
minCombinedSerializedChars: 0,
|
||||
nativeFailOpen: false,
|
||||
});
|
||||
const nativeElapsedMs = performance.now() - nativeStartedAt;
|
||||
nativeSamples.push({
|
||||
elapsedMs: nativeElapsedMs,
|
||||
upsertNodes: nativeDelta.upsertNodes.length,
|
||||
upsertEdges: nativeDelta.upsertEdges.length,
|
||||
deleteNodeIds: nativeDelta.deleteNodeIds.length,
|
||||
deleteEdgeIds: nativeDelta.deleteEdgeIds.length,
|
||||
});
|
||||
}
|
||||
|
||||
const jsTimingSummary = summarize(jsSamples.map((sample) => sample.elapsedMs));
|
||||
const nativeTimingSummary = summarize(nativeSamples.map((sample) => sample.elapsedMs));
|
||||
const avgUpserts =
|
||||
jsSamples.reduce((acc, sample) => acc + sample.upsertNodes + sample.upsertEdges, 0) /
|
||||
jsSamples.length;
|
||||
const avgDeletes =
|
||||
jsSamples.reduce((acc, sample) => acc + sample.deleteNodeIds + sample.deleteEdgeIds, 0) /
|
||||
jsSamples.length;
|
||||
|
||||
console.log(
|
||||
`[ST-BME][bench] persist-delta native-source=${nativeStatus.source || "unknown"}`,
|
||||
);
|
||||
console.log(
|
||||
`[ST-BME][bench] persist-delta runs=${RUNS} | js avg=${jsTimingSummary.avg.toFixed(2)}ms p95=${jsTimingSummary.p95.toFixed(2)}ms min=${jsTimingSummary.min.toFixed(2)}ms max=${jsTimingSummary.max.toFixed(2)}ms | native avg=${nativeTimingSummary.avg.toFixed(2)}ms p95=${nativeTimingSummary.p95.toFixed(2)}ms min=${nativeTimingSummary.min.toFixed(2)}ms max=${nativeTimingSummary.max.toFixed(2)}ms | avgUpserts=${avgUpserts.toFixed(1)} avgDeletes=${avgDeletes.toFixed(1)}`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("[ST-BME][bench] persist-delta failed:", error?.message || String(error));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user