136 lines
6.7 KiB
TypeScript
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);
|
|
});
|