Files
SystemSimulationApp/frontend/src/resultPersistence.ts
T
lujingze 3bc4be3c06 优化原生结果编码传输与浏览器缓存,记录八路性能基线
原生结果series通过字节索引直传,C端使用Ryu精确回读编码和64 KiB批量写出;网页采用Float64缓存和CSV工作线程,减少结果处理与保存等待。

补充八路AME曲线核查、全流程分阶段计时、独立编码基准和复现工具,固定后续优化采用修正八路及rtol=1e-8。C写出1.1808→0.1638 s,点击到可查看8.0100→6.9756 s。

验证:最终10项编码专项、29项相关后端回归通过;8份原生结果逐位一致,16次网页结果/CSV/刷新恢复通过。前端构建及缓存/CSV专项在本轮结果处理工作中通过。环境、原始大结果与临时构建不纳入Git。
2026-09-11 15:09:15 +00:00

223 lines
9.8 KiB
TypeScript

import type { SimulationResultsSnapshot } from "./SimulationResultsView";
export const RESULT_SNAPSHOT_KEY = "system-simulation-flow:latest-result";
const DATABASE = "system-simulation-results";
// Pack short output columns together: an eight-branch result needs seven writes,
// rather than one write per output column and fourteen serial transactions.
const PACKED_VALUES = 262144;
type Header = {
snapshot: SimulationResultsSnapshot;
lengths: Record<string, number>;
layout?: "packed-f64-v1";
};
type PackedChunk = { offset: number; values: Float64Array };
let database: Promise<IDBDatabase> | undefined;
let saveSequence = 0;
let pendingSaves = 0;
let ownedCacheId: string | undefined;
export function resultSavePending() { return pendingSaves > 0; }
function openDatabase() {
if (!database) database = new Promise<IDBDatabase>((resolve, reject) => {
let blocked = false;
const request = indexedDB.open(DATABASE, 1);
request.onupgradeneeded = () => {
request.result.createObjectStore("headers");
request.result.createObjectStore("chunks");
};
request.onerror = () => reject(request.error);
request.onblocked = () => {
blocked = true;
reject(new Error("结果数据库被其他页面占用,请关闭旧页面后重试"));
};
request.onsuccess = () => {
const db = request.result;
if (blocked) { db.close(); return; }
db.onversionchange = () => { db.close(); database = undefined; };
resolve(db);
};
}).catch((error) => { database = undefined; throw error; });
return database;
}
function writeSnapshot(db: IDBDatabase, cacheId: string, chunks: PackedChunk[], header: Header) {
return new Promise<void>((resolve, reject) => {
const transaction = db.transaction(["headers", "chunks"], "readwrite");
transaction.oncomplete = () => resolve();
transaction.onabort = () => reject(transaction.error ?? new Error("结果保存事务已中止"));
transaction.onerror = () => {}; // onabort reports the transaction failure once.
try {
const store = transaction.objectStore("chunks");
for (const chunk of chunks) store.put(chunk, [cacheId, chunk.offset]);
transaction.objectStore("headers").put(header, cacheId);
transaction.commit?.();
} catch (error) {
transaction.abort();
reject(error);
}
});
}
function deleteCache(db: IDBDatabase, cacheId: string) {
return new Promise<void>((resolve, reject) => {
const transaction = db.transaction(["headers", "chunks"], "readwrite");
transaction.oncomplete = () => resolve();
transaction.onabort = () => reject(transaction.error);
transaction.objectStore("headers").delete(cacheId);
transaction.objectStore("chunks").delete(IDBKeyRange.bound([cacheId], [cacheId, []]));
});
}
/** Publish the small session pointer only after the atomic snapshot write commits. */
export async function storeResultSnapshot(snapshot: SimulationResultsSnapshot) {
const sequence = ++saveSequence;
const cacheId = typeof crypto.randomUUID === "function"
? crypto.randomUUID()
: `${Date.now()}-${sequence}-${Math.random().toString(36).slice(2)}`;
pendingSaves++;
let db: IDBDatabase | undefined;
let committed = false;
let writeStarted = false;
try {
// Yield before copying so the ready result and controls can paint first.
await new Promise<void>((resolve) => setTimeout(resolve, 0));
if (sequence !== saveSequence) return false;
const columns = Object.entries(snapshot.result.series);
const lengths: Record<string, number> = Object.create(null);
let total = 0;
for (const [name, values] of columns) { lengths[name] = values.length; total += values.length; }
const chunks: PackedChunk[] = [];
let columnIndex = 0;
let columnOffset = 0;
for (let offset = 0; offset < total; offset += PACKED_VALUES) {
if (sequence !== saveSequence) return false;
const values = new Float64Array(Math.min(PACKED_VALUES, total - offset));
let written = 0;
while (written < values.length) {
const source = columns[columnIndex][1];
const count = Math.min(source.length - columnOffset, values.length - written);
if (columnOffset === 0 && count === source.length) values.set(source, written);
else for (let i = 0; i < count; i++) values[written + i] = source[columnOffset + i];
written += count;
columnOffset += count;
if (columnOffset === source.length) { columnIndex++; columnOffset = 0; }
}
chunks.push({ offset, values });
// Keep cancellation responsive without holding an idle IndexedDB transaction open.
if (offset + values.length < total) await new Promise<void>((resolve) => setTimeout(resolve, 0));
}
if (sequence !== saveSequence) return false;
db = await openDatabase();
if (sequence !== saveSequence) return false;
writeStarted = true;
await writeSnapshot(db, cacheId, chunks, {
snapshot: { ...snapshot, result: { ...snapshot.result, series: Object.create(null) } },
lengths, layout: "packed-f64-v1",
});
if (sequence !== saveSequence) return false;
sessionStorage.setItem(RESULT_SNAPSHOT_KEY, JSON.stringify({ storage: "indexeddb", version: 1, cacheId }));
committed = true;
// Only retire this page's own previous save, never a snapshot loaded from another tab.
const previous = ownedCacheId;
ownedCacheId = cacheId;
if (previous) void deleteCache(db, previous).catch(() => {});
return true;
} finally {
pendingSaves--;
if (db && writeStarted && !committed) await deleteCache(db, cacheId).catch(() => {});
}
}
function cacheError() { return new Error("结果缓存不完整或分块无效,请重新载入结果文件"); }
function restorePacked(header: Header, chunks: PackedChunk[]) {
const columns = Object.entries(header.lengths);
const total = columns.reduce((sum, [, length]) => sum + length, 0);
let loaded = 0;
let columnIndex = 0;
let columnOffset = 0;
for (const chunk of chunks) {
if (!chunk || chunk.offset !== loaded || !(chunk.values instanceof Float64Array) ||
chunk.values.length === 0 || loaded + chunk.values.length > total) throw cacheError();
let read = 0;
while (read < chunk.values.length) {
while (columnIndex < columns.length && columnOffset === columns[columnIndex][1]) {
columnIndex++; columnOffset = 0;
}
if (columnIndex >= columns.length) throw cacheError();
const [name, length] = columns[columnIndex];
const count = Math.min(length - columnOffset, chunk.values.length - read);
const target = header.snapshot.result.series[name];
for (let i = 0; i < count; i++) target[columnOffset + i] = chunk.values[read + i];
read += count;
columnOffset += count;
}
loaded += chunk.values.length;
}
if (loaded !== total) throw cacheError();
}
export async function loadStoredResultSnapshot(): Promise<unknown | null> {
const raw = sessionStorage.getItem(RESULT_SNAPSHOT_KEY);
if (!raw) return null;
const value = JSON.parse(raw);
// Read legacy sessionStorage data without rewriting or duplicating it at startup.
if (value?.storage !== "indexeddb") return value;
if (value.version !== 1 || typeof value.cacheId !== "string") return null;
const db = await openDatabase();
return new Promise<SimulationResultsSnapshot | null>((resolve, reject) => {
const transaction = db.transaction(["headers", "chunks"], "readonly");
let result: SimulationResultsSnapshot | null = null;
let failure: unknown = null;
transaction.oncomplete = () => failure ? reject(failure) : resolve(result);
transaction.onabort = () => reject(transaction.error);
const request = transaction.objectStore("headers").get(value.cacheId);
request.onsuccess = () => {
const header = request.result as Header | undefined;
if (!header) { failure = new Error("结果缓存已丢失,请重新载入结果文件"); return; }
try {
result = header.snapshot;
for (const [name, length] of Object.entries(header.lengths)) {
if (!Number.isSafeInteger(length) || length < 0) throw cacheError();
result.result.series[name] = new Array<number>(length);
}
const range = IDBKeyRange.bound([value.cacheId], [value.cacheId, []]);
if (header.layout === "packed-f64-v1") {
// A handful of packed records can be restored with one IndexedDB delivery.
const blocks = transaction.objectStore("chunks").getAll(range);
blocks.onsuccess = () => {
try { restorePacked(header, blocks.result as PackedChunk[]); }
catch (error) { failure = error; }
};
return;
}
if (header.layout !== undefined) throw cacheError();
// Compatibility with the original [cacheId, columnName, offset] records.
const loaded: Record<string, number> = Object.create(null);
const cursor = transaction.objectStore("chunks").openCursor(range);
cursor.onsuccess = () => {
const entry = cursor.result;
if (!entry) {
for (const [name, length] of Object.entries(header.lengths)) {
if ((loaded[name] ?? 0) !== length) failure = cacheError();
}
return;
}
const [, name, offset] = entry.key as [string, string, number];
const block = entry.value as Float64Array;
const values = result!.result.series[name];
if (!values || !(block instanceof Float64Array) || offset !== (loaded[name] ?? 0) ||
offset + block.length > values.length) {
failure = cacheError();
} else {
for (let i = 0; i < block.length; i++) values[offset + i] = block[i];
loaded[name] = offset + block.length;
}
entry.continue();
};
} catch (error) { failure = error; }
};
});
}