Files
workinf_Blender_Wasm/web/tests/e2e/long-media-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

293 lines
15 KiB
TypeScript

import { expect, test } from "@playwright/test";
import fs from "node:fs";
import path from "node:path";
const imagePath = path.resolve(import.meta.dirname, "../../../tests/files/web/media/sequencer-frame.png");
const audioPath = path.resolve(import.meta.dirname, "../../../tests/files/web/media/sequencer-silence.wav");
test("indexes, seeks, cancels and reopens a bounded one-million-frame media timeline", async ({ page }) => {
test.setTimeout(120_000);
await page.goto("/");
const image = fs.readFileSync(imagePath);
const audio = fs.readFileSync(audioPath);
const result = await page.evaluate(async ({ imageBytes, audioBytes }) => {
const [mediaModule, storageModule] = await Promise.all([
import("/src/sequencer/LongMediaTimeline.ts"),
import("/src/storage/StorageClient.ts"),
]);
const {
buildLongMediaTimelineIndex,
deserializeLongMediaSessionManifest,
gateSequencerCodec,
parseLongMediaSessionManifest,
sequencerRuntimeCapabilities,
serializeLongMediaSessionManifest,
} = mediaModule;
const { StorageClient } = storageModule;
const started = performance.now();
const projectId = `long-media-${Date.now()}`;
const cacheMaxBytes = 20 * 1024;
const frameCount = 1_000_000;
const stripFrames = 100;
const imageStripCount = frameCount / stripFrames;
const timeline = {
schemaVersion: 1 as const,
id: "sequencer:long-media",
revision: 1,
frameStart: 0,
frameEnd: frameCount,
fpsNumerator: 24_000,
fpsDenominator: 1_001,
strips: [
{
id: "strip:long-audio", name: "Long Audio", type: "SOUND" as const, channel: 1,
frameStart: 0, frameEnd: frameCount, sourceStart: 0, sourceEnd: frameCount,
speed: 1, muted: false, locked: false, sourceId: "media:audio", sourcePath: "//media/sequencer-silence.wav", mimeType: "audio/wav",
},
...Array.from({ length: imageStripCount }, (_, index) => ({
id: `strip:image:${index}`, name: `Image ${index}`, type: "IMAGE" as const, channel: 2,
frameStart: index * stripFrames, frameEnd: (index + 1) * stripFrames, sourceStart: 0, sourceEnd: 1,
speed: 1, muted: false, locked: false, sourceId: "media:image", sourcePath: "//media/sequencer-frame.png", mimeType: "image/png",
})),
],
};
const writer = new StorageClient();
const storedImage = await writer.putAsset(projectId, imageBytes.buffer.slice(imageBytes.byteOffset, imageBytes.byteOffset + imageBytes.byteLength), "image/png", "media/sequencer-frame.png");
const storedAudio = await writer.putAsset(projectId, audioBytes.buffer.slice(audioBytes.byteOffset, audioBytes.byteOffset + audioBytes.byteLength), "audio/wav", "media/sequencer-silence.wav");
const cancellation = new AbortController();
const cancelledBuild = buildLongMediaTimelineIndex(timeline, cancellation.signal)
.then(() => "unexpected-success")
.catch((error: unknown) => error instanceof Error ? error.message.split(":", 1)[0] : String(error));
setTimeout(() => cancellation.abort(), 0);
const indexCancelCode = await cancelledBuild;
const indexStarted = performance.now();
const directIndex = await buildLongMediaTimelineIndex(timeline, new AbortController().signal);
const indexBuildMs = Math.round(performance.now() - indexStarted);
const seekCancellation = new AbortController();
seekCancellation.abort();
let seekCancelCode = "";
try { directIndex.resolve(0, seekCancellation.signal); }
catch (error) { seekCancelCode = error instanceof Error ? error.message.split(":", 1)[0] : String(error); }
interface WorkerClient {
worker: Worker;
init(timelineValue: unknown, assets: Array<{ sourceId: string; data: ArrayBuffer }>): Promise<{ stripCount: number; bucketCount: number; referenceCount: number; estimatedBytes: number }>;
seek(frame: number, decodeDelayMs?: number): Promise<import("/src/sequencer/LongMediaTimeline.ts").LongMediaSeekResultIR>;
cancel(): void;
dispose(): Promise<{ releasedCacheBytes: number; cacheBytesAfter: number }>;
terminate(): void;
}
const createWorkerClient = (): WorkerClient => {
const worker = new Worker("/src/workers/long-media.worker.ts", { type: "module" });
let counter = 0;
let readyResolve: ((value: { stripCount: number; bucketCount: number; referenceCount: number; estimatedBytes: number }) => void) | undefined;
let readyReject: ((reason: Error) => void) | undefined;
let disposeResolve: ((status: { releasedCacheBytes: number; cacheBytesAfter: number }) => void) | undefined;
const pending = new Map<string, { resolve: (value: import("/src/sequencer/LongMediaTimeline.ts").LongMediaSeekResultIR) => void; reject: (reason: Error) => void }>();
worker.onmessage = (event: MessageEvent<any>) => {
const message = event.data;
if (message.type === "ready") { readyResolve?.(message.index); readyResolve = undefined; readyReject = undefined; return; }
if (message.type === "disposed") { disposeResolve?.({ releasedCacheBytes: message.releasedCacheBytes, cacheBytesAfter: message.cacheBytesAfter }); disposeResolve = undefined; return; }
if (message.type === "error") {
if (message.requestId) { pending.get(message.requestId)?.reject(new Error(message.message)); pending.delete(message.requestId); }
else { readyReject?.(new Error(message.message)); readyResolve = undefined; readyReject = undefined; }
return;
}
if (message.type === "seekResult") {
pending.get(message.requestId)?.resolve(message.result);
pending.delete(message.requestId);
}
};
worker.onerror = (event) => {
const error = new Error(event.message);
readyReject?.(error);
for (const request of pending.values()) request.reject(error);
pending.clear();
};
return {
worker,
init: (timelineValue, assets) => new Promise((resolve, reject) => {
readyResolve = resolve;
readyReject = reject;
worker.postMessage({ type: "init", timeline: timelineValue, assets, cacheMaxBytes }, assets.map((asset) => asset.data));
}),
seek: (frame, decodeDelayMs = 0) => new Promise((resolve, reject) => {
const requestId = `seek-${++counter}`;
pending.set(requestId, { resolve, reject });
worker.postMessage({ type: "seek", requestId, frame, decodeDelayMs });
}),
cancel: () => worker.postMessage({ type: "cancel" }),
dispose: () => new Promise((resolve) => { disposeResolve = resolve; worker.postMessage({ type: "dispose" }); }),
terminate: () => worker.terminate(),
};
};
const sourceImage = await writer.readAsset(projectId, storedImage.sha256);
const sourceAudio = await writer.readAsset(projectId, storedAudio.sha256);
const client = createWorkerClient();
const workerIndex = await client.init(timeline, [
{ sourceId: "media:image", data: sourceImage.data },
{ sourceId: "media:audio", data: sourceAudio.data },
]);
const coldStarted = performance.now();
const cold = await client.seek(123_450);
const coldSeekMs = Math.round(performance.now() - coldStarted);
const hotStarted = performance.now();
const hot = await client.seek(123_450);
const hotSeekMs = Math.round(performance.now() - hotStarted);
const endpoints = await Promise.all([0, 500_000, 999_999].map((frame) => client.seek(frame)));
const randomFrames = Array.from({ length: 64 }, (_, index) => (index * 104_729) % frameCount);
const randomStarted = performance.now();
const random = [];
for (const frame of randomFrames) random.push(await client.seek(frame));
const randomSeekMs = Math.round(performance.now() - randomStarted);
const supersededPromise = client.seek(250_000, 50);
const latestPromise = client.seek(765_432, 0);
const [superseded, latest] = await Promise.all([supersededPromise, latestPromise]);
const cancelledSeekPromise = client.seek(333_333, 100);
setTimeout(() => client.cancel(), 0);
const cancelledSeek = await cancelledSeekPromise;
const manifest = parseLongMediaSessionManifest({
schemaVersion: 1,
timeline,
currentFrame: latest.frame,
cacheMaxBytes,
assets: [
{ sourceId: "media:image", sha256: storedImage.sha256, mimeType: "image/png" },
{ sourceId: "media:audio", sha256: storedAudio.sha256, mimeType: "audio/wav" },
],
});
const serialized = serializeLongMediaSessionManifest(manifest);
const manifestSha256 = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", serialized.slice(0))), (value) => value.toString(16).padStart(2, "0")).join("");
const storedSession = await writer.putAsset(projectId, serialized, "application/vnd.blender.long-media+json", "cache/long-media-session.json");
const disposed = await client.dispose();
client.terminate();
writer.terminate();
const reader = new StorageClient();
const reopenedSessionAsset = await reader.readAsset(projectId, storedSession.sha256);
const reopenedManifest = deserializeLongMediaSessionManifest(reopenedSessionAsset.data);
const reopenedImage = await reader.readAsset(projectId, reopenedManifest.assets.find((asset) => asset.sourceId === "media:image")!.sha256);
const reopenedAudio = await reader.readAsset(projectId, reopenedManifest.assets.find((asset) => asset.sourceId === "media:audio")!.sha256);
reader.terminate();
const restarted = createWorkerClient();
const restartedIndex = await restarted.init(reopenedManifest.timeline, [
{ sourceId: "media:image", data: reopenedImage.data },
{ sourceId: "media:audio", data: reopenedAudio.data },
]);
const reopenedSeek = await restarted.seek(reopenedManifest.currentFrame);
const restartedDisposed = await restarted.dispose();
restarted.terminate();
const codecRequest = {
schemaVersion: 1 as const,
stripType: "MOVIE" as const,
mimeType: "video/mp4",
byteLength: 1,
sourceSha256: "0".repeat(64),
};
const codec = gateSequencerCodec(codecRequest, {
...codecRequest,
status: "BLOCKED",
backend: null,
reason: "RUNTIME_UNAVAILABLE",
decoded: null,
});
const runtime = sequencerRuntimeCapabilities();
let corruptManifestCode = "";
try { parseLongMediaSessionManifest({ ...manifest, assets: [{ ...manifest.assets[0], sha256: "bad" }, manifest.assets[1]] }); }
catch (error) { corruptManifestCode = error instanceof Error ? error.message.split(":", 1)[0] : String(error); }
return {
frameCount,
stripCount: timeline.strips.length,
localBytes: { image: imageBytes.byteLength, audio: audioBytes.byteLength, session: reopenedSessionAsset.data.byteLength },
indexCancelCode,
seekCancelCode,
indexBuildMs,
directIndex: directIndex.stats,
workerIndex,
restartedIndex,
cold: { status: cold.status, strips: cold.strips.length, cache: cold.cache, elapsedMs: coldSeekMs },
hot: { status: hot.status, strips: hot.strips.length, cache: hot.cache, elapsedMs: hotSeekMs },
endpoints: endpoints.map((item) => [item.frame, item.status, item.strips.length]),
randomCompleted: random.filter((item) => item.status === "COMPLETED").length,
randomCache: random.at(-1)?.cache,
randomSeekMs,
latest: [superseded.status, latest.status, latest.frame],
cancelledSeek: cancelledSeek.status,
disposed,
restartedDisposed,
manifestSha256,
storedSessionSha256: storedSession.sha256,
reopenedFrame: reopenedSeek.frame,
reopenedStatus: reopenedSeek.status,
codec: { status: codec.status, code: codec.issues[0]?.code },
localEncoding: runtime.localEncoding,
corruptManifestCode,
elapsedMs: Math.round(performance.now() - started),
};
}, { imageBytes: new Uint8Array(image), audioBytes: new Uint8Array(audio) });
console.log("long-media-performance", JSON.stringify({
frameCount: result.frameCount,
stripCount: result.stripCount,
referenceCount: result.workerIndex.referenceCount,
indexBuildMs: result.indexBuildMs,
coldSeekMs: result.cold.elapsedMs,
hotSeekMs: result.hot.elapsedMs,
randomSeekMs: result.randomSeekMs,
cacheBytes: result.randomCache?.bytes,
evictions: result.randomCache?.evictions,
releasedCacheBytes: result.disposed.releasedCacheBytes,
cacheBytesAfterDispose: result.disposed.cacheBytesAfter,
restartedReleasedCacheBytes: result.restartedDisposed.releasedCacheBytes,
restartedCacheBytesAfterDispose: result.restartedDisposed.cacheBytesAfter,
workerRestartRecovered: result.reopenedStatus === "COMPLETED",
manifestSha256: result.manifestSha256,
elapsedMs: result.elapsedMs,
}));
expect(result).toMatchObject({
frameCount: 1_000_000,
stripCount: 10_001,
indexCancelCode: "SEQUENCER_CANCELLED",
seekCancelCode: "SEQUENCER_CANCELLED",
latest: ["SUPERSEDED", "COMPLETED", 765_432],
cancelledSeek: "CANCELLED",
reopenedFrame: 765_432,
reopenedStatus: "COMPLETED",
codec: { status: "BLOCKED", code: "SEQUENCER_CODEC_UNSUPPORTED" },
localEncoding: "BLOCKED",
corruptManifestCode: "SEQUENCER_SCHEMA_INVALID",
});
expect(result.localBytes).toMatchObject({ image: 261, audio: 16_044 });
expect(result.localBytes.session).toBeGreaterThan(1_000_000);
expect(result.directIndex).toEqual(result.workerIndex);
expect(result.restartedIndex).toEqual(result.workerIndex);
expect(result.workerIndex.stripCount).toBe(10_001);
expect(result.workerIndex.referenceCount).toBeLessThan(25_000);
expect(result.workerIndex.estimatedBytes).toBeLessThan(256 * 1024);
expect(result.indexBuildMs).toBeLessThan(10_000);
expect(result.cold).toMatchObject({ status: "COMPLETED", strips: 2 });
expect(result.hot).toMatchObject({ status: "COMPLETED", strips: 2 });
expect(result.hot.cache.hits).toBeGreaterThan(result.cold.cache.hits);
expect(result.hot.cache.bytes).toBeLessThanOrEqual(result.hot.cache.maxBytes);
expect(result.endpoints).toEqual([[0, "COMPLETED", 2], [500_000, "COMPLETED", 2], [999_999, "COMPLETED", 2]]);
expect(result.randomCompleted).toBe(64);
expect(result.randomCache?.bytes).toBeLessThanOrEqual(20 * 1024);
expect(result.randomCache?.evictions).toBeGreaterThan(0);
expect(result.randomCache?.keys).toEqual(expect.arrayContaining(["media:audio:597927", "media:image:1"]));
expect(result.randomSeekMs).toBeLessThan(15_000);
expect(result.disposed.releasedCacheBytes).toBeLessThanOrEqual(20 * 1024);
expect(result.disposed.cacheBytesAfter).toBe(0);
expect(result.restartedDisposed.releasedCacheBytes).toBeLessThanOrEqual(20 * 1024);
expect(result.restartedDisposed.cacheBytesAfter).toBe(0);
expect(result.manifestSha256).toBe(result.storedSessionSha256);
expect(result.manifestSha256).toBe("d2e7e3c5ed9ae4fda6fe358cebb474f5774f1d2037dff3df9b6dfde9e0bebd2d");
expect(result.elapsedMs).toBeLessThan(30_000);
});