优化原生结果编码传输与浏览器缓存,记录八路性能基线
原生结果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
@@ -0,0 +1,128 @@
|
||||
export type ResultCsvInput = {
|
||||
variables: readonly { key: string }[];
|
||||
series: Readonly<Record<string, readonly number[]>>;
|
||||
};
|
||||
|
||||
export type ResultCsvWorkerMessage =
|
||||
| { type: "start"; keys: string[]; rowCount: number }
|
||||
| { type: "chunk"; offset: number; values: Float64Array }
|
||||
| { type: "finish" };
|
||||
export type ResultCsvWorkerResponse =
|
||||
| { type: "complete"; blob: Blob }
|
||||
| { type: "error"; message: string };
|
||||
|
||||
function csvColumns(input: ResultCsvInput) {
|
||||
const times = input.series.time;
|
||||
if (!Array.isArray(times) || !times.length) {
|
||||
throw new Error("Simulation results must contain a non-empty time series.");
|
||||
}
|
||||
const variableKeys = input.variables.map((variable) => variable.key);
|
||||
if (!variableKeys.length) {
|
||||
throw new Error("Simulation results do not contain exportable variables.");
|
||||
}
|
||||
if (new Set(variableKeys).size !== variableKeys.length) {
|
||||
throw new Error("Simulation result metadata contains duplicate variable keys.");
|
||||
}
|
||||
const keys = ["time", ...variableKeys];
|
||||
const expectedKeys = new Set(keys);
|
||||
const missing = [...expectedKeys].filter((key) => !Object.hasOwn(input.series, key)).sort();
|
||||
const unknown = Object.keys(input.series).filter((key) => !expectedKeys.has(key)).sort();
|
||||
if (missing.length || unknown.length) {
|
||||
const details = [missing.length ? `missing ${missing.join(", ")}` : "", unknown.length ? `unmapped ${unknown.join(", ")}` : ""].filter(Boolean);
|
||||
throw new Error(`Simulation result columns do not match metadata: ${details.join("; ")}.`);
|
||||
}
|
||||
for (const key of keys) {
|
||||
if (!Array.isArray(input.series[key]) || input.series[key].length !== times.length) {
|
||||
throw new Error(`Simulation result column '${key}' has an inconsistent length.`);
|
||||
}
|
||||
}
|
||||
return { keys, rowCount: times.length };
|
||||
}
|
||||
|
||||
export function resultCsvFilename(projectName: string): string {
|
||||
const stem = projectName.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "_").replace(/^[ .]+|[ .]+$/g, "");
|
||||
return `${Array.from(stem).slice(0, 80).join("") || "simulation"}-results.csv`;
|
||||
}
|
||||
|
||||
/** Transfer at most 1 MiB per task; never stringify or clone the full result. */
|
||||
export function exportResultCsv(input: ResultCsvInput, signal?: AbortSignal): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (signal?.aborted) {
|
||||
reject(new DOMException("CSV export was cancelled.", "AbortError"));
|
||||
return;
|
||||
}
|
||||
let worker: Worker | undefined;
|
||||
let settled = false;
|
||||
let yieldTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let resumeYield: (() => void) | undefined;
|
||||
const finish = (error?: unknown, blob?: Blob) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
signal?.removeEventListener("abort", abort);
|
||||
if (yieldTimer !== undefined) clearTimeout(yieldTimer);
|
||||
resumeYield?.();
|
||||
worker?.terminate();
|
||||
if (error !== undefined) reject(error);
|
||||
else resolve(blob!);
|
||||
};
|
||||
const abort = () => finish(new DOMException("CSV export was cancelled.", "AbortError"));
|
||||
signal?.addEventListener("abort", abort, { once: true });
|
||||
const yieldMainThread = () => new Promise<void>((resume) => {
|
||||
resumeYield = resume;
|
||||
yieldTimer = setTimeout(() => {
|
||||
yieldTimer = undefined;
|
||||
resumeYield = undefined;
|
||||
resume();
|
||||
}, 0);
|
||||
});
|
||||
try {
|
||||
const { keys, rowCount } = csvColumns(input);
|
||||
worker = new Worker(new URL("./resultCsv.worker.ts", import.meta.url), { type: "module" });
|
||||
worker.onmessage = ({ data }: MessageEvent<ResultCsvWorkerResponse>) => {
|
||||
if (data.type === "complete" && data.blob instanceof Blob) finish(undefined, data.blob);
|
||||
else finish(new Error(data.type === "error" ? data.message : "CSV worker returned an invalid response."));
|
||||
};
|
||||
worker.onerror = (event) => {
|
||||
event.preventDefault();
|
||||
finish(new Error(event.message || "CSV worker failed."));
|
||||
};
|
||||
worker.onmessageerror = () => finish(new Error("CSV worker response could not be read."));
|
||||
const transfer = async () => {
|
||||
// Let the pending indicator render before copying any numeric payload.
|
||||
await yieldMainThread();
|
||||
if (settled) return;
|
||||
worker!.postMessage({ type: "start", keys, rowCount } satisfies ResultCsvWorkerMessage);
|
||||
const total = keys.length * rowCount;
|
||||
let column = 0;
|
||||
let row = 0;
|
||||
for (let offset = 0; offset < total;) {
|
||||
if (settled) return;
|
||||
const batch = new Float64Array(Math.min(128 * 1024, total - offset));
|
||||
let filled = 0;
|
||||
while (filled < batch.length) {
|
||||
const source = input.series[keys[column]];
|
||||
const count = Math.min(rowCount - row, batch.length - filled);
|
||||
for (let index = 0; index < count; index++) {
|
||||
const value = source[row + index];
|
||||
if (typeof value !== "number") {
|
||||
throw new Error(`Simulation result column '${keys[column]}' contains non-numeric values.`);
|
||||
}
|
||||
batch[filled + index] = value;
|
||||
}
|
||||
row += count;
|
||||
filled += count;
|
||||
if (row === rowCount) { column++; row = 0; }
|
||||
}
|
||||
const transferredCount = batch.length;
|
||||
worker!.postMessage({ type: "chunk", offset, values: batch } satisfies ResultCsvWorkerMessage, [batch.buffer]);
|
||||
offset += transferredCount;
|
||||
if (offset < total) await yieldMainThread();
|
||||
}
|
||||
if (!settled) worker!.postMessage({ type: "finish" } satisfies ResultCsvWorkerMessage);
|
||||
};
|
||||
void transfer().catch(finish);
|
||||
} catch (error) {
|
||||
finish(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user