perf: optimize persist/load P1 hot paths

This commit is contained in:
Youzini-afk
2026-04-22 18:34:56 +08:00
parent b1937336bd
commit cfc122244a
13 changed files with 1707 additions and 78 deletions

View File

@@ -646,6 +646,26 @@ export function buildSnapshotFromGraph(graph, options = {}) {
!Array.isArray(options.baseSnapshot)
? options.baseSnapshot
: {};
const shouldCollectDiagnostics = typeof options?.onDiagnostics === "function";
const snapshotStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
const snapshotDiagnostics = shouldCollectDiagnostics
? {
nodeCount: 0,
edgeCount: 0,
tombstoneCount: 0,
reusedNodeCount: 0,
reusedEdgeCount: 0,
reusedTombstoneCount: 0,
clonedNodeCount: 0,
clonedEdgeCount: 0,
clonedTombstoneCount: 0,
nodesMs: 0,
edgesMs: 0,
tombstonesMs: 0,
stateMs: 0,
metaMs: 0,
}
: null;
const baseSnapshot = sanitizeSnapshot(baseSnapshotInput);
const baseSnapshotView = normalizePersistSnapshotView(baseSnapshotInput);
const nowMs = normalizeTimestamp(options.nowMs, Date.now());
@@ -674,6 +694,7 @@ export function buildSnapshotFromGraph(graph, options = {}) {
baseSnapshotView.tombstones,
);
const nodesStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
const nodes = toArray(runtimeGraph?.nodes)
.map((node) => {
if (!node || typeof node !== "object" || Array.isArray(node)) {
@@ -689,18 +710,29 @@ export function buildSnapshotFromGraph(graph, options = {}) {
updatedAt: normalizedUpdatedAt,
})
) {
if (snapshotDiagnostics) {
snapshotDiagnostics.reusedNodeCount += 1;
}
return baseNode;
}
const plainNode = clonePersistSnapshotRecord(node);
if (!plainNode || typeof plainNode !== "object" || Array.isArray(plainNode)) {
return null;
}
if (snapshotDiagnostics) {
snapshotDiagnostics.clonedNodeCount += 1;
}
plainNode.id = id;
plainNode.updatedAt = normalizedUpdatedAt;
return plainNode;
})
.filter(Boolean);
if (snapshotDiagnostics) {
snapshotDiagnostics.nodeCount = nodes.length;
snapshotDiagnostics.nodesMs = readPersistDeltaNow() - nodesStartedAt;
}
const edgesStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
const edges = toArray(runtimeGraph?.edges)
.map((edge) => {
if (!edge || typeof edge !== "object" || Array.isArray(edge)) {
@@ -719,12 +751,18 @@ export function buildSnapshotFromGraph(graph, options = {}) {
updatedAt: normalizedUpdatedAt,
})
) {
if (snapshotDiagnostics) {
snapshotDiagnostics.reusedEdgeCount += 1;
}
return baseEdge;
}
const plainEdge = clonePersistSnapshotRecord(edge);
if (!plainEdge || typeof plainEdge !== "object" || Array.isArray(plainEdge)) {
return null;
}
if (snapshotDiagnostics) {
snapshotDiagnostics.clonedEdgeCount += 1;
}
plainEdge.id = id;
plainEdge.fromId = normalizedFromId;
plainEdge.toId = normalizedToId;
@@ -732,7 +770,12 @@ export function buildSnapshotFromGraph(graph, options = {}) {
return plainEdge;
})
.filter(Boolean);
if (snapshotDiagnostics) {
snapshotDiagnostics.edgeCount = edges.length;
snapshotDiagnostics.edgesMs = readPersistDeltaNow() - edgesStartedAt;
}
const tombstonesStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
const tombstones = toArray(options.tombstones ?? baseSnapshotView.tombstones)
.map((record) => {
if (!record || typeof record !== "object" || Array.isArray(record))
@@ -752,12 +795,18 @@ export function buildSnapshotFromGraph(graph, options = {}) {
deletedAt: normalizedDeletedAt,
})
) {
if (snapshotDiagnostics) {
snapshotDiagnostics.reusedTombstoneCount += 1;
}
return baseTombstone;
}
const plainRecord = clonePersistSnapshotRecord(record);
if (!plainRecord || typeof plainRecord !== "object" || Array.isArray(plainRecord)) {
return null;
}
if (snapshotDiagnostics) {
snapshotDiagnostics.clonedTombstoneCount += 1;
}
plainRecord.id = id;
plainRecord.kind = normalizedKind;
plainRecord.targetId = normalizedTargetId;
@@ -766,7 +815,13 @@ export function buildSnapshotFromGraph(graph, options = {}) {
return plainRecord;
})
.filter(Boolean);
if (snapshotDiagnostics) {
snapshotDiagnostics.tombstoneCount = tombstones.length;
snapshotDiagnostics.tombstonesMs =
readPersistDeltaNow() - tombstonesStartedAt;
}
const stateStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
const state = {
...normalizeStateSnapshot(baseSnapshot),
...(options.state || {}),
@@ -783,7 +838,11 @@ export function buildSnapshotFromGraph(graph, options = {}) {
? Number(runtimeGraph.historyState.extractionCount)
: META_DEFAULT_EXTRACTION_COUNT,
};
if (snapshotDiagnostics) {
snapshotDiagnostics.stateMs = readPersistDeltaNow() - stateStartedAt;
}
const metaStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
const mergedMeta = {
...baseSnapshot.meta,
...(options.meta || {}),
@@ -869,14 +928,26 @@ export function buildSnapshotFromGraph(graph, options = {}) {
? Number(runtimeGraph.version)
: Number(baseSnapshot.meta?.[BME_RUNTIME_GRAPH_VERSION_META_KEY] || 0),
};
if (snapshotDiagnostics) {
snapshotDiagnostics.metaMs = readPersistDeltaNow() - metaStartedAt;
}
return {
const snapshotResult = {
meta: mergedMeta,
nodes,
edges,
tombstones,
state,
};
if (snapshotDiagnostics) {
emitOptionalDiagnostics(options, {
...snapshotDiagnostics,
runtimeMetaKeyCount: Object.keys(mergedMeta).length,
totalMs: readPersistDeltaNow() - snapshotStartedAt,
});
}
return snapshotResult;
}
function normalizeSnapshotMetaState(snapshot = {}) {
@@ -1630,7 +1701,7 @@ function readPersistDeltaNow() {
return Date.now();
}
function emitPersistDeltaDiagnostics(options = {}, snapshot = null) {
function emitOptionalDiagnostics(options = {}, snapshot = null) {
if (typeof options?.onDiagnostics !== "function") return;
try {
options.onDiagnostics(snapshot ? toPlainData(snapshot, snapshot) : null);
@@ -1639,6 +1710,10 @@ function emitPersistDeltaDiagnostics(options = {}, snapshot = null) {
}
}
function emitPersistDeltaDiagnostics(options = {}, snapshot = null) {
emitOptionalDiagnostics(options, snapshot);
}
function tryBuildNativePersistDelta(
beforeSnapshot,
afterSnapshot,
@@ -2016,6 +2091,23 @@ export function buildPersistDelta(beforeSnapshot, afterSnapshot, options = {}) {
}
export function buildGraphFromSnapshot(snapshot, options = {}) {
const shouldCollectDiagnostics = typeof options?.onDiagnostics === "function";
const hydrateStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
const hydrateDiagnostics = shouldCollectDiagnostics
? {
success: false,
nodeCount: 0,
edgeCount: 0,
tombstoneCount: 0,
nodesMs: 0,
edgesMs: 0,
runtimeMetaMs: 0,
stateMs: 0,
normalizeMs: 0,
integrityMs: 0,
integrityReasonCount: 0,
}
: null;
const snapshotView = normalizePersistSnapshotView(snapshot);
const snapshotMeta =
snapshotView.meta &&
@@ -2048,8 +2140,24 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
)
? Number(snapshotMeta[BME_RUNTIME_GRAPH_VERSION_META_KEY])
: runtimeGraph.version;
const hydrateNodesStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
runtimeGraph.nodes = toArray(toPlainData(snapshotView.nodes, []));
if (hydrateDiagnostics) {
hydrateDiagnostics.nodeCount = runtimeGraph.nodes.length;
hydrateDiagnostics.nodesMs = readPersistDeltaNow() - hydrateNodesStartedAt;
}
const hydrateEdgesStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
runtimeGraph.edges = toArray(toPlainData(snapshotView.edges, []));
if (hydrateDiagnostics) {
hydrateDiagnostics.edgeCount = runtimeGraph.edges.length;
hydrateDiagnostics.edgesMs = readPersistDeltaNow() - hydrateEdgesStartedAt;
}
const hydrateRuntimeMetaStartedAt = shouldCollectDiagnostics
? readPersistDeltaNow()
: 0;
runtimeGraph.batchJournal = toArray(
toPlainData(snapshotMeta?.[BME_RUNTIME_BATCH_JOURNAL_META_KEY], []),
);
@@ -2076,6 +2184,10 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
snapshotMeta?.[BME_RUNTIME_SUMMARY_STATE_META_KEY],
runtimeGraph.summaryState || {},
);
if (hydrateDiagnostics) {
hydrateDiagnostics.runtimeMetaMs =
readPersistDeltaNow() - hydrateRuntimeMetaStartedAt;
}
const rawKnowledgeState =
runtimeGraph.knowledgeState &&
typeof runtimeGraph.knowledgeState === "object" &&
@@ -2095,6 +2207,7 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
? runtimeGraph.timelineState
: {};
const hydrateStateStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
runtimeGraph.historyState = {
...(runtimeGraph.historyState || {}),
...snapshotHistoryState,
@@ -2183,8 +2296,16 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
)
? Number(snapshotMeta[BME_RUNTIME_LAST_PROCESSED_SEQ_META_KEY])
: Number(runtimeGraph.historyState.lastProcessedAssistantFloor);
if (hydrateDiagnostics) {
hydrateDiagnostics.tombstoneCount = toArray(snapshotView.tombstones).length;
hydrateDiagnostics.stateMs = readPersistDeltaNow() - hydrateStateStartedAt;
}
const normalizeStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
const normalizedGraph = normalizeGraphRuntimeState(runtimeGraph, chatId);
if (hydrateDiagnostics) {
hydrateDiagnostics.normalizeMs = readPersistDeltaNow() - normalizeStartedAt;
}
if (
normalizedGraph.knowledgeState &&
typeof normalizedGraph.knowledgeState === "object" &&
@@ -2238,6 +2359,7 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
);
const inconsistentReasons = [];
const integrityStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
if (
Number.isFinite(resolvedLastProcessedFloor) &&
Number.isFinite(resolvedLastProcessedSeq) &&
@@ -2255,8 +2377,20 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
if (collectionId && collectionId !== expectedCollectionId) {
inconsistentReasons.push("vector-collection-mismatch");
}
if (hydrateDiagnostics) {
hydrateDiagnostics.integrityMs = readPersistDeltaNow() - integrityStartedAt;
hydrateDiagnostics.integrityReasonCount = inconsistentReasons.length;
}
if (inconsistentReasons.length > 0) {
if (hydrateDiagnostics) {
emitOptionalDiagnostics(options, {
...hydrateDiagnostics,
success: false,
integrityReasons: [...inconsistentReasons],
totalMs: readPersistDeltaNow() - hydrateStartedAt,
});
}
const error = new Error(
`图谱快照完整性校验失败: ${inconsistentReasons.join(", ")}`,
);
@@ -2266,6 +2400,15 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
throw error;
}
if (hydrateDiagnostics) {
emitOptionalDiagnostics(options, {
...hydrateDiagnostics,
success: true,
integrityReasons: [],
totalMs: readPersistDeltaNow() - hydrateStartedAt,
});
}
return normalizedGraph;
}
@@ -3151,6 +3294,40 @@ export class BmeDatabase {
return snapshot;
}
async exportSnapshotProbe() {
const db = await this.open();
const metaRows = await db.transaction("r", db.table("meta"), async () =>
await db.table("meta").toArray(),
);
const metaMap = toMetaMap(metaRows);
const meta = {
...metaMap,
schemaVersion: BME_DB_SCHEMA_VERSION,
chatId: this.chatId,
revision: normalizeRevision(metaMap?.revision),
nodeCount: normalizeNonNegativeInteger(metaMap?.nodeCount, 0),
edgeCount: normalizeNonNegativeInteger(metaMap?.edgeCount, 0),
tombstoneCount: normalizeNonNegativeInteger(metaMap?.tombstoneCount, 0),
};
const state = {
lastProcessedFloor: Number.isFinite(Number(meta.lastProcessedFloor))
? Number(meta.lastProcessedFloor)
: META_DEFAULT_LAST_PROCESSED_FLOOR,
extractionCount: Number.isFinite(Number(meta.extractionCount))
? Number(meta.extractionCount)
: META_DEFAULT_EXTRACTION_COUNT,
};
return {
meta,
state,
nodes: [],
edges: [],
tombstones: [],
__stBmeProbeOnly: true,
__stBmeTombstonesOmitted: true,
};
}
async importSnapshot(snapshot, options = {}) {
const db = await this.open();
const normalizedSnapshot = sanitizeSnapshot(snapshot);

View File

@@ -426,12 +426,16 @@ async function readJsonFile(parentHandle, name, fallbackValue = null) {
return JSON.parse(text);
}
async function writeJsonFile(parentHandle, name, value) {
async function writeJsonFile(parentHandle, name, value, options = {}) {
const serializedText =
typeof options?.serializedText === "string"
? options.serializedText
: JSON.stringify(value);
const fileHandle = await parentHandle.getFileHandle(String(name || ""), {
create: true,
});
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(value));
await writable.write(serializedText);
await writable.close();
return fileHandle;
}
@@ -2416,12 +2420,18 @@ export class OpfsGraphStore {
runtimeMetaPatch: normalizedDelta.runtimeMetaPatch,
countDelta: nextCountDelta,
};
const walSerializeStartedAt = readPersistCommitNow();
const walSerializedText = JSON.stringify(walRecord);
const walSerializeMs = readPersistCommitNow() - walSerializeStartedAt;
const walWriteStartedAt = readPersistCommitNow();
const walDirectory = await this._getWalDirectory();
const walFilename = buildOpfsV2WalFilename(nextRevision);
await writeJsonFile(walDirectory, walFilename, walRecord);
const walByteLength = JSON.stringify(walRecord).length;
const walWriteMs = readPersistCommitNow() - walWriteStartedAt;
await writeJsonFile(walDirectory, walFilename, walRecord, {
serializedText: walSerializedText,
});
const walByteLength = walSerializedText.length;
const walFileWriteMs = readPersistCommitNow() - walWriteStartedAt;
const walWriteMs = walSerializeMs + walFileWriteMs;
const hadPendingWal =
normalizeRevision(manifest?.pendingLogFromRevision) <= currentHeadRevision;
@@ -2448,12 +2458,40 @@ export class OpfsGraphStore {
lastReason: reason,
},
};
const manifestWriteStartedAt = readPersistCommitNow();
await this._writeManifest(nextManifest);
const manifestWriteMs = readPersistCommitNow() - manifestWriteStartedAt;
const manifestWriteDiagnostics = {};
await this._writeManifest(nextManifest, {
diagnostics: manifestWriteDiagnostics,
});
const manifestSerializeMs = Number(
manifestWriteDiagnostics.serializeMs || 0,
);
const manifestFileWriteMs = Number(
manifestWriteDiagnostics.writeMs || 0,
);
const manifestWriteMs = manifestSerializeMs + manifestFileWriteMs;
const committedSnapshot =
options?.committedSnapshot &&
typeof options.committedSnapshot === "object" &&
!Array.isArray(options.committedSnapshot)
? sanitizeSnapshot(options.committedSnapshot)
: null;
let cacheApplyMs = 0;
if (this._snapshotCache) {
if (committedSnapshot) {
const cacheApplyStartedAt = readPersistCommitNow();
committedSnapshot.meta = {
...committedSnapshot.meta,
...nextMeta,
};
committedSnapshot.state = normalizeSnapshotState(committedSnapshot);
committedSnapshot.meta.lastProcessedFloor = committedSnapshot.state.lastProcessedFloor;
committedSnapshot.meta.extractionCount = committedSnapshot.state.extractionCount;
committedSnapshot.meta.nodeCount = committedSnapshot.nodes.length;
committedSnapshot.meta.edgeCount = committedSnapshot.edges.length;
committedSnapshot.meta.tombstoneCount = committedSnapshot.tombstones.length;
this._snapshotCache = committedSnapshot;
cacheApplyMs = readPersistCommitNow() - cacheApplyStartedAt;
} else if (this._snapshotCache) {
const cacheApplyStartedAt = readPersistCommitNow();
const nextSnapshot = applyOpfsV2DeltaToSnapshot(
this._snapshotCache,
@@ -2494,7 +2532,11 @@ export class OpfsGraphStore {
readPersistCommitNow() - commitStartedAt,
),
manifestReadMs: normalizePersistCommitMs(manifestReadMs),
walSerializeMs: normalizePersistCommitMs(walSerializeMs),
walFileWriteMs: normalizePersistCommitMs(walFileWriteMs),
walWriteMs: normalizePersistCommitMs(walWriteMs),
manifestSerializeMs: normalizePersistCommitMs(manifestSerializeMs),
manifestFileWriteMs: normalizePersistCommitMs(manifestFileWriteMs),
manifestWriteMs: normalizePersistCommitMs(manifestWriteMs),
cacheApplyMs: normalizePersistCommitMs(cacheApplyMs),
payloadBytes,
@@ -2740,6 +2782,39 @@ export class OpfsGraphStore {
return exported;
}
async exportSnapshotProbe() {
const manifest = await this._ensureV2Ready();
const meta = {
...createDefaultMetaValues(this.chatId),
...(manifest?.meta || {}),
chatId: this.chatId,
revision: normalizeRevision(manifest?.headRevision || manifest?.meta?.revision),
nodeCount: normalizeNonNegativeInteger(manifest?.meta?.nodeCount, 0),
edgeCount: normalizeNonNegativeInteger(manifest?.meta?.edgeCount, 0),
tombstoneCount: normalizeNonNegativeInteger(manifest?.meta?.tombstoneCount, 0),
storagePrimary: OPFS_STORE_KIND,
storageMode: this.storeMode,
schemaVersion: BME_DB_SCHEMA_VERSION,
};
const state = {
lastProcessedFloor: Number.isFinite(Number(meta.lastProcessedFloor))
? Number(meta.lastProcessedFloor)
: META_DEFAULT_LAST_PROCESSED_FLOOR,
extractionCount: Number.isFinite(Number(meta.extractionCount))
? Number(meta.extractionCount)
: META_DEFAULT_EXTRACTION_COUNT,
};
return {
meta,
state,
nodes: [],
edges: [],
tombstones: [],
__stBmeProbeOnly: true,
__stBmeTombstonesOmitted: true,
};
}
async importSnapshot(snapshot, options = {}) {
return await this._runSerializedWrite("importSnapshot", async () => {
await this._ensureV2Ready({ awaitWrites: false });
@@ -2993,7 +3068,7 @@ export class OpfsGraphStore {
return manifest;
}
async _writeManifest(manifest = {}) {
async _writeManifest(manifest = {}, options = {}) {
const chatDirectory = await this._getChatDirectory();
const nextManifest = {
...manifest,
@@ -3017,7 +3092,24 @@ export class OpfsGraphStore {
storageMode: this.storeMode,
},
};
await writeJsonFile(chatDirectory, OPFS_MANIFEST_FILENAME, nextManifest);
let serializedText = "";
let serializeMs = 0;
if (options?.diagnostics && typeof options.diagnostics === "object") {
const serializeStartedAt = readPersistCommitNow();
serializedText = JSON.stringify(nextManifest);
serializeMs = readPersistCommitNow() - serializeStartedAt;
}
const writeStartedAt =
options?.diagnostics && typeof options.diagnostics === "object"
? readPersistCommitNow()
: 0;
await writeJsonFile(chatDirectory, OPFS_MANIFEST_FILENAME, nextManifest, {
serializedText,
});
if (options?.diagnostics && typeof options.diagnostics === "object") {
options.diagnostics.serializeMs = serializeMs;
options.diagnostics.writeMs = readPersistCommitNow() - writeStartedAt;
}
this._manifestCache = nextManifest;
return nextManifest;
}

View File

@@ -47,6 +47,30 @@ export function buildRestoreSafetyChatId(chatId) {
return `__restore_safety__${normalizeChatId(chatId)}`;
}
function readSyncTimingNow() {
if (typeof performance === "object" && typeof performance.now === "function") {
return performance.now();
}
return Date.now();
}
function normalizeSyncTimingMs(value = 0) {
return Math.round((Number(value) || 0) * 10) / 10;
}
function finalizeSyncTimings(record = {}, startedAt = 0) {
const result = {};
for (const [key, value] of Object.entries(record || {})) {
if (typeof value === "number" && Number.isFinite(value)) {
result[key] = normalizeSyncTimingMs(value);
}
}
if (startedAt > 0) {
result.totalMs = normalizeSyncTimingMs(readSyncTimingNow() - startedAt);
}
return result;
}
function resolveCloudStorageMode(options = {}) {
const mode =
typeof options.getCloudStorageMode === "function"
@@ -494,14 +518,20 @@ async function resolveBackupLookupContext(chatId, options = {}) {
}
async function readBackupEnvelope(chatId, options = {}) {
const readStartedAt = readSyncTimingNow();
const normalizedChatId = normalizeChatId(chatId);
const lookupStartedAt = readSyncTimingNow();
const lookup = await resolveBackupLookupContext(normalizedChatId, options);
const lookupMs = readSyncTimingNow() - lookupStartedAt;
const fetchImpl = getFetch(options);
const fallbackFilename = buildBackupFilename(normalizedChatId);
let lastMissingFilename = lookup.candidates[0]?.filename || fallbackFilename;
let networkMs = 0;
let parseMs = 0;
for (const candidate of lookup.candidates) {
try {
const networkStartedAt = readSyncTimingNow();
const response = await fetchImpl(
`${candidate.serverPath || `/user/files/${encodeURIComponent(candidate.filename)}`}?t=${Date.now()}`,
{
@@ -509,6 +539,7 @@ async function readBackupEnvelope(chatId, options = {}) {
cache: "no-store",
},
);
networkMs += readSyncTimingNow() - networkStartedAt;
if (response.status === 404) {
lastMissingFilename = candidate.filename;
continue;
@@ -521,10 +552,13 @@ async function readBackupEnvelope(chatId, options = {}) {
envelope: null,
reason: "backup-read-error",
error: new Error(errorText || `HTTP ${response.status}`),
timings: finalizeSyncTimings({ lookupMs, networkMs, parseMs }, readStartedAt),
};
}
const parseStartedAt = readSyncTimingNow();
const payload = await response.json();
parseMs += readSyncTimingNow() - parseStartedAt;
const envelope = normalizeBackupEnvelope(payload, normalizedChatId);
if (!envelope) {
return {
@@ -532,6 +566,7 @@ async function readBackupEnvelope(chatId, options = {}) {
filename: candidate.filename,
envelope: null,
reason: "invalid-backup",
timings: finalizeSyncTimings({ lookupMs, networkMs, parseMs }, readStartedAt),
};
}
return {
@@ -539,6 +574,7 @@ async function readBackupEnvelope(chatId, options = {}) {
filename: candidate.filename,
envelope,
reason: "ok",
timings: finalizeSyncTimings({ lookupMs, networkMs, parseMs }, readStartedAt),
};
} catch (error) {
return {
@@ -547,6 +583,7 @@ async function readBackupEnvelope(chatId, options = {}) {
envelope: null,
reason: "backup-read-error",
error,
timings: finalizeSyncTimings({ lookupMs, networkMs, parseMs }, readStartedAt),
};
}
}
@@ -557,6 +594,7 @@ async function readBackupEnvelope(chatId, options = {}) {
envelope: null,
reason: "not-found",
manifestError: lookup.manifestError,
timings: finalizeSyncTimings({ lookupMs, networkMs, parseMs }, readStartedAt),
};
}
@@ -581,10 +619,14 @@ async function syncDeletedBackupMeta(chatId, remainingEntry, options = {}) {
}
async function writeBackupEnvelope(envelope, chatId, options = {}) {
const writeStartedAt = readSyncTimingNow();
const normalizedChatId = normalizeChatId(chatId);
const filename = buildBackupFilename(normalizedChatId);
const fetchImpl = getFetch(options);
const serializeStartedAt = readSyncTimingNow();
const payload = JSON.stringify(envelope);
const serializeMs = readSyncTimingNow() - serializeStartedAt;
const uploadStartedAt = readSyncTimingNow();
const response = await fetchImpl("/api/files/upload", {
method: "POST",
headers: {
@@ -596,16 +638,27 @@ async function writeBackupEnvelope(envelope, chatId, options = {}) {
data: encodeBase64Utf8(payload),
}),
});
const uploadMs = readSyncTimingNow() - uploadStartedAt;
if (!response.ok) {
const errorText = await response.text().catch(() => response.statusText);
throw new Error(errorText || `HTTP ${response.status}`);
}
const responseParseStartedAt = readSyncTimingNow();
const uploadResult = await response.json().catch(() => ({}));
const responseParseMs = readSyncTimingNow() - responseParseStartedAt;
return {
filename,
path: String(uploadResult?.path || `/user/files/${filename}`),
timings: finalizeSyncTimings(
{
serializeMs,
uploadMs,
responseParseMs,
},
writeStartedAt,
),
};
}
@@ -1825,6 +1878,7 @@ async function resolveSyncFilenameCandidates(chatId, options = {}) {
}
async function readRemoteSnapshot(chatId, options = {}) {
const readStartedAt = readSyncTimingNow();
const normalizedChatId = normalizeChatId(chatId);
if (!normalizedChatId) {
return {
@@ -1832,15 +1886,22 @@ async function readRemoteSnapshot(chatId, options = {}) {
status: "missing-chat-id",
filename: "",
snapshot: null,
timings: finalizeSyncTimings({}, readStartedAt),
};
}
const fetchImpl = getFetch(options);
const resolveStartedAt = readSyncTimingNow();
const candidateFilenames = await resolveSyncFilenameCandidates(
normalizedChatId,
options,
);
const resolveCandidatesMs = readSyncTimingNow() - resolveStartedAt;
let lastNotFoundFilename = candidateFilenames[0] || "";
let networkMs = 0;
let parseMs = 0;
let chunkReadMs = 0;
let normalizeMs = 0;
for (const filename of candidateFilenames) {
const cacheBust = `t=${Date.now()}`;
@@ -1848,10 +1909,12 @@ async function readRemoteSnapshot(chatId, options = {}) {
let response;
try {
const networkStartedAt = readSyncTimingNow();
response = await fetchImpl(url, {
method: "GET",
cache: "no-store",
});
networkMs += readSyncTimingNow() - networkStartedAt;
} catch (error) {
console.warn("[ST-BME] 读取远端同步文件失败:", error);
return {
@@ -1860,6 +1923,10 @@ async function readRemoteSnapshot(chatId, options = {}) {
filename,
snapshot: null,
error,
timings: finalizeSyncTimings(
{ resolveCandidatesMs, networkMs, parseMs, chunkReadMs, normalizeMs },
readStartedAt,
),
};
}
@@ -1879,14 +1946,20 @@ async function readRemoteSnapshot(chatId, options = {}) {
snapshot: null,
error,
statusCode: response.status,
timings: finalizeSyncTimings(
{ resolveCandidatesMs, networkMs, parseMs, chunkReadMs, normalizeMs },
readStartedAt,
),
};
}
try {
const parseStartedAt = readSyncTimingNow();
const remotePayload = await response.json();
parseMs += readSyncTimingNow() - parseStartedAt;
let snapshot = null;
if (Number(remotePayload?.formatVersion || 0) === BME_REMOTE_SYNC_FORMAT_VERSION_V2) {
snapshot = await readRemoteSnapshotV2Manifest(
const manifestResult = await readRemoteSnapshotV2Manifest(
remotePayload,
normalizedChatId,
{
@@ -1894,8 +1967,13 @@ async function readRemoteSnapshot(chatId, options = {}) {
filename,
},
);
snapshot = manifestResult.snapshot;
chunkReadMs += Number(manifestResult?.timings?.chunkReadMs || 0);
normalizeMs += Number(manifestResult?.timings?.normalizeMs || 0);
} else {
const normalizeStartedAt = readSyncTimingNow();
snapshot = normalizeSyncSnapshot(remotePayload, normalizedChatId);
normalizeMs += readSyncTimingNow() - normalizeStartedAt;
}
rememberResolvedSyncFilename(normalizedChatId, filename);
return {
@@ -1903,6 +1981,10 @@ async function readRemoteSnapshot(chatId, options = {}) {
status: "ok",
filename,
snapshot,
timings: finalizeSyncTimings(
{ resolveCandidatesMs, networkMs, parseMs, chunkReadMs, normalizeMs },
readStartedAt,
),
};
} catch (error) {
console.warn("[ST-BME] 解析远端同步文件失败:", error);
@@ -1912,6 +1994,10 @@ async function readRemoteSnapshot(chatId, options = {}) {
filename,
snapshot: null,
error,
timings: finalizeSyncTimings(
{ resolveCandidatesMs, networkMs, parseMs, chunkReadMs, normalizeMs },
readStartedAt,
),
};
}
}
@@ -1921,6 +2007,10 @@ async function readRemoteSnapshot(chatId, options = {}) {
status: "not-found",
filename: lastNotFoundFilename,
snapshot: null,
timings: finalizeSyncTimings(
{ resolveCandidatesMs, networkMs, parseMs, chunkReadMs, normalizeMs },
readStartedAt,
),
};
}
@@ -1944,17 +2034,21 @@ async function readRemoteJsonFile(filename, options = {}) {
}
async function readRemoteSnapshotV2Manifest(manifest = {}, chatId = "", options = {}) {
const readStartedAt = readSyncTimingNow();
const normalizedChatId = normalizeChatId(chatId);
const chunks = Array.isArray(manifest?.chunks) ? manifest.chunks : [];
const nodes = [];
const edges = [];
const tombstones = [];
let runtimeMeta = {};
let chunkReadMs = 0;
for (const chunk of chunks) {
const filename = String(chunk?.filename || "").trim();
if (!filename) continue;
const chunkStartedAt = readSyncTimingNow();
const payload = await readRemoteJsonFile(filename, options);
chunkReadMs += readSyncTimingNow() - chunkStartedAt;
const records = Array.isArray(payload?.records) ? payload.records : [];
switch (String(chunk.kind || "").trim()) {
case "nodes":
@@ -1977,7 +2071,8 @@ async function readRemoteSnapshotV2Manifest(manifest = {}, chatId = "", options
}
}
return normalizeSyncSnapshot(
const normalizeStartedAt = readSyncTimingNow();
const snapshot = normalizeSyncSnapshot(
{
meta: {
...runtimeMeta,
@@ -1994,55 +2089,94 @@ async function readRemoteSnapshotV2Manifest(manifest = {}, chatId = "", options
},
normalizedChatId,
);
const normalizeMs = readSyncTimingNow() - normalizeStartedAt;
return {
snapshot,
timings: finalizeSyncTimings(
{
chunkReadMs,
normalizeMs,
},
readStartedAt,
),
};
}
async function writeSnapshotToRemote(snapshot, chatId, options = {}) {
const writeStartedAt = readSyncTimingNow();
const normalizedChatId = normalizeChatId(chatId);
const normalizedSnapshot = normalizeSyncSnapshot(snapshot, normalizedChatId);
const filename = await resolveSyncFilename(normalizedChatId, options);
const fetchImpl = getFetch(options);
const envelopeBuildStartedAt = readSyncTimingNow();
const syncEnvelope = buildRemoteSyncEnvelopeV2(
normalizedSnapshot,
normalizedChatId,
filename,
);
const envelopeBuildMs = readSyncTimingNow() - envelopeBuildStartedAt;
const requestHeaders = {
...getRequestHeadersSafe(options),
"Content-Type": "application/json",
};
let chunkSerializeMs = 0;
let chunkUploadMs = 0;
for (const chunk of syncEnvelope.chunks) {
const serializeStartedAt = readSyncTimingNow();
const chunkPayload = JSON.stringify(chunk.payload, null, 2);
chunkSerializeMs += readSyncTimingNow() - serializeStartedAt;
const uploadStartedAt = readSyncTimingNow();
const chunkResponse = await fetchImpl("/api/files/upload", {
method: "POST",
headers: requestHeaders,
body: JSON.stringify({
name: chunk.filename,
data: encodeBase64Utf8(JSON.stringify(chunk.payload, null, 2)),
data: encodeBase64Utf8(chunkPayload),
}),
});
chunkUploadMs += readSyncTimingNow() - uploadStartedAt;
if (!chunkResponse.ok) {
const errorText = await chunkResponse.text().catch(() => chunkResponse.statusText);
throw new Error(errorText || `HTTP ${chunkResponse.status}`);
}
}
const manifestSerializeStartedAt = readSyncTimingNow();
const manifestPayload = JSON.stringify(syncEnvelope.manifest, null, 2);
const manifestSerializeMs = readSyncTimingNow() - manifestSerializeStartedAt;
const manifestUploadStartedAt = readSyncTimingNow();
const response = await fetchImpl("/api/files/upload", {
method: "POST",
headers: requestHeaders,
body: JSON.stringify({
name: filename,
data: encodeBase64Utf8(JSON.stringify(syncEnvelope.manifest, null, 2)),
data: encodeBase64Utf8(manifestPayload),
}),
});
const manifestUploadMs = readSyncTimingNow() - manifestUploadStartedAt;
if (!response.ok) {
const errorText = await response.text().catch(() => response.statusText);
throw new Error(errorText || `HTTP ${response.status}`);
}
const responseParseStartedAt = readSyncTimingNow();
const uploadResult = await response.json().catch(() => ({}));
const responseParseMs = readSyncTimingNow() - responseParseStartedAt;
return {
filename,
path: String(uploadResult?.path || ""),
payload: syncEnvelope.manifest,
timings: finalizeSyncTimings(
{
envelopeBuildMs,
chunkSerializeMs,
chunkUploadMs,
manifestSerializeMs,
manifestUploadMs,
responseParseMs,
},
writeStartedAt,
),
};
}
@@ -2160,15 +2294,19 @@ export async function backupToServer(chatId, options = {}) {
backedUp: false,
chatId: "",
reason: "missing-chat-id",
timings: finalizeSyncTimings({}, readSyncTimingNow()),
};
}
const backupStartedAt = readSyncTimingNow();
try {
const db = await getDb(normalizedChatId, options);
const exportStartedAt = readSyncTimingNow();
const snapshot = normalizeSyncSnapshot(
await db.exportSnapshot(),
normalizedChatId,
);
const exportMs = readSyncTimingNow() - exportStartedAt;
const nowMs = Date.now();
const deviceId = getOrCreateDeviceId();
@@ -2179,6 +2317,7 @@ export async function backupToServer(chatId, options = {}) {
nowMs,
);
const envelopeBuildStartedAt = readSyncTimingNow();
const backupSnapshot = buildManualBackupSnapshot(snapshot, normalizedChatId);
const envelope = {
kind: "st-bme-backup",
@@ -2188,15 +2327,18 @@ export async function backupToServer(chatId, options = {}) {
sourceDeviceId: deviceId,
snapshot: backupSnapshot,
};
const envelopeBuildMs = readSyncTimingNow() - envelopeBuildStartedAt;
const uploadResult = await writeBackupEnvelope(
envelope,
normalizedChatId,
options,
);
const uploadTimings = uploadResult?.timings || {};
const serializedEnvelope = JSON.stringify(envelope);
try {
const manifestWriteStartedAt = readSyncTimingNow();
await upsertBackupManifestEntry(
{
filename: uploadResult.filename,
@@ -2210,6 +2352,37 @@ export async function backupToServer(chatId, options = {}) {
},
options,
);
const manifestWriteMs = readSyncTimingNow() - manifestWriteStartedAt;
const metaPatchStartedAt = readSyncTimingNow();
await patchDbMeta(db, {
deviceId,
syncDirty: false,
syncDirtyReason: "",
lastBackupUploadedAt: nowMs,
lastBackupFilename: uploadResult.filename,
});
const metaPatchMs = readSyncTimingNow() - metaPatchStartedAt;
return {
backedUp: true,
chatId: normalizedChatId,
filename: uploadResult.filename,
remotePath: uploadResult.path,
revision: normalizeRevision(snapshot.meta.revision),
backupTime: nowMs,
timings: finalizeSyncTimings(
{
exportMs,
envelopeBuildMs,
uploadMs: Number(uploadTimings.totalMs || 0),
envelopeSerializeMs: Number(uploadTimings.serializeMs || 0),
envelopeResponseParseMs: Number(uploadTimings.responseParseMs || 0),
manifestWriteMs,
metaPatchMs,
},
backupStartedAt,
),
};
} catch (manifestError) {
return {
backedUp: false,
@@ -2219,25 +2392,18 @@ export async function backupToServer(chatId, options = {}) {
reason: "backup-manifest-error",
backupUploaded: true,
error: manifestError,
timings: finalizeSyncTimings(
{
exportMs,
envelopeBuildMs,
uploadMs: Number(uploadTimings.totalMs || 0),
envelopeSerializeMs: Number(uploadTimings.serializeMs || 0),
envelopeResponseParseMs: Number(uploadTimings.responseParseMs || 0),
},
backupStartedAt,
),
};
}
await patchDbMeta(db, {
deviceId,
syncDirty: false,
syncDirtyReason: "",
lastBackupUploadedAt: nowMs,
lastBackupFilename: uploadResult.filename,
});
return {
backedUp: true,
chatId: normalizedChatId,
filename: uploadResult.filename,
remotePath: uploadResult.path,
revision: normalizeRevision(snapshot.meta.revision),
backupTime: nowMs,
};
} catch (error) {
console.warn("[ST-BME] 手动备份到云端失败:", error);
return {
@@ -2245,6 +2411,7 @@ export async function backupToServer(chatId, options = {}) {
chatId: normalizedChatId,
reason: "backup-error",
error,
timings: finalizeSyncTimings({}, backupStartedAt),
};
}
}
@@ -2256,18 +2423,30 @@ export async function restoreFromServer(chatId, options = {}) {
restored: false,
chatId: "",
reason: "missing-chat-id",
timings: finalizeSyncTimings({}, readSyncTimingNow()),
};
}
const restoreStartedAt = readSyncTimingNow();
try {
const db = await getDb(normalizedChatId, options);
const remoteResult = await readBackupEnvelope(normalizedChatId, options);
const downloadTimings = remoteResult?.timings || {};
if (!remoteResult.exists || !remoteResult.envelope) {
return {
restored: false,
chatId: normalizedChatId,
filename: remoteResult.filename || "",
reason: remoteResult.reason || "backup-missing",
timings: finalizeSyncTimings(
{
downloadMs: Number(downloadTimings.totalMs || 0),
lookupMs: Number(downloadTimings.lookupMs || 0),
networkMs: Number(downloadTimings.networkMs || 0),
envelopeParseMs: Number(downloadTimings.parseMs || 0),
},
restoreStartedAt,
),
};
}
@@ -2278,6 +2457,15 @@ export async function restoreFromServer(chatId, options = {}) {
chatId: normalizedChatId,
filename: remoteResult.filename,
reason: "backup-version-mismatch",
timings: finalizeSyncTimings(
{
downloadMs: Number(downloadTimings.totalMs || 0),
lookupMs: Number(downloadTimings.lookupMs || 0),
networkMs: Number(downloadTimings.networkMs || 0),
envelopeParseMs: Number(downloadTimings.parseMs || 0),
},
restoreStartedAt,
),
};
}
@@ -2287,6 +2475,15 @@ export async function restoreFromServer(chatId, options = {}) {
chatId: normalizedChatId,
filename: remoteResult.filename,
reason: "backup-chat-id-mismatch",
timings: finalizeSyncTimings(
{
downloadMs: Number(downloadTimings.totalMs || 0),
lookupMs: Number(downloadTimings.lookupMs || 0),
networkMs: Number(downloadTimings.networkMs || 0),
envelopeParseMs: Number(downloadTimings.parseMs || 0),
},
restoreStartedAt,
),
};
}
@@ -2304,26 +2501,42 @@ export async function restoreFromServer(chatId, options = {}) {
chatId: normalizedChatId,
filename: remoteResult.filename,
reason: "snapshot-chat-id-mismatch",
timings: finalizeSyncTimings(
{
downloadMs: Number(downloadTimings.totalMs || 0),
lookupMs: Number(downloadTimings.lookupMs || 0),
networkMs: Number(downloadTimings.networkMs || 0),
envelopeParseMs: Number(downloadTimings.parseMs || 0),
},
restoreStartedAt,
),
};
}
const localExportStartedAt = readSyncTimingNow();
const localSnapshot = normalizeSyncSnapshot(
await db.exportSnapshot(),
normalizedChatId,
);
const localExportMs = readSyncTimingNow() - localExportStartedAt;
const safetySnapshotStartedAt = readSyncTimingNow();
await createRestoreSafetySnapshot(
normalizedChatId,
localSnapshot,
options,
);
const safetySnapshotMs = readSyncTimingNow() - safetySnapshotStartedAt;
const importStartedAt = readSyncTimingNow();
await db.importSnapshot(snapshot, {
mode: "replace",
preserveRevision: true,
revision: normalizeRevision(snapshot.meta.revision),
markSyncDirty: false,
});
const importMs = readSyncTimingNow() - importStartedAt;
const metaPatchStartedAt = readSyncTimingNow();
await patchDbMeta(db, {
deviceId: getOrCreateDeviceId(),
syncDirty: false,
@@ -2332,12 +2545,15 @@ export async function restoreFromServer(chatId, options = {}) {
lastBackupFilename:
remoteResult.filename || buildBackupFilename(normalizedChatId),
});
const metaPatchMs = readSyncTimingNow() - metaPatchStartedAt;
const hookStartedAt = readSyncTimingNow();
await invokeSyncAppliedHook(options, {
chatId: normalizedChatId,
action: "restore-backup",
revision: normalizeRevision(snapshot.meta.revision),
});
const hookMs = readSyncTimingNow() - hookStartedAt;
return {
restored: true,
@@ -2345,6 +2561,20 @@ export async function restoreFromServer(chatId, options = {}) {
filename: remoteResult.filename,
revision: normalizeRevision(snapshot.meta.revision),
backupTime: normalizeTimestamp(envelope.createdAt, 0),
timings: finalizeSyncTimings(
{
downloadMs: Number(downloadTimings.totalMs || 0),
lookupMs: Number(downloadTimings.lookupMs || 0),
networkMs: Number(downloadTimings.networkMs || 0),
envelopeParseMs: Number(downloadTimings.parseMs || 0),
localExportMs,
safetySnapshotMs,
importMs,
metaPatchMs,
hookMs,
},
restoreStartedAt,
),
};
} catch (error) {
console.warn("[ST-BME] 从云端恢复备份失败:", error);
@@ -2353,6 +2583,7 @@ export async function restoreFromServer(chatId, options = {}) {
chatId: normalizedChatId,
reason: "restore-error",
error,
timings: finalizeSyncTimings({}, restoreStartedAt),
};
}
}
@@ -2455,12 +2686,16 @@ export async function upload(chatId, options = {}) {
uploaded: false,
chatId: "",
reason: "missing-chat-id",
timings: finalizeSyncTimings({}, readSyncTimingNow()),
};
}
const uploadStartedAt = readSyncTimingNow();
try {
const db = await getDb(normalizedChatId, options);
const exportStartedAt = readSyncTimingNow();
const localSnapshot = normalizeSyncSnapshot(await db.exportSnapshot(), normalizedChatId);
const exportMs = readSyncTimingNow() - exportStartedAt;
const nowMs = Date.now();
const deviceId = getOrCreateDeviceId();
@@ -2469,7 +2704,9 @@ export async function upload(chatId, options = {}) {
localSnapshot.meta.lastModified = normalizeTimestamp(localSnapshot.meta.lastModified, nowMs);
const uploadResult = await writeSnapshotToRemote(localSnapshot, normalizedChatId, options);
const uploadTimings = uploadResult?.timings || {};
const metaPatchStartedAt = readSyncTimingNow();
await patchDbMeta(db, {
deviceId,
lastSyncUploadedAt: nowMs,
@@ -2479,6 +2716,7 @@ export async function upload(chatId, options = {}) {
lastModified: localSnapshot.meta.lastModified,
remoteSyncFormatVersion: BME_REMOTE_SYNC_FORMAT_VERSION_V2,
});
const metaPatchMs = readSyncTimingNow() - metaPatchStartedAt;
return {
uploaded: true,
@@ -2486,6 +2724,19 @@ export async function upload(chatId, options = {}) {
filename: uploadResult.filename,
remotePath: uploadResult.path,
revision: normalizeRevision(localSnapshot.meta.revision),
timings: finalizeSyncTimings(
{
exportMs,
envelopeBuildMs: Number(uploadTimings.envelopeBuildMs || 0),
chunkSerializeMs: Number(uploadTimings.chunkSerializeMs || 0),
chunkUploadMs: Number(uploadTimings.chunkUploadMs || 0),
manifestSerializeMs: Number(uploadTimings.manifestSerializeMs || 0),
manifestUploadMs: Number(uploadTimings.manifestUploadMs || 0),
responseParseMs: Number(uploadTimings.responseParseMs || 0),
metaPatchMs,
},
uploadStartedAt,
),
};
} catch (error) {
console.warn("[ST-BME] 上传同步文件失败:", error);
@@ -2494,6 +2745,7 @@ export async function upload(chatId, options = {}) {
chatId: normalizedChatId,
reason: "upload-error",
error,
timings: finalizeSyncTimings({}, uploadStartedAt),
};
}
}
@@ -2506,12 +2758,15 @@ export async function download(chatId, options = {}) {
exists: false,
chatId: "",
reason: "missing-chat-id",
timings: finalizeSyncTimings({}, readSyncTimingNow()),
};
}
const downloadStartedAt = readSyncTimingNow();
try {
const db = await getDb(normalizedChatId, options);
const remoteResult = await readRemoteSnapshot(normalizedChatId, options);
const remoteTimings = remoteResult?.timings || {};
if (!remoteResult.exists || !remoteResult.snapshot) {
return {
@@ -2520,6 +2775,16 @@ export async function download(chatId, options = {}) {
chatId: normalizedChatId,
filename: remoteResult.filename || "",
reason: remoteResult.status || "remote-missing",
timings: finalizeSyncTimings(
{
resolveCandidatesMs: Number(remoteTimings.resolveCandidatesMs || 0),
networkMs: Number(remoteTimings.networkMs || 0),
parseMs: Number(remoteTimings.parseMs || 0),
chunkReadMs: Number(remoteTimings.chunkReadMs || 0),
normalizeMs: Number(remoteTimings.normalizeMs || 0),
},
downloadStartedAt,
),
};
}
@@ -2530,13 +2795,16 @@ export async function download(chatId, options = {}) {
);
const remoteRevision = normalizeRevision(remoteSnapshot.meta.revision);
const importStartedAt = readSyncTimingNow();
await db.importSnapshot(remoteSnapshot, {
mode: "replace",
preserveRevision: true,
revision: remoteRevision,
markSyncDirty: false,
});
const importMs = readSyncTimingNow() - importStartedAt;
const metaPatchStartedAt = readSyncTimingNow();
await patchDbMeta(db, {
deviceId: getOrCreateDeviceId(),
lastSyncDownloadedAt: Date.now(),
@@ -2545,12 +2813,15 @@ export async function download(chatId, options = {}) {
syncDirtyReason: "",
remoteSyncFormatVersion: BME_REMOTE_SYNC_FORMAT_VERSION_V2,
});
const metaPatchMs = readSyncTimingNow() - metaPatchStartedAt;
const hookStartedAt = readSyncTimingNow();
await invokeSyncAppliedHook(options, {
chatId: normalizedChatId,
action: "download",
revision: remoteRevision,
});
const hookMs = readSyncTimingNow() - hookStartedAt;
return {
downloaded: true,
@@ -2558,6 +2829,19 @@ export async function download(chatId, options = {}) {
chatId: normalizedChatId,
filename: remoteResult.filename,
revision: remoteRevision,
timings: finalizeSyncTimings(
{
resolveCandidatesMs: Number(remoteTimings.resolveCandidatesMs || 0),
networkMs: Number(remoteTimings.networkMs || 0),
parseMs: Number(remoteTimings.parseMs || 0),
chunkReadMs: Number(remoteTimings.chunkReadMs || 0),
normalizeMs: Number(remoteTimings.normalizeMs || 0),
importMs,
metaPatchMs,
hookMs,
},
downloadStartedAt,
),
};
} catch (error) {
console.warn("[ST-BME] 下载同步文件失败:", error);
@@ -2567,6 +2851,7 @@ export async function download(chatId, options = {}) {
chatId: normalizedChatId,
reason: "download-error",
error,
timings: finalizeSyncTimings({}, downloadStartedAt),
};
}
}