mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
perf: complete persist-load P2 hydration pass
This commit is contained in:
@@ -58,6 +58,48 @@ function normalizeStringArray(values = []) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAlreadyNormalizedStringArray(values = []) {
|
||||||
|
if (!Array.isArray(values)) return false;
|
||||||
|
const seen = new Set();
|
||||||
|
for (const value of values) {
|
||||||
|
if (typeof value !== "string") return false;
|
||||||
|
const normalized = normalizeString(value);
|
||||||
|
const key = normalizeKey(normalized);
|
||||||
|
if (!normalized || normalized !== value || seen.has(key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canReuseNormalizedMemoryScope(scope = {}, defaults = {}) {
|
||||||
|
if (
|
||||||
|
!scope ||
|
||||||
|
typeof scope !== "object" ||
|
||||||
|
Array.isArray(scope) ||
|
||||||
|
(defaults && typeof defaults === "object" && Object.keys(defaults).length > 0)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const layer = normalizeLayer(scope.layer);
|
||||||
|
const ownerType = normalizeOwnerType(layer, normalizeString(scope.ownerType));
|
||||||
|
const ownerId = ownerType
|
||||||
|
? normalizeString(scope.ownerId || scope.ownerName)
|
||||||
|
: "";
|
||||||
|
const ownerName = ownerType ? normalizeString(scope.ownerName) : "";
|
||||||
|
const regionPrimary = normalizeString(scope.regionPrimary);
|
||||||
|
return (
|
||||||
|
scope.layer === layer &&
|
||||||
|
normalizeString(scope.ownerType) === ownerType &&
|
||||||
|
normalizeString(scope.ownerId || "") === ownerId &&
|
||||||
|
normalizeString(scope.ownerName || "") === ownerName &&
|
||||||
|
normalizeString(scope.regionPrimary || "") === regionPrimary &&
|
||||||
|
isAlreadyNormalizedStringArray(scope.regionPath) &&
|
||||||
|
isAlreadyNormalizedStringArray(scope.regionSecondary)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeOwnerValueSet(values = []) {
|
function normalizeOwnerValueSet(values = []) {
|
||||||
return new Set(
|
return new Set(
|
||||||
normalizeStringArray(values).map((value) => normalizeKey(value)),
|
normalizeStringArray(values).map((value) => normalizeKey(value)),
|
||||||
@@ -88,6 +130,9 @@ export function createDefaultMemoryScope(overrides = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeMemoryScope(scope = {}, defaults = {}) {
|
export function normalizeMemoryScope(scope = {}, defaults = {}) {
|
||||||
|
if (canReuseNormalizedMemoryScope(scope, defaults)) {
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
const merged = {
|
const merged = {
|
||||||
...DEFAULT_MEMORY_SCOPE,
|
...DEFAULT_MEMORY_SCOPE,
|
||||||
...(defaults || {}),
|
...(defaults || {}),
|
||||||
|
|||||||
@@ -147,7 +147,50 @@ export function createDefaultTimelineState(overrides = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canReuseNormalizedStoryTime(value = {}, defaults = {}) {
|
||||||
|
if (
|
||||||
|
!value ||
|
||||||
|
typeof value !== "object" ||
|
||||||
|
Array.isArray(value) ||
|
||||||
|
(defaults && typeof defaults === "object" && Object.keys(defaults).length > 0)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
normalizeString(value.segmentId || "") === String(value.segmentId || "") &&
|
||||||
|
normalizeString(value.label || "") === String(value.label || "") &&
|
||||||
|
normalizeEnum(value.tense, STORY_TENSE_VALUES, "unknown") === value.tense &&
|
||||||
|
normalizeEnum(value.relation, STORY_RELATION_VALUES, "unknown") === value.relation &&
|
||||||
|
normalizeString(value.anchorLabel || "") === String(value.anchorLabel || "") &&
|
||||||
|
normalizeEnum(value.confidence, STORY_CONFIDENCE_VALUES, "medium") ===
|
||||||
|
value.confidence &&
|
||||||
|
normalizeEnum(value.source, STORY_SOURCE_VALUES, "derived") === value.source
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function canReuseNormalizedStoryTimeSpan(value = {}, defaults = {}) {
|
||||||
|
if (
|
||||||
|
!value ||
|
||||||
|
typeof value !== "object" ||
|
||||||
|
Array.isArray(value) ||
|
||||||
|
(defaults && typeof defaults === "object" && Object.keys(defaults).length > 0)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
normalizeString(value.startSegmentId || "") === String(value.startSegmentId || "") &&
|
||||||
|
normalizeString(value.endSegmentId || "") === String(value.endSegmentId || "") &&
|
||||||
|
normalizeString(value.startLabel || "") === String(value.startLabel || "") &&
|
||||||
|
normalizeString(value.endLabel || "") === String(value.endLabel || "") &&
|
||||||
|
(value.mixed === true || value.mixed === false) &&
|
||||||
|
normalizeEnum(value.source, STORY_SOURCE_VALUES, "derived") === value.source
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeStoryTime(value = {}, defaults = {}) {
|
export function normalizeStoryTime(value = {}, defaults = {}) {
|
||||||
|
if (canReuseNormalizedStoryTime(value, defaults)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
return createDefaultStoryTime({
|
return createDefaultStoryTime({
|
||||||
...defaults,
|
...defaults,
|
||||||
...(value && typeof value === "object" && !Array.isArray(value) ? value : {}),
|
...(value && typeof value === "object" && !Array.isArray(value) ? value : {}),
|
||||||
@@ -155,6 +198,9 @@ export function normalizeStoryTime(value = {}, defaults = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeStoryTimeSpan(value = {}, defaults = {}) {
|
export function normalizeStoryTimeSpan(value = {}, defaults = {}) {
|
||||||
|
if (canReuseNormalizedStoryTimeSpan(value, defaults)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
return createDefaultStoryTimeSpan({
|
return createDefaultStoryTimeSpan({
|
||||||
...defaults,
|
...defaults,
|
||||||
...(value && typeof value === "object" && !Array.isArray(value) ? value : {}),
|
...(value && typeof value === "object" && !Array.isArray(value) ? value : {}),
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
"test:trivial-input": "node tests/trivial-user-input.mjs",
|
"test:trivial-input": "node tests/trivial-user-input.mjs",
|
||||||
"bench:graph-layout": "node tests/perf/graph-layout-bench.mjs",
|
"bench:graph-layout": "node tests/perf/graph-layout-bench.mjs",
|
||||||
"bench:persist-delta": "node tests/perf/persist-delta-bench.mjs",
|
"bench:persist-delta": "node tests/perf/persist-delta-bench.mjs",
|
||||||
|
"bench:persist-load": "node tests/perf/persist-load-bench.mjs",
|
||||||
|
"bench:load-preapply": "node tests/perf/load-preapply-bench.mjs",
|
||||||
|
"bench:p1-compare": "node scripts/compare-p1-bench.mjs",
|
||||||
"bench:native": "npm run bench:graph-layout && npm run bench:persist-delta",
|
"bench:native": "npm run bench:graph-layout && npm run bench:persist-delta",
|
||||||
"test:indexeddb": "npm run test:indexeddb-persistence && npm run test:indexeddb-sync && npm run test:indexeddb-migration",
|
"test:indexeddb": "npm run test:indexeddb-persistence && npm run test:indexeddb-sync && npm run test:indexeddb-migration",
|
||||||
"test:persistence-matrix": "npm run test:p0 && npm run test:runtime-history && npm run test:graph-persistence && npm run test:indexeddb",
|
"test:persistence-matrix": "npm run test:p0 && npm run test:runtime-history && npm run test:graph-persistence && npm run test:indexeddb",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "../graph/knowledge-state.js";
|
} from "../graph/knowledge-state.js";
|
||||||
import {
|
import {
|
||||||
createDefaultTimelineState,
|
createDefaultTimelineState,
|
||||||
|
normalizeTimelineState,
|
||||||
normalizeGraphStoryTimeline,
|
normalizeGraphStoryTimeline,
|
||||||
} from "../graph/story-timeline.js";
|
} from "../graph/story-timeline.js";
|
||||||
import {
|
import {
|
||||||
@@ -224,10 +225,12 @@ function getRequiredJournalCoverageStartFloor(graph, journals = []) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeGraphRuntimeState(graph, chatId = "") {
|
export function normalizeGraphRuntimeState(graph, chatId = "", options = {}) {
|
||||||
if (!graph || typeof graph !== "object") {
|
if (!graph || typeof graph !== "object") {
|
||||||
return graph;
|
return graph;
|
||||||
}
|
}
|
||||||
|
const skipRecordFieldNormalization =
|
||||||
|
options?.skipRecordFieldNormalization === true;
|
||||||
const hadSummaryState =
|
const hadSummaryState =
|
||||||
graph.summaryState &&
|
graph.summaryState &&
|
||||||
typeof graph.summaryState === "object" &&
|
typeof graph.summaryState === "object" &&
|
||||||
@@ -475,10 +478,10 @@ export function normalizeGraphRuntimeState(graph, chatId = "") {
|
|||||||
|
|
||||||
graph.historyState = historyState;
|
graph.historyState = historyState;
|
||||||
graph.vectorIndexState = vectorIndexState;
|
graph.vectorIndexState = vectorIndexState;
|
||||||
if (Array.isArray(graph.nodes)) {
|
if (!skipRecordFieldNormalization && Array.isArray(graph.nodes)) {
|
||||||
graph.nodes.forEach((node) => normalizeNodeMemoryScope(node));
|
graph.nodes.forEach((node) => normalizeNodeMemoryScope(node));
|
||||||
}
|
}
|
||||||
if (Array.isArray(graph.edges)) {
|
if (!skipRecordFieldNormalization && Array.isArray(graph.edges)) {
|
||||||
graph.edges.forEach((edge) => normalizeEdgeMemoryScope(edge));
|
graph.edges.forEach((edge) => normalizeEdgeMemoryScope(edge));
|
||||||
}
|
}
|
||||||
graph.batchJournal = Array.isArray(graph.batchJournal)
|
graph.batchJournal = Array.isArray(graph.batchJournal)
|
||||||
@@ -496,10 +499,16 @@ export function normalizeGraphRuntimeState(graph, chatId = "") {
|
|||||||
: createDefaultMaintenanceJournal();
|
: createDefaultMaintenanceJournal();
|
||||||
graph.knowledgeState = createDefaultKnowledgeState(graph.knowledgeState);
|
graph.knowledgeState = createDefaultKnowledgeState(graph.knowledgeState);
|
||||||
graph.regionState = createDefaultRegionState(graph.regionState);
|
graph.regionState = createDefaultRegionState(graph.regionState);
|
||||||
graph.timelineState = createDefaultTimelineState(graph.timelineState);
|
graph.timelineState = skipRecordFieldNormalization
|
||||||
|
? normalizeTimelineState(graph.timelineState)
|
||||||
|
: createDefaultTimelineState(graph.timelineState);
|
||||||
graph.summaryState = createDefaultSummaryState(graph.summaryState);
|
graph.summaryState = createDefaultSummaryState(graph.summaryState);
|
||||||
normalizeGraphCognitiveState(graph);
|
normalizeGraphCognitiveState(graph);
|
||||||
normalizeGraphStoryTimeline(graph);
|
if (skipRecordFieldNormalization) {
|
||||||
|
graph.timelineState = normalizeTimelineState(graph.timelineState);
|
||||||
|
} else {
|
||||||
|
normalizeGraphStoryTimeline(graph);
|
||||||
|
}
|
||||||
normalizeGraphSummaryState(graph);
|
normalizeGraphSummaryState(graph);
|
||||||
if (!hadSummaryState) {
|
if (!hadSummaryState) {
|
||||||
importLegacySynopsisToSummaryState(graph);
|
importLegacySynopsisToSummaryState(graph);
|
||||||
|
|||||||
196
scripts/compare-p1-bench.mjs
Normal file
196
scripts/compare-p1-bench.mjs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { execFile } from "node:child_process";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const projectRoot = path.resolve(scriptDir, "..");
|
||||||
|
const args = new Map(
|
||||||
|
process.argv.slice(2).map((entry) => {
|
||||||
|
const [key, ...rest] = String(entry || "").split("=");
|
||||||
|
return [key, rest.join("=") || true];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const baselineRef = String(args.get("--baseline") || "origin/main");
|
||||||
|
const currentRef = String(args.get("--current") || "HEAD");
|
||||||
|
const outputJson = args.has("--json");
|
||||||
|
|
||||||
|
async function runCommand(command, commandArgs, cwd) {
|
||||||
|
const { stdout, stderr } = await execFileAsync(command, commandArgs, {
|
||||||
|
cwd,
|
||||||
|
windowsHide: true,
|
||||||
|
maxBuffer: 1024 * 1024 * 20,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
ST_BME_NODE_MODULES_ROOT: projectRoot,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
stdout: String(stdout || "").trim(),
|
||||||
|
stderr: String(stderr || "").trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveRef(ref) {
|
||||||
|
const result = await runCommand("git", ["rev-parse", ref], projectRoot);
|
||||||
|
return result.stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureFileFromCurrentRepo(relativePath, targetRoot) {
|
||||||
|
const sourcePath = path.join(projectRoot, relativePath);
|
||||||
|
const targetPath = path.join(targetRoot, relativePath);
|
||||||
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
||||||
|
await fs.copyFile(sourcePath, targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonLine(stdout = "") {
|
||||||
|
const trimmed = String(stdout || "").trim();
|
||||||
|
const lines = trimmed.split(/\r?\n/).filter(Boolean);
|
||||||
|
return JSON.parse(lines[lines.length - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDelta(current = 0, baseline = 0) {
|
||||||
|
const delta = current - baseline;
|
||||||
|
const ratio = baseline !== 0 ? (delta / baseline) * 100 : 0;
|
||||||
|
const sign = delta > 0 ? "+" : "";
|
||||||
|
return `${sign}${delta.toFixed(2)}ms (${sign}${ratio.toFixed(1)}%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectMetricRows(compare, metricPath, label) {
|
||||||
|
return Object.entries(compare).map(([preset, metrics]) => ({
|
||||||
|
preset,
|
||||||
|
label,
|
||||||
|
baseline: Number(metricPath(metrics.baseline) || 0),
|
||||||
|
current: Number(metricPath(metrics.current) || 0),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function printRows(rows = [], title = "") {
|
||||||
|
console.log(`\n[ST-BME][P1-compare] ${title}`);
|
||||||
|
for (const row of rows) {
|
||||||
|
console.log(
|
||||||
|
`${row.preset} baseline=${row.baseline.toFixed(2)}ms current=${row.current.toFixed(2)}ms delta=${formatDelta(row.current, row.baseline)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runBenchSuite(cwd) {
|
||||||
|
const persistLoad = await runCommand(
|
||||||
|
process.execPath,
|
||||||
|
["tests/perf/persist-load-bench.mjs", "--json"],
|
||||||
|
cwd,
|
||||||
|
);
|
||||||
|
const loadPreapply = await runCommand(
|
||||||
|
process.execPath,
|
||||||
|
["tests/perf/load-preapply-bench.mjs", "--json"],
|
||||||
|
cwd,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
persistLoad: readJsonLine(persistLoad.stdout),
|
||||||
|
loadPreapply: readJsonLine(loadPreapply.stdout),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareBenchResults(baseline, current) {
|
||||||
|
const presets = {};
|
||||||
|
const presetNames = new Set([
|
||||||
|
...Object.keys(baseline.persistLoad?.presets || {}),
|
||||||
|
...Object.keys(current.persistLoad?.presets || {}),
|
||||||
|
...Object.keys(baseline.loadPreapply?.presets || {}),
|
||||||
|
...Object.keys(current.loadPreapply?.presets || {}),
|
||||||
|
]);
|
||||||
|
for (const preset of presetNames) {
|
||||||
|
presets[preset] = {
|
||||||
|
baseline: {
|
||||||
|
...(baseline.persistLoad?.presets?.[preset] || {}),
|
||||||
|
...(baseline.loadPreapply?.presets?.[preset] || {}),
|
||||||
|
},
|
||||||
|
current: {
|
||||||
|
...(current.persistLoad?.presets?.[preset] || {}),
|
||||||
|
...(current.loadPreapply?.presets?.[preset] || {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return presets;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createWorktree(ref, tempRoot, name) {
|
||||||
|
const worktreePath = path.join(tempRoot, name);
|
||||||
|
await runCommand("git", ["worktree", "add", "--detach", worktreePath, ref], projectRoot);
|
||||||
|
await ensureFileFromCurrentRepo("tests/perf/persist-load-bench.mjs", worktreePath);
|
||||||
|
await ensureFileFromCurrentRepo("tests/perf/load-preapply-bench.mjs", worktreePath);
|
||||||
|
await ensureFileFromCurrentRepo("tests/helpers/memory-opfs.mjs", worktreePath);
|
||||||
|
return worktreePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeWorktree(worktreePath) {
|
||||||
|
await runCommand("git", ["worktree", "remove", "--force", worktreePath], projectRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const baselineSha = await resolveRef(baselineRef);
|
||||||
|
const currentSha = await resolveRef(currentRef);
|
||||||
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "st-bme-p1-compare-"));
|
||||||
|
let baselinePath = "";
|
||||||
|
let currentPath = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
baselinePath = await createWorktree(baselineSha, tempRoot, "baseline");
|
||||||
|
currentPath =
|
||||||
|
currentRef === "HEAD" ? projectRoot : await createWorktree(currentSha, tempRoot, "current");
|
||||||
|
|
||||||
|
const baselineResults = await runBenchSuite(baselinePath);
|
||||||
|
const currentResults = await runBenchSuite(currentPath);
|
||||||
|
const compare = compareBenchResults(baselineResults, currentResults);
|
||||||
|
|
||||||
|
if (outputJson) {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
baselineRef,
|
||||||
|
baselineSha,
|
||||||
|
currentRef,
|
||||||
|
currentSha,
|
||||||
|
compare,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[ST-BME][P1-compare] baseline=${baselineRef} (${baselineSha.slice(0, 7)})`);
|
||||||
|
console.log(`[ST-BME][P1-compare] current=${currentRef} (${currentSha.slice(0, 7)})`);
|
||||||
|
|
||||||
|
printRows(
|
||||||
|
collectMetricRows(compare, (entry) => entry.opfsCommitMs?.p95, "opfsCommitMs.p95"),
|
||||||
|
"opfs commit p95",
|
||||||
|
);
|
||||||
|
printRows(
|
||||||
|
collectMetricRows(compare, (entry) => entry.indexedDbProbeRejectMs?.p95, "indexedDbProbeRejectMs.p95"),
|
||||||
|
"indexeddb probe-reject preApply p95",
|
||||||
|
);
|
||||||
|
printRows(
|
||||||
|
collectMetricRows(compare, (entry) => entry.opfsProbeRejectMs?.p95, "opfsProbeRejectMs.p95"),
|
||||||
|
"opfs probe-reject preApply p95",
|
||||||
|
);
|
||||||
|
printRows(
|
||||||
|
collectMetricRows(compare, (entry) => entry.indexedDbPreApplySuccessMs?.p95, "indexedDbPreApplySuccessMs.p95"),
|
||||||
|
"indexeddb success preApply p95",
|
||||||
|
);
|
||||||
|
printRows(
|
||||||
|
collectMetricRows(compare, (entry) => entry.hydrateMs?.p95, "hydrateMs.p95"),
|
||||||
|
"hydrate p95",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (baselinePath) {
|
||||||
|
await removeWorktree(baselinePath);
|
||||||
|
}
|
||||||
|
if (currentPath && currentPath !== projectRoot) {
|
||||||
|
await removeWorktree(currentPath);
|
||||||
|
}
|
||||||
|
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
176
sync/bme-db.js
176
sync/bme-db.js
@@ -40,6 +40,8 @@ export const BME_RUNTIME_TIMELINE_STATE_META_KEY = "timelineState";
|
|||||||
export const BME_RUNTIME_LAST_PROCESSED_SEQ_META_KEY =
|
export const BME_RUNTIME_LAST_PROCESSED_SEQ_META_KEY =
|
||||||
"runtimeLastProcessedSeq";
|
"runtimeLastProcessedSeq";
|
||||||
export const BME_RUNTIME_GRAPH_VERSION_META_KEY = "runtimeGraphVersion";
|
export const BME_RUNTIME_GRAPH_VERSION_META_KEY = "runtimeGraphVersion";
|
||||||
|
export const BME_RUNTIME_RECORDS_NORMALIZED_META_KEY =
|
||||||
|
"runtimeRecordsNormalized";
|
||||||
|
|
||||||
export const BME_DB_TABLE_SCHEMAS = Object.freeze({
|
export const BME_DB_TABLE_SCHEMAS = Object.freeze({
|
||||||
nodes:
|
nodes:
|
||||||
@@ -153,6 +155,169 @@ function toArray(value) {
|
|||||||
return Array.isArray(value) ? value : [];
|
return Array.isArray(value) ? value : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cloneHydrateSnapshotNestedValue(value, fallbackValue = null) {
|
||||||
|
if (value == null || typeof value !== "object") {
|
||||||
|
return value == null ? fallbackValue : value;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const output = new Array(value.length);
|
||||||
|
for (let index = 0; index < value.length; index += 1) {
|
||||||
|
const entry = value[index];
|
||||||
|
output[index] =
|
||||||
|
entry != null && typeof entry === "object"
|
||||||
|
? cloneHydrateSnapshotNestedValue(entry, entry)
|
||||||
|
: entry;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
const prototype = Object.getPrototypeOf(value);
|
||||||
|
if (prototype !== Object.prototype && prototype !== null) {
|
||||||
|
return toPlainData(value, fallbackValue ?? value);
|
||||||
|
}
|
||||||
|
const output = {};
|
||||||
|
for (const key in value) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
|
||||||
|
const entry = value[key];
|
||||||
|
output[key] =
|
||||||
|
entry != null && typeof entry === "object"
|
||||||
|
? cloneHydrateSnapshotNestedValue(entry, entry)
|
||||||
|
: entry;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneHydrateSnapshotMemoryScope(scope = null) {
|
||||||
|
if (!scope || typeof scope !== "object" || Array.isArray(scope)) {
|
||||||
|
return cloneHydrateSnapshotNestedValue(scope, scope);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...scope,
|
||||||
|
regionPath: Array.isArray(scope.regionPath) ? [...scope.regionPath] : [],
|
||||||
|
regionSecondary: Array.isArray(scope.regionSecondary)
|
||||||
|
? [...scope.regionSecondary]
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneHydrateSnapshotStoryTime(storyTime = null) {
|
||||||
|
if (!storyTime || typeof storyTime !== "object" || Array.isArray(storyTime)) {
|
||||||
|
return cloneHydrateSnapshotNestedValue(storyTime, storyTime);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...storyTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneHydrateSnapshotStoryTimeSpan(storyTimeSpan = null) {
|
||||||
|
if (
|
||||||
|
!storyTimeSpan ||
|
||||||
|
typeof storyTimeSpan !== "object" ||
|
||||||
|
Array.isArray(storyTimeSpan)
|
||||||
|
) {
|
||||||
|
return cloneHydrateSnapshotNestedValue(storyTimeSpan, storyTimeSpan);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...storyTimeSpan,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneHydrateSnapshotNodeRecord(record = null) {
|
||||||
|
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const cloned = {};
|
||||||
|
for (const key in record) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(record, key)) continue;
|
||||||
|
const value = record[key];
|
||||||
|
switch (key) {
|
||||||
|
case "fields":
|
||||||
|
cloned.fields = cloneHydrateSnapshotNestedValue(value, {});
|
||||||
|
break;
|
||||||
|
case "seqRange":
|
||||||
|
cloned.seqRange = Array.isArray(value)
|
||||||
|
? value.slice()
|
||||||
|
: cloneHydrateSnapshotNestedValue(value, value);
|
||||||
|
break;
|
||||||
|
case "childIds":
|
||||||
|
cloned.childIds = Array.isArray(value)
|
||||||
|
? value.slice()
|
||||||
|
: cloneHydrateSnapshotNestedValue(value, value);
|
||||||
|
break;
|
||||||
|
case "clusters":
|
||||||
|
cloned.clusters = Array.isArray(value)
|
||||||
|
? value.slice()
|
||||||
|
: cloneHydrateSnapshotNestedValue(value, value);
|
||||||
|
break;
|
||||||
|
case "scope":
|
||||||
|
cloned.scope = cloneHydrateSnapshotMemoryScope(value);
|
||||||
|
break;
|
||||||
|
case "storyTime":
|
||||||
|
cloned.storyTime = cloneHydrateSnapshotStoryTime(value);
|
||||||
|
break;
|
||||||
|
case "storyTimeSpan":
|
||||||
|
cloned.storyTimeSpan = cloneHydrateSnapshotStoryTimeSpan(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
cloned[key] =
|
||||||
|
value != null && typeof value === "object"
|
||||||
|
? cloneHydrateSnapshotNestedValue(value, value)
|
||||||
|
: value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneHydrateSnapshotEdgeRecord(record = null) {
|
||||||
|
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const cloned = {};
|
||||||
|
for (const key in record) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(record, key)) continue;
|
||||||
|
const value = record[key];
|
||||||
|
if (key === "scope") {
|
||||||
|
cloned.scope = cloneHydrateSnapshotMemoryScope(value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cloned[key] =
|
||||||
|
value != null && typeof value === "object"
|
||||||
|
? cloneHydrateSnapshotNestedValue(value, value)
|
||||||
|
: value;
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneHydrateSnapshotNodeRecords(records = []) {
|
||||||
|
const sourceRecords = toArray(records);
|
||||||
|
if (sourceRecords.length === 0) return [];
|
||||||
|
const output = new Array(sourceRecords.length);
|
||||||
|
let writeIndex = 0;
|
||||||
|
for (let index = 0; index < sourceRecords.length; index += 1) {
|
||||||
|
const cloned = cloneHydrateSnapshotNodeRecord(sourceRecords[index]);
|
||||||
|
if (!cloned) continue;
|
||||||
|
output[writeIndex] = cloned;
|
||||||
|
writeIndex += 1;
|
||||||
|
}
|
||||||
|
output.length = writeIndex;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneHydrateSnapshotEdgeRecords(records = []) {
|
||||||
|
const sourceRecords = toArray(records);
|
||||||
|
if (sourceRecords.length === 0) return [];
|
||||||
|
const output = new Array(sourceRecords.length);
|
||||||
|
let writeIndex = 0;
|
||||||
|
for (let index = 0; index < sourceRecords.length; index += 1) {
|
||||||
|
const cloned = cloneHydrateSnapshotEdgeRecord(sourceRecords[index]);
|
||||||
|
if (!cloned) continue;
|
||||||
|
output[writeIndex] = cloned;
|
||||||
|
writeIndex += 1;
|
||||||
|
}
|
||||||
|
output.length = writeIndex;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
function toMetaMap(rows = []) {
|
function toMetaMap(rows = []) {
|
||||||
const output = {};
|
const output = {};
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
@@ -927,6 +1092,7 @@ export function buildSnapshotFromGraph(graph, options = {}) {
|
|||||||
)
|
)
|
||||||
? Number(runtimeGraph.version)
|
? Number(runtimeGraph.version)
|
||||||
: Number(baseSnapshot.meta?.[BME_RUNTIME_GRAPH_VERSION_META_KEY] || 0),
|
: Number(baseSnapshot.meta?.[BME_RUNTIME_GRAPH_VERSION_META_KEY] || 0),
|
||||||
|
[BME_RUNTIME_RECORDS_NORMALIZED_META_KEY]: true,
|
||||||
};
|
};
|
||||||
if (snapshotDiagnostics) {
|
if (snapshotDiagnostics) {
|
||||||
snapshotDiagnostics.metaMs = readPersistDeltaNow() - metaStartedAt;
|
snapshotDiagnostics.metaMs = readPersistDeltaNow() - metaStartedAt;
|
||||||
@@ -2133,6 +2299,8 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
|
|||||||
snapshotMeta?.[BME_RUNTIME_VECTOR_META_KEY],
|
snapshotMeta?.[BME_RUNTIME_VECTOR_META_KEY],
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
const snapshotRecordsNormalized =
|
||||||
|
snapshotMeta?.[BME_RUNTIME_RECORDS_NORMALIZED_META_KEY] === true;
|
||||||
|
|
||||||
const runtimeGraph = createEmptyGraph();
|
const runtimeGraph = createEmptyGraph();
|
||||||
runtimeGraph.version = Number.isFinite(
|
runtimeGraph.version = Number.isFinite(
|
||||||
@@ -2142,14 +2310,14 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
|
|||||||
: runtimeGraph.version;
|
: runtimeGraph.version;
|
||||||
|
|
||||||
const hydrateNodesStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
const hydrateNodesStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||||
runtimeGraph.nodes = toArray(toPlainData(snapshotView.nodes, []));
|
runtimeGraph.nodes = cloneHydrateSnapshotNodeRecords(snapshotView.nodes);
|
||||||
if (hydrateDiagnostics) {
|
if (hydrateDiagnostics) {
|
||||||
hydrateDiagnostics.nodeCount = runtimeGraph.nodes.length;
|
hydrateDiagnostics.nodeCount = runtimeGraph.nodes.length;
|
||||||
hydrateDiagnostics.nodesMs = readPersistDeltaNow() - hydrateNodesStartedAt;
|
hydrateDiagnostics.nodesMs = readPersistDeltaNow() - hydrateNodesStartedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hydrateEdgesStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
const hydrateEdgesStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||||
runtimeGraph.edges = toArray(toPlainData(snapshotView.edges, []));
|
runtimeGraph.edges = cloneHydrateSnapshotEdgeRecords(snapshotView.edges);
|
||||||
if (hydrateDiagnostics) {
|
if (hydrateDiagnostics) {
|
||||||
hydrateDiagnostics.edgeCount = runtimeGraph.edges.length;
|
hydrateDiagnostics.edgeCount = runtimeGraph.edges.length;
|
||||||
hydrateDiagnostics.edgesMs = readPersistDeltaNow() - hydrateEdgesStartedAt;
|
hydrateDiagnostics.edgesMs = readPersistDeltaNow() - hydrateEdgesStartedAt;
|
||||||
@@ -2302,7 +2470,9 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const normalizeStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
const normalizeStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||||
const normalizedGraph = normalizeGraphRuntimeState(runtimeGraph, chatId);
|
const normalizedGraph = normalizeGraphRuntimeState(runtimeGraph, chatId, {
|
||||||
|
skipRecordFieldNormalization: snapshotRecordsNormalized,
|
||||||
|
});
|
||||||
if (hydrateDiagnostics) {
|
if (hydrateDiagnostics) {
|
||||||
hydrateDiagnostics.normalizeMs = readPersistDeltaNow() - normalizeStartedAt;
|
hydrateDiagnostics.normalizeMs = readPersistDeltaNow() - normalizeStartedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
BME_DB_SCHEMA_VERSION,
|
BME_DB_SCHEMA_VERSION,
|
||||||
BME_RUNTIME_BATCH_JOURNAL_META_KEY,
|
BME_RUNTIME_BATCH_JOURNAL_META_KEY,
|
||||||
BME_RUNTIME_HISTORY_META_KEY,
|
BME_RUNTIME_HISTORY_META_KEY,
|
||||||
|
BME_RUNTIME_RECORDS_NORMALIZED_META_KEY,
|
||||||
BME_RUNTIME_VECTOR_META_KEY,
|
BME_RUNTIME_VECTOR_META_KEY,
|
||||||
BME_TOMBSTONE_RETENTION_MS,
|
BME_TOMBSTONE_RETENTION_MS,
|
||||||
BmeDatabase,
|
BmeDatabase,
|
||||||
@@ -618,6 +619,33 @@ async function testGraphSnapshotConverters() {
|
|||||||
title: "Converter Node",
|
title: "Converter Node",
|
||||||
},
|
},
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
|
embedding: [0.25, 0.5, 0.75],
|
||||||
|
scope: {
|
||||||
|
layer: "pov",
|
||||||
|
ownerType: "character",
|
||||||
|
ownerId: "hero",
|
||||||
|
ownerName: "Hero",
|
||||||
|
regionPrimary: "camp",
|
||||||
|
regionPath: ["camp", "tent"],
|
||||||
|
regionSecondary: ["forest"],
|
||||||
|
},
|
||||||
|
storyTime: {
|
||||||
|
segmentId: "segment-1",
|
||||||
|
label: "Dawn",
|
||||||
|
tense: "ongoing",
|
||||||
|
relation: "same",
|
||||||
|
anchorLabel: "Night",
|
||||||
|
confidence: "high",
|
||||||
|
source: "derived",
|
||||||
|
},
|
||||||
|
storyTimeSpan: {
|
||||||
|
startSegmentId: "segment-0",
|
||||||
|
endSegmentId: "segment-1",
|
||||||
|
startLabel: "Night",
|
||||||
|
endLabel: "Dawn",
|
||||||
|
mixed: false,
|
||||||
|
source: "derived",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let snapshotDiagnostics = null;
|
let snapshotDiagnostics = null;
|
||||||
@@ -630,6 +658,7 @@ async function testGraphSnapshotConverters() {
|
|||||||
});
|
});
|
||||||
assert.equal(snapshot.meta.chatId, "chat-a");
|
assert.equal(snapshot.meta.chatId, "chat-a");
|
||||||
assert.equal(snapshot.meta.revision, 17);
|
assert.equal(snapshot.meta.revision, 17);
|
||||||
|
assert.equal(snapshot.meta[BME_RUNTIME_RECORDS_NORMALIZED_META_KEY], true);
|
||||||
assert.equal(snapshot.state.lastProcessedFloor, 9);
|
assert.equal(snapshot.state.lastProcessedFloor, 9);
|
||||||
assert.equal(snapshot.state.extractionCount, 4);
|
assert.equal(snapshot.state.extractionCount, 4);
|
||||||
assert.equal(snapshot.nodes.length, 1);
|
assert.equal(snapshot.nodes.length, 1);
|
||||||
@@ -687,18 +716,44 @@ async function testGraphSnapshotConverters() {
|
|||||||
const rebuilt = buildGraphFromSnapshot(snapshot, {
|
const rebuilt = buildGraphFromSnapshot(snapshot, {
|
||||||
chatId: "chat-a",
|
chatId: "chat-a",
|
||||||
});
|
});
|
||||||
|
const legacyCompatibleSnapshot = {
|
||||||
|
...snapshot,
|
||||||
|
meta: {
|
||||||
|
...snapshot.meta,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
delete legacyCompatibleSnapshot.meta[BME_RUNTIME_RECORDS_NORMALIZED_META_KEY];
|
||||||
|
legacyCompatibleSnapshot.nodes = [
|
||||||
|
{
|
||||||
|
...legacyCompatibleSnapshot.nodes[0],
|
||||||
|
scope: undefined,
|
||||||
|
storyTime: undefined,
|
||||||
|
storyTimeSpan: undefined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const rebuiltLegacyCompatible = buildGraphFromSnapshot(legacyCompatibleSnapshot, {
|
||||||
|
chatId: "chat-a",
|
||||||
|
});
|
||||||
assert.equal(rebuilt.historyState.lastProcessedAssistantFloor, 9);
|
assert.equal(rebuilt.historyState.lastProcessedAssistantFloor, 9);
|
||||||
assert.equal(rebuilt.historyState.extractionCount, 4);
|
assert.equal(rebuilt.historyState.extractionCount, 4);
|
||||||
assert.equal(rebuilt.nodes.length, 1);
|
assert.equal(rebuilt.nodes.length, 1);
|
||||||
assert.equal(rebuilt.nodes[0].id, "node-converter");
|
assert.equal(rebuilt.nodes[0].id, "node-converter");
|
||||||
|
assert.equal(rebuilt.nodes[0].scope?.ownerType, "character");
|
||||||
|
assert.equal(rebuilt.nodes[0].scope?.regionPrimary, "camp");
|
||||||
|
assert.equal(rebuilt.nodes[0].storyTime?.label, "Dawn");
|
||||||
|
assert.equal(rebuilt.nodes[0].storyTimeSpan?.endLabel, "Dawn");
|
||||||
assert.equal(rebuilt.vectorIndexState.hashToNodeId["vec-hash"], "node-converter");
|
assert.equal(rebuilt.vectorIndexState.hashToNodeId["vec-hash"], "node-converter");
|
||||||
assert.equal(rebuilt.maintenanceJournal[0].id, "maintenance-1");
|
assert.equal(rebuilt.maintenanceJournal[0].id, "maintenance-1");
|
||||||
assert.equal(rebuilt.knowledgeState.activeOwnerKey, "owner:hero");
|
assert.equal(rebuilt.knowledgeState.activeOwnerKey, "owner:hero");
|
||||||
assert.equal(rebuilt.regionState.activeRegion, "camp");
|
assert.equal(rebuilt.regionState.activeRegion, "camp");
|
||||||
assert.equal(rebuilt.timelineState.activeSegmentId, "segment-1");
|
assert.equal(rebuilt.timelineState.activeSegmentId, "segment-1");
|
||||||
assert.equal(rebuilt.summaryState.entries[0].id, "summary-1");
|
assert.equal(rebuilt.summaryState.entries[0].id, "summary-1");
|
||||||
|
assert.equal(rebuiltLegacyCompatible.nodes[0].scope?.layer, "objective");
|
||||||
|
assert.equal(rebuiltLegacyCompatible.nodes[0].storyTime?.tense, "unknown");
|
||||||
|
assert.equal(rebuiltLegacyCompatible.nodes[0].storyTimeSpan?.mixed, false);
|
||||||
|
|
||||||
rebuilt.nodes[0].fields.title = "Mutated Converter Node";
|
rebuilt.nodes[0].fields.title = "Mutated Converter Node";
|
||||||
|
rebuilt.nodes[0].embedding[0] = 99;
|
||||||
rebuilt.historyState.processedMessageHashes[1] = "mutated-hash";
|
rebuilt.historyState.processedMessageHashes[1] = "mutated-hash";
|
||||||
rebuilt.vectorIndexState.hashToNodeId["vec-hash"] = "node-mutated";
|
rebuilt.vectorIndexState.hashToNodeId["vec-hash"] = "node-mutated";
|
||||||
rebuilt.batchJournal[0].processedRange[0] = 99;
|
rebuilt.batchJournal[0].processedRange[0] = 99;
|
||||||
@@ -713,6 +768,11 @@ async function testGraphSnapshotConverters() {
|
|||||||
"hash-1",
|
"hash-1",
|
||||||
"buildGraphFromSnapshot 不应复用 snapshot historyState 的嵌套对象引用",
|
"buildGraphFromSnapshot 不应复用 snapshot historyState 的嵌套对象引用",
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
snapshot.nodes[0].embedding[0],
|
||||||
|
0.25,
|
||||||
|
"buildGraphFromSnapshot 不应复用 snapshot 节点的数组字段引用",
|
||||||
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
snapshot.meta[BME_RUNTIME_VECTOR_META_KEY].hashToNodeId["vec-hash"],
|
snapshot.meta[BME_RUNTIME_VECTOR_META_KEY].hashToNodeId["vec-hash"],
|
||||||
"node-converter",
|
"node-converter",
|
||||||
|
|||||||
397
tests/perf/load-preapply-bench.mjs
Normal file
397
tests/perf/load-preapply-bench.mjs
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
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();
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { createMemoryOpfsRoot } from "../helpers/memory-opfs.mjs";
|
import { createMemoryOpfsRoot } from "../helpers/memory-opfs.mjs";
|
||||||
|
|
||||||
const RUNS = 4;
|
const RUNS = 4;
|
||||||
|
const outputJson = process.argv.includes("--json");
|
||||||
const SIZE_PRESETS = [
|
const SIZE_PRESETS = [
|
||||||
{ label: "M", seed: 17, nodeCount: 1200, edgeCount: 3600, churn: 0.08 },
|
{ label: "M", seed: 17, nodeCount: 1200, edgeCount: 3600, churn: 0.08 },
|
||||||
{ label: "L", seed: 29, nodeCount: 3600, edgeCount: 10800, churn: 0.1 },
|
{ label: "L", seed: 29, nodeCount: 3600, edgeCount: 10800, churn: 0.1 },
|
||||||
@@ -260,6 +261,11 @@ async function runPreset(preset) {
|
|||||||
const opfsCommitSamples = [];
|
const opfsCommitSamples = [];
|
||||||
const snapshotNodesSamples = [];
|
const snapshotNodesSamples = [];
|
||||||
const hydrateRuntimeMetaSamples = [];
|
const hydrateRuntimeMetaSamples = [];
|
||||||
|
const hydrateNodesSamples = [];
|
||||||
|
const hydrateEdgesSamples = [];
|
||||||
|
const hydrateStateSamples = [];
|
||||||
|
const hydrateNormalizeSamples = [];
|
||||||
|
const hydrateIntegritySamples = [];
|
||||||
const walFileWriteSamples = [];
|
const walFileWriteSamples = [];
|
||||||
const manifestFileWriteSamples = [];
|
const manifestFileWriteSamples = [];
|
||||||
|
|
||||||
@@ -295,31 +301,64 @@ async function runPreset(preset) {
|
|||||||
opfsCommitSamples.push(opfsCommitResult.elapsedMs);
|
opfsCommitSamples.push(opfsCommitResult.elapsedMs);
|
||||||
snapshotNodesSamples.push(Number(afterSnapshotResult.diagnostics?.nodesMs || 0));
|
snapshotNodesSamples.push(Number(afterSnapshotResult.diagnostics?.nodesMs || 0));
|
||||||
hydrateRuntimeMetaSamples.push(Number(hydrateResult.diagnostics?.runtimeMetaMs || 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));
|
||||||
walFileWriteSamples.push(Number(opfsCommitResult.diagnostics?.walFileWriteMs || 0));
|
walFileWriteSamples.push(Number(opfsCommitResult.diagnostics?.walFileWriteMs || 0));
|
||||||
manifestFileWriteSamples.push(
|
manifestFileWriteSamples.push(
|
||||||
Number(opfsCommitResult.diagnostics?.manifestFileWriteMs || 0),
|
Number(opfsCommitResult.diagnostics?.manifestFileWriteMs || 0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n[ST-BME][persist-load-bench] ${preset.label}`);
|
const result = {
|
||||||
console.log(
|
snapshotBuildMs: summarize(snapshotBuildSamples),
|
||||||
formatSummary("snapshot-build", snapshotBuildSamples),
|
snapshotNodesMs: summarize(snapshotNodesSamples),
|
||||||
`nodesPhaseP95=${summarize(snapshotNodesSamples).p95.toFixed(2)}ms`,
|
hydrateMs: summarize(hydrateSamples),
|
||||||
);
|
hydrateNodesMs: summarize(hydrateNodesSamples),
|
||||||
console.log(
|
hydrateEdgesMs: summarize(hydrateEdgesSamples),
|
||||||
formatSummary("hydrate", hydrateSamples),
|
hydrateStateMs: summarize(hydrateStateSamples),
|
||||||
`runtimeMetaP95=${summarize(hydrateRuntimeMetaSamples).p95.toFixed(2)}ms`,
|
hydrateNormalizeMs: summarize(hydrateNormalizeSamples),
|
||||||
);
|
hydrateIntegrityMs: summarize(hydrateIntegritySamples),
|
||||||
console.log(
|
hydrateRuntimeMetaMs: summarize(hydrateRuntimeMetaSamples),
|
||||||
formatSummary("opfs-commit", opfsCommitSamples),
|
opfsCommitMs: summarize(opfsCommitSamples),
|
||||||
`walFileP95=${summarize(walFileWriteSamples).p95.toFixed(2)}ms`,
|
opfsWalFileWriteMs: summarize(walFileWriteSamples),
|
||||||
`manifestFileP95=${summarize(manifestFileWriteSamples).p95.toFixed(2)}ms`,
|
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`,
|
||||||
|
`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() {
|
async function main() {
|
||||||
|
const results = {};
|
||||||
for (const preset of SIZE_PRESETS) {
|
for (const preset of SIZE_PRESETS) {
|
||||||
await runPreset(preset);
|
results[preset.label] = await runPreset(preset);
|
||||||
|
}
|
||||||
|
if (outputJson) {
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
runs: RUNS,
|
||||||
|
presets: results,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import {
|
|||||||
findLatestNode,
|
findLatestNode,
|
||||||
serializeGraph,
|
serializeGraph,
|
||||||
} from "../graph/graph.js";
|
} from "../graph/graph.js";
|
||||||
|
import { normalizeMemoryScope } from "../graph/memory-scope.js";
|
||||||
|
import {
|
||||||
|
normalizeStoryTime,
|
||||||
|
normalizeStoryTimeSpan,
|
||||||
|
} from "../graph/story-timeline.js";
|
||||||
|
|
||||||
const graph = createEmptyGraph();
|
const graph = createEmptyGraph();
|
||||||
const objectiveNode = createNode({
|
const objectiveNode = createNode({
|
||||||
@@ -53,6 +58,50 @@ const latestPov = findLatestNode(
|
|||||||
assert.equal(latestObjective?.id, objectiveNode.id);
|
assert.equal(latestObjective?.id, objectiveNode.id);
|
||||||
assert.equal(latestPov?.id, povNode.id);
|
assert.equal(latestPov?.id, povNode.id);
|
||||||
|
|
||||||
|
const normalizedScope = {
|
||||||
|
layer: "pov",
|
||||||
|
ownerType: "character",
|
||||||
|
ownerId: "艾琳",
|
||||||
|
ownerName: "艾琳",
|
||||||
|
regionPrimary: "钟楼",
|
||||||
|
regionPath: ["钟楼", "塔顶"],
|
||||||
|
regionSecondary: ["旧城区"],
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
normalizeMemoryScope(normalizedScope),
|
||||||
|
normalizedScope,
|
||||||
|
"已规范的 scope 对象应直接复用",
|
||||||
|
);
|
||||||
|
|
||||||
|
const normalizedStoryTime = {
|
||||||
|
segmentId: "tl-1",
|
||||||
|
label: "第二天清晨",
|
||||||
|
tense: "ongoing",
|
||||||
|
relation: "same",
|
||||||
|
anchorLabel: "昨夜",
|
||||||
|
confidence: "high",
|
||||||
|
source: "derived",
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
normalizeStoryTime(normalizedStoryTime),
|
||||||
|
normalizedStoryTime,
|
||||||
|
"已规范的 storyTime 对象应直接复用",
|
||||||
|
);
|
||||||
|
|
||||||
|
const normalizedStoryTimeSpan = {
|
||||||
|
startSegmentId: "tl-0",
|
||||||
|
endSegmentId: "tl-1",
|
||||||
|
startLabel: "昨夜",
|
||||||
|
endLabel: "第二天清晨",
|
||||||
|
mixed: false,
|
||||||
|
source: "derived",
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
normalizeStoryTimeSpan(normalizedStoryTimeSpan),
|
||||||
|
normalizedStoryTimeSpan,
|
||||||
|
"已规范的 storyTimeSpan 对象应直接复用",
|
||||||
|
);
|
||||||
|
|
||||||
const legacyGraph = deserializeGraph({
|
const legacyGraph = deserializeGraph({
|
||||||
version: 6,
|
version: 6,
|
||||||
lastProcessedSeq: 0,
|
lastProcessedSeq: 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user