feat(web): preload completed wasm and load debug modules on demand
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#202124" />
|
||||
<title>Web Blender Modeler V1</title>
|
||||
<link rel="preload" as="fetch" type="application/wasm" crossorigin href="/vendor/blender/single/web_engine.wasm" />
|
||||
<link rel="stylesheet" href="/src/app/app-shell.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -3,26 +3,9 @@
|
||||
"releaseId": "blender-wasm-0.1.0-rc.1",
|
||||
"modules": [
|
||||
{
|
||||
"id": "core-completed",
|
||||
"id": "core-runtime-completed",
|
||||
"lifecycle": "complete",
|
||||
"preload": true,
|
||||
"resources": {
|
||||
"single": {
|
||||
"fileName": "web_engine.wasm",
|
||||
"url": "/vendor/blender/web_engine.wasm",
|
||||
"sha256": "66fc59cba55a8c17d398b74d12ea4a9f09746c0f30c40f579c44aa34956e3072"
|
||||
},
|
||||
"pthread": {
|
||||
"fileName": "web_engine.wasm",
|
||||
"url": "/vendor/blender/pthread/web_engine.wasm",
|
||||
"sha256": "f2a26a9221b5d4d5e710463a89e2d93faf9b2d8a9076a455c5ce1a0e52d88b5f"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "debug-incremental",
|
||||
"lifecycle": "debug",
|
||||
"preload": false,
|
||||
"resources": {
|
||||
"single": {
|
||||
"fileName": "web_engine.wasm",
|
||||
@@ -35,6 +18,41 @@
|
||||
"sha256": "f2a26a9221b5d4d5e710463a89e2d93faf9b2d8a9076a455c5ce1a0e52d88b5f"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mesh-editing-completed",
|
||||
"lifecycle": "complete",
|
||||
"preload": true,
|
||||
"resources": {
|
||||
"single": { "fileName": "web_engine.wasm", "url": "/vendor/blender/single/web_engine.wasm", "sha256": "7f3a50f1d41b2ac0e6366e84caaabefc3cdfc47c055d5ea459abec654da6fcc0" },
|
||||
"pthread": { "fileName": "web_engine.wasm", "url": "/vendor/blender/pthread/web_engine.wasm", "sha256": "f2a26a9221b5d4d5e710463a89e2d93faf9b2d8a9076a455c5ce1a0e52d88b5f" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reader-capabilities-completed",
|
||||
"lifecycle": "complete",
|
||||
"preload": true,
|
||||
"resources": {
|
||||
"single": { "fileName": "web_engine.wasm", "url": "/vendor/blender/single/web_engine.wasm", "sha256": "7f3a50f1d41b2ac0e6366e84caaabefc3cdfc47c055d5ea459abec654da6fcc0" },
|
||||
"pthread": { "fileName": "web_engine.wasm", "url": "/vendor/blender/pthread/web_engine.wasm", "sha256": "f2a26a9221b5d4d5e710463a89e2d93faf9b2d8a9076a455c5ce1a0e52d88b5f" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "debug-incremental",
|
||||
"lifecycle": "debug",
|
||||
"preload": false,
|
||||
"resources": {
|
||||
"single": {
|
||||
"fileName": "web_engine.wasm",
|
||||
"url": "/vendor/blender/web_engine.wasm",
|
||||
"sha256": "66fc59cba55a8c17d398b74d12ea4a9f09746c0f30c40f579c44aa34956e3072"
|
||||
},
|
||||
"pthread": {
|
||||
"fileName": "web_engine.wasm",
|
||||
"url": "/vendor/blender/pthread/web_engine.wasm",
|
||||
"sha256": "f2a26a9221b5d4d5e710463a89e2d93faf9b2d8a9076a455c5ce1a0e52d88b5f"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -83,6 +83,17 @@ export class WebEngineClient {
|
||||
return (await this.request({ type: "init" })).status;
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
if (!this.worker) return;
|
||||
try {
|
||||
await this.request({ type: "shutdown" });
|
||||
}
|
||||
finally {
|
||||
this.worker?.terminate();
|
||||
this.worker = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Load one debug WASM module without replacing the completed engine module. */
|
||||
async loadDebugWasmModule(id: string): Promise<NonNullable<WebEngineResult["wasmModules"]>> {
|
||||
const result = await this.request({ type: "loadWasmModule", id });
|
||||
|
||||
@@ -37,20 +37,27 @@ export function createWasmModuleLoader(
|
||||
const bytes = new Map<string, ArrayBuffer>();
|
||||
const statuses = new Map<string, WasmModuleState>(manifest.modules.map((entry) => [entry.id, { id: entry.id, lifecycle: entry.lifecycle, status: "pending", byteLength: 0 }]));
|
||||
const loading = new Map<string, Promise<ArrayBuffer>>();
|
||||
const markLoaded = (entry: WasmModuleEntry, value: ArrayBuffer): ArrayBuffer => {
|
||||
statuses.set(entry.id, { id: entry.id, lifecycle: entry.lifecycle, status: "loaded", byteLength: value.byteLength });
|
||||
return value;
|
||||
};
|
||||
const load = (entry: WasmModuleEntry): Promise<ArrayBuffer> => {
|
||||
const existing = bytes.get(entry.id);
|
||||
if (existing) return Promise.resolve(existing);
|
||||
const active = loading.get(entry.id);
|
||||
if (active) return active;
|
||||
const promise = fetchVerified(entry.resources[variant], fetcher).then((value) => {
|
||||
bytes.set(entry.id, value);
|
||||
statuses.set(entry.id, { id: entry.id, lifecycle: entry.lifecycle, status: "loaded", byteLength: value.byteLength });
|
||||
return value;
|
||||
const resource = entry.resources[variant];
|
||||
const existing = bytes.get(resource.url);
|
||||
if (existing) return Promise.resolve(markLoaded(entry, existing));
|
||||
const active = loading.get(resource.url);
|
||||
if (active) return active.then((value) => markLoaded(entry, value), (error: unknown) => {
|
||||
statuses.set(entry.id, { id: entry.id, lifecycle: entry.lifecycle, status: "failed", byteLength: 0, error: error instanceof Error ? error.message : String(error) });
|
||||
throw error;
|
||||
});
|
||||
const promise = fetchVerified(resource, fetcher).then((value) => {
|
||||
bytes.set(resource.url, value);
|
||||
return markLoaded(entry, value);
|
||||
}).catch((error: unknown) => {
|
||||
statuses.set(entry.id, { id: entry.id, lifecycle: entry.lifecycle, status: "failed", byteLength: 0, error: error instanceof Error ? error.message : String(error) });
|
||||
throw error;
|
||||
}).finally(() => loading.delete(entry.id));
|
||||
loading.set(entry.id, promise);
|
||||
}).finally(() => loading.delete(resource.url));
|
||||
loading.set(resource.url, promise);
|
||||
return promise;
|
||||
};
|
||||
return {
|
||||
|
||||
@@ -961,7 +961,7 @@ async function initialize(): Promise<WebEngineStatus> {
|
||||
await wasmModuleLoader.preloadCompleted();
|
||||
const imported = await import("../vendor/blender/web_engine.js") as unknown as { default: WasmFactory };
|
||||
const factory = imported.default;
|
||||
const binary = await wasmModuleLoader.load("core-completed");
|
||||
const binary = await wasmModuleLoader.loadDebug("debug-incremental");
|
||||
const initializedModule = await factory({ wasmBinary: binary });
|
||||
const initializedHandle = initializedModule._web_engine_create();
|
||||
if (initializedHandle <= 0) throw new Error("WebEngine handle creation failed");
|
||||
|
||||
28
web/tests/e2e/wasm-module-loading.spec.ts
Normal file
28
web/tests/e2e/wasm-module-loading.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("preloads completed WASM and loads the debug module independently", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const result = await page.evaluate(async () => {
|
||||
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
|
||||
const client = new WebEngineClient({ timeoutMs: 60_000 });
|
||||
try {
|
||||
await client.init();
|
||||
const before = await client.snapshot();
|
||||
const pending = before.status;
|
||||
const loaded = await client.loadDebugWasmModule("debug-incremental");
|
||||
return {
|
||||
pending,
|
||||
loaded,
|
||||
debug: loaded.find((module) => module.id === "debug-incremental"),
|
||||
completed: loaded.filter((module) => module.lifecycle === "complete"),
|
||||
};
|
||||
}
|
||||
finally { await client.shutdown(); }
|
||||
});
|
||||
expect(result.pending.ready).toBe(true);
|
||||
expect(result.completed.length).toBeGreaterThan(0);
|
||||
expect(result.completed.every((module) => module.status === "loaded")).toBe(true);
|
||||
expect(result.debug?.status).toBe("loaded");
|
||||
expect(result.debug?.lifecycle).toBe("debug");
|
||||
expect(result.debug?.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
Reference in New Issue
Block a user