Files
workinf_Blender_Wasm/web/tests/e2e/simulation-cache-performance.spec.ts
mes123456 0fe8d2bb56
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

136 lines
6.7 KiB
TypeScript

import { expect, test } from "@playwright/test";
test("meets the Chromium OPFS Simulation cache playback performance gate", async ({ page }) => {
test.setTimeout(45_000);
await page.goto("/");
const result = await page.evaluate(async () => {
const { StorageClient } = await import("/src/storage/StorageClient.ts");
const { BrowserTransformCachePlaybackSession } = await import("/src/simulation/BrowserTransformCachePlayback.ts");
const frameCount = 600;
const frameBytes = 88;
const digest = async (data: ArrayBuffer): Promise<string> => {
const hash = await crypto.subtle.digest("SHA-256", data);
return Array.from(new Uint8Array(hash), (value) => value.toString(16).padStart(2, "0")).join("");
};
const payload = new ArrayBuffer(frameCount * frameBytes);
const payloadBytes = new Uint8Array(payload);
const objectId = new TextEncoder().encode("object:CacheTarget");
for (let frame = 1; frame <= frameCount; frame += 1) {
const offset = (frame - 1) * frameBytes;
const view = new DataView(payload, offset, frameBytes);
view.setUint32(0, 0x31465442, true);
view.setUint16(4, 1, true);
view.setUint16(6, 16, true);
view.setInt32(8, frame, true);
view.setUint32(12, 1, true);
view.setUint8(16, objectId.length);
payloadBytes.set(objectId, offset + 17);
[frame / 10, 0, 0, 0, 0, 0, 1, 1, 1, 1].forEach((value, index) => view.setFloat32(48 + index * 4, value, true));
}
const frames = await Promise.all(Array.from({ length: frameCount }, async (_, index) => ({
frame: index + 1,
byteOffset: index * frameBytes,
byteLength: frameBytes,
sha256: await digest(payload.slice(index * frameBytes, (index + 1) * frameBytes)),
})));
const sourceBlend = Uint8Array.from([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]).buffer;
const fixedHash = await digest(Uint8Array.from([1, 2, 3]).buffer);
const binding = {
graphId: "geometry-node-tree:simulation-performance",
graphHash: fixedHash,
sourceBlendSha256: await digest(sourceBlend),
sourceRevision: 1,
inputHash: await digest(Uint8Array.from([4, 5, 6]).buffer),
blenderVersion: "5.2.0",
frameStart: 1,
frameEnd: frameCount,
};
const revisionHash = await digest(new TextEncoder().encode(JSON.stringify([
"blender-web-simulation-cache-revision-v2", binding.graphId, binding.graphHash,
binding.sourceBlendSha256, String(binding.sourceRevision), binding.inputHash,
binding.blenderVersion, String(binding.frameStart), String(binding.frameEnd),
])).buffer);
const manifest = {
schemaVersion: 2 as const,
...binding,
revisionHash,
cacheSha256: await digest(payload),
byteLength: payload.byteLength,
frames,
};
const projectId = `simulation-performance-${Date.now()}`;
const started = performance.now();
const writer = new StorageClient();
const saved = await writer.saveProject(projectId, 1, sourceBlend.slice(0));
const stored = await writer.putSimulationCache(projectId, manifest, payload);
writer.terminate();
const writerPendingAfterTerminate = writer.getPendingRequestCount();
const storedAt = performance.now();
const reader = new StorageClient();
await reader.prepareSimulationCachePlayback(projectId, stored.cacheKey);
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
const scene = {
schemaVersion: 1 as const,
revision: 1,
sceneId: "scene:CachePerformance",
source: { kind: "mock" as const },
coordinateSystem: { upAxis: "Z" as const, forwardAxis: "-Y" as const, handedness: "RIGHT" as const, unitSystem: 0, unitScale: 1 },
activeObjectId: "object:CacheTarget",
frame: { current: 1, start: 1, end: frameCount },
nodes: [{ id: "object:CacheTarget", name: "CacheTarget", type: "MESH" as const, parentId: null, dataId: "mesh:CacheTarget", visible: true, selectable: true, localMatrix: identity, worldMatrix: identity, transform: { translation: [0, 0, 0] as [number, number, number], rotationEuler: [0, 0, 0] as [number, number, number], scale: [1, 1, 1] as [number, number, number], rotationMode: 1 } }],
meshes: [], materials: [], cameras: [], lights: [], worlds: [], images: [], animations: [], collections: [], scenes: [],
};
let publishedFrames = 0;
let lastTranslation = 0;
const playback = new BrowserTransformCachePlaybackSession(scene, {
frameStart: 1,
frameEnd: frameCount,
readFrame: async (frame, signal) => {
if (signal.aborted) throw new DOMException("Playback aborted", "AbortError");
const read = await reader.readSimulationCacheFrame(projectId, stored.cacheKey, frame, signal);
if (signal.aborted) throw new DOMException("Playback aborted", "AbortError");
return read.data;
},
}, (preview) => {
publishedFrames += 1;
lastTranslation = preview.nodes[0].transform.translation[0];
});
const playbackResult = await playback.play();
await reader.releaseSimulationCachePlayback(projectId, stored.cacheKey);
const finished = performance.now();
reader.terminate();
const readerPendingAfterTerminate = reader.getPendingRequestCount();
const recovery = new StorageClient();
await recovery.prepareSimulationCachePlayback(projectId, stored.cacheKey);
const recovered = await recovery.readSimulationCacheFrame(projectId, stored.cacheKey, frameCount);
recovery.terminate();
const recoveryPendingAfterTerminate = recovery.getPendingRequestCount();
return {
backend: saved.backend,
frameCount,
byteLength: manifest.byteLength,
status: playbackResult.status,
appliedFrames: playbackResult.appliedFrames,
lastFrame: playbackResult.lastFrame,
publishedFrames,
lastTranslation,
storeMs: Math.round(storedAt - started),
playbackMs: Math.round(finished - storedAt),
elapsedMs: Math.round(finished - started),
peakCacheBytes: manifest.byteLength,
pendingRequestsAfterTerminate: writerPendingAfterTerminate + readerPendingAfterTerminate + recoveryPendingAfterTerminate,
workerRestartRecovered: recovered.data.byteLength === frameBytes,
};
});
console.log("simulation-cache-performance", JSON.stringify(result));
expect(result.backend).toBe("opfs");
expect(result).toMatchObject({ frameCount: 600, byteLength: 52_800, status: "COMPLETED", appliedFrames: 600, lastFrame: 600, publishedFrames: 600 });
expect(result.lastTranslation).toBeCloseTo(60, 5);
expect(result.storeMs).toBeLessThan(30_000);
expect(result.playbackMs).toBeLessThan(30_000);
expect(result.elapsedMs).toBeLessThan(30_000);
expect(result.pendingRequestsAfterTerminate).toBe(0);
expect(result.workerRestartRecovered).toBe(true);
});