优化原生结果编码传输与浏览器缓存,记录八路性能基线
原生结果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。
This commit is contained in:
1 parent
808c484f5b
commit
3bc4be3c06
55 files changed
+20522
-141
No files matched your search
@@ -2,13 +2,15 @@ import type { SimulationResultsSnapshot } from "./SimulationResultsView";
|
||||
|
||||
export const RESULT_SNAPSHOT_KEY = "system-simulation-flow:latest-result";
|
||||
const DATABASE = "system-simulation-results";
|
||||
const CHUNK_SIZE = 32768;
|
||||
const BATCH_VALUES = 131072;
|
||||
// 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 Chunk = { key: IDBValidKey; values: Float64Array };
|
||||
type PackedChunk = { offset: number; values: Float64Array };
|
||||
let database: Promise<IDBDatabase> | undefined;
|
||||
let saveSequence = 0;
|
||||
let pendingSaves = 0;
|
||||
@@ -39,15 +41,17 @@ function openDatabase() {
|
||||
return database;
|
||||
}
|
||||
|
||||
function writeBatch(db: IDBDatabase, chunks: Chunk[], header?: [string, Header]) {
|
||||
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 {
|
||||
for (const chunk of chunks) transaction.objectStore("chunks").put(chunk.values, chunk.key);
|
||||
if (header) transaction.objectStore("headers").put(header[1], header[0]);
|
||||
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);
|
||||
@@ -65,7 +69,7 @@ function deleteCache(db: IDBDatabase, cacheId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Publish the small session pointer only after every data chunk is durably committed. */
|
||||
/** 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"
|
||||
@@ -74,49 +78,86 @@ export async function storeResultSnapshot(snapshot: SimulationResultsSnapshot) {
|
||||
pendingSaves++;
|
||||
let db: IDBDatabase | undefined;
|
||||
let committed = false;
|
||||
let writeStarted = false;
|
||||
try {
|
||||
// Yield before serialization so the ready result and controls can paint first.
|
||||
// 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;
|
||||
db = await openDatabase();
|
||||
const columns = Object.entries(snapshot.result.series);
|
||||
const lengths: Record<string, number> = Object.create(null);
|
||||
let batch: Chunk[] = [];
|
||||
let batchSize = 0;
|
||||
for (const [name, values] of Object.entries(snapshot.result.series)) {
|
||||
lengths[name] = values.length;
|
||||
for (let offset = 0; offset < values.length; offset += CHUNK_SIZE) {
|
||||
if (sequence !== saveSequence) return false;
|
||||
const length = Math.min(CHUNK_SIZE, values.length - offset);
|
||||
const block = new Float64Array(length);
|
||||
for (let i = 0; i < length; i++) block[i] = values[offset + i];
|
||||
batch.push({ key: [cacheId, name, offset], values: block });
|
||||
batchSize += length;
|
||||
if (batchSize >= BATCH_VALUES) {
|
||||
await writeBatch(db, batch);
|
||||
batch = [];
|
||||
batchSize = 0;
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
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;
|
||||
await writeBatch(db, batch, [cacheId, {
|
||||
snapshot: { ...snapshot, result: { ...snapshot.result, series: Object.create(null) } }, lengths,
|
||||
}]);
|
||||
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 another tab's loaded snapshot.
|
||||
// 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 && !committed) await deleteCache(db, cacheId).catch(() => {});
|
||||
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;
|
||||
@@ -128,38 +169,54 @@ export async function loadStoredResultSnapshot(): Promise<unknown | null> {
|
||||
return new Promise<SimulationResultsSnapshot | null>((resolve, reject) => {
|
||||
const transaction = db.transaction(["headers", "chunks"], "readonly");
|
||||
let result: SimulationResultsSnapshot | null = null;
|
||||
let failure: Error | 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; }
|
||||
result = header.snapshot;
|
||||
for (const [name, length] of Object.entries(header.lengths)) {
|
||||
result.result.series[name] = new Array<number>(length);
|
||||
}
|
||||
const loaded: Record<string, number> = Object.create(null);
|
||||
const cursor = transaction.objectStore("chunks").openCursor(IDBKeyRange.bound([value.cacheId], [value.cacheId, []]));
|
||||
cursor.onsuccess = () => {
|
||||
const entry = cursor.result;
|
||||
if (!entry) {
|
||||
for (const [name, length] of Object.entries(header.lengths)) {
|
||||
if ((loaded[name] ?? 0) !== length) failure = new Error("结果缓存不完整,请重新载入结果文件");
|
||||
}
|
||||
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;
|
||||
}
|
||||
const [, name, offset] = entry.key as [string, string, number];
|
||||
const block = entry.value as Float64Array;
|
||||
const values = result!.result.series[name];
|
||||
if (!values || offset < 0 || offset + block.length > values.length) {
|
||||
failure = new Error("结果缓存分块无效");
|
||||
} else {
|
||||
for (let i = 0; i < block.length; i++) values[offset + i] = block[i];
|
||||
loaded[name] = (loaded[name] ?? 0) + block.length;
|
||||
}
|
||||
entry.continue();
|
||||
};
|
||||
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; }
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user