Skip to content

Commit 9bb9b97

Browse files
committed
fix: preload wasm-zstd for inline workers and stabilize E2E
Upgrade @ioai/wasm-zstd to 1.1.1 and pass preloaded wasm bytes from the main thread into MCAP/bag inline workers, matching the HDF5 loading pattern. Add shared E2E helpers that wait on data-player-presence before asserting dockview readiness.
1 parent ca342f5 commit 9bb9b97

23 files changed

Lines changed: 353 additions & 184 deletions

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
"@foxglove/rosmsg-serialization": "^2.0.4",
9898
"@foxglove/rosmsg2-serialization": "^3.0.3",
9999
"@ioai/hdf5": "^1.0.0",
100-
"@ioai/wasm-zstd": "^1.1.0",
100+
"@ioai/wasm-zstd": "^1.1.1",
101101
"@mcap/browser": "^1.1.0",
102102
"@mcap/core": "^2.0.2",
103103
"@playwright/test": "^1.59.1",

playwright.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default defineConfig({
1111
// Match vite preview host; static preview is more reliable for Range/HEAD than raw dev.
1212
baseURL: 'http://127.0.0.1:4173',
1313
trace: 'on-first-retry',
14+
screenshot: 'only-on-failure',
1415
},
1516
projects: [
1617
{

src/features/viewer/RosViewProvider.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import React, { createContext, useContext, useEffect, useMemo, useState } from '
22
import { IntlProvider } from 'react-intl';
33
import { getRosViewMessages, type RosViewLocale } from '@/shared/intl/loadRosViewMessages';
44
import { Toaster } from '@/shared/ui/sonner';
5+
import { useMessagePipeline } from '@/core/pipeline/useMessagePipeline';
6+
import type { MessagePipelineState } from '@/core/pipeline/store';
57

68
export interface RosViewProviderProps {
79
theme?: 'light' | 'dark' | 'system';
@@ -73,13 +75,17 @@ export const RosViewProvider: React.FC<RosViewProviderProps> = ({
7375
);
7476

7577
const messages = useMemo(() => getRosViewMessages(language), [language]);
78+
const playerPresence = useMessagePipeline(
79+
(state: MessagePipelineState) => state.playerState.presence,
80+
);
7681

7782
return (
7883
<RosViewThemeContext.Provider value={contextValue}>
7984
<div
8085
id="rosview-root"
8186
data-language={language}
8287
data-theme={resolvedTheme}
88+
data-player-presence={playerPresence}
8389
className={`w-full h-full ${resolvedTheme === 'dark' ? 'dark' : ''}`}
8490
>
8591
<IntlProvider locale={intlLocaleFor(language)} defaultLocale="en" messages={messages}>

src/features/viewer/RosViewerImpl.tsx

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -67,31 +67,12 @@ import { resolveEmbedChrome, type RosViewerChrome, type RosViewerMode } from './
6767
import { RosViewerLayoutProvider } from './RosViewerLayoutContext';
6868
import type { RosViewExtension } from '@/core/extensions/types';
6969
import { toast } from 'sonner';
70-
import sqlWasmUrl from 'sql.js/dist/sql-wasm.wasm?url';
71-
import hdf5WasmUrl from '@ioai/hdf5/wasm/ioai_hdf5.wasm?url';
72-
73-
let sqlWasmBinaryPromise: Promise<ArrayBuffer> | null = null;
74-
let hdf5WasmBinaryPromise: Promise<ArrayBuffer> | null = null;
75-
76-
async function loadSqlWasmBinary(): Promise<ArrayBuffer> {
77-
sqlWasmBinaryPromise ??= fetch(sqlWasmUrl).then((response) => {
78-
if (!response.ok) {
79-
throw new Error(`Failed to load SQL wasm: HTTP ${response.status}`);
80-
}
81-
return response.arrayBuffer();
82-
});
83-
return await sqlWasmBinaryPromise;
84-
}
85-
86-
async function loadHdf5WasmBinary(): Promise<ArrayBuffer> {
87-
hdf5WasmBinaryPromise ??= fetch(hdf5WasmUrl).then((response) => {
88-
if (!response.ok) {
89-
throw new Error(`Failed to load HDF5 wasm: HTTP ${response.status}`);
90-
}
91-
return response.arrayBuffer();
92-
});
93-
return await hdf5WasmBinaryPromise;
94-
}
70+
import {
71+
loadHdf5WasmBinary,
72+
loadSqlWasmBinary,
73+
loadZstdWasmBinary,
74+
needsZstdWasmForWorker,
75+
} from '@/infra/workers/preloadWorkerWasm';
9576

9677
function extensionForDataset(ds: DatasetItem): string | undefined {
9778
if (ds.kind === 'file' && ds.file) {
@@ -139,6 +120,7 @@ async function initializePlayerForDataset(
139120
typeof window !== 'undefined' && new URLSearchParams(window.location.search).get('workerPerf') === '1';
140121
const sqlWasmBinary = ext === 'db3' ? await loadSqlWasmBinary() : undefined;
141122
const hdf5WasmBinary = ext === 'hdf5' || ext === 'h5' ? await loadHdf5WasmBinary() : undefined;
123+
const zstdWasmBinary = needsZstdWasmForWorker(ext) ? await loadZstdWasmBinary() : undefined;
142124
if (ds.kind === 'url' && ds.url) {
143125
const init: Record<string, unknown> = {
144126
url: resolveBrowserHttpUrl(ds.url),
@@ -151,6 +133,9 @@ async function initializePlayerForDataset(
151133
if (hdf5WasmBinary) {
152134
init.hdf5WasmBinary = hdf5WasmBinary;
153135
}
136+
if (zstdWasmBinary) {
137+
init.zstdWasmBinary = zstdWasmBinary;
138+
}
154139
if (
155140
typeof ds.sizeBytes === 'number' &&
156141
Number.isFinite(ds.sizeBytes) &&
@@ -171,13 +156,15 @@ async function initializePlayerForDataset(
171156
workerPerf,
172157
autoDataQualityScan,
173158
...(sqlWasmBinary ? { sqlWasmBinary } : {}),
159+
...(zstdWasmBinary ? { zstdWasmBinary } : {}),
174160
});
175161
} else {
176162
await player.initialize({
177163
file: ds.file,
178164
workerPerf,
179165
autoDataQualityScan,
180166
...(hdf5WasmBinary ? { hdf5WasmBinary } : {}),
167+
...(zstdWasmBinary ? { zstdWasmBinary } : {}),
181168
});
182169
}
183170
return;

src/features/workspace/common/LoadingOverlay.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ sourceName, onCa
1818
role="status"
1919
aria-live="polite"
2020
aria-busy="true"
21+
data-testid="rosview-loading-overlay"
2122
>
2223
<Card className="pointer-events-auto w-full max-w-sm border-border shadow-none">
2324
<CardHeader className="gap-2 pb-4 text-center">

src/infra/sources/BagIterableSource.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,18 @@ function asTime(value: unknown): Time {
4747

4848
export class BagIterableSource implements IIterableSource {
4949
private _source: BagSource;
50+
private _wasmBinary: ArrayBuffer;
5051
private _bag?: Bag;
5152
private _datatypesByConnectionId = new Map<number, string>();
5253
private _readersByConnectionId = new Map<number, MessageReader>();
5354

54-
constructor(source: BagSource) {
55+
constructor(source: BagSource, options: { wasmBinary: ArrayBuffer }) {
5556
this._source = source;
57+
this._wasmBinary = options.wasmBinary;
5658
}
5759

5860
async initialize(): Promise<Initialization> {
59-
const decompressHandlers = await loadDecompressHandlers();
61+
const decompressHandlers = await loadDecompressHandlers({ wasmBinary: this._wasmBinary });
6062

6163
const fileLike: BlobReader | RemoteBagReadable =
6264
this._source.type === "remote" ? this._source.readable : new BlobReader(this._source.file);

src/infra/sources/decompressHandlers.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import type { McapTypes } from "@mcap/core";
2-
import { decompress as zstdDecompress, init as initZstd } from "@ioai/wasm-zstd";
3-
import zstdWasmUrl from "@ioai/wasm-zstd/wasm-zstd.wasm?url";
42
import * as lz4js from "lz4js";
3+
import { decompress as zstdDecompress } from "@ioai/wasm-zstd";
4+
import { ensureZstdRuntime } from "@/infra/workers/zstdRuntimeLoader";
55

6-
// Load the zstd wasm module explicitly so Vite owns the wasm asset URL in both
7-
// dev and production worker bundles.
6+
export type LoadDecompressHandlersOptions = {
7+
wasmBinary: ArrayBuffer;
8+
};
89

9-
let handlersPromise: Promise<McapTypes.DecompressHandlers> | undefined;
10-
11-
export async function loadDecompressHandlers(): Promise<McapTypes.DecompressHandlers> {
12-
return await (handlersPromise ??= _loadDecompressHandlers());
13-
}
14-
15-
async function _loadDecompressHandlers(): Promise<McapTypes.DecompressHandlers> {
16-
await initZstd({ wasmUrl: zstdWasmUrl });
10+
export async function loadDecompressHandlers(
11+
options: LoadDecompressHandlersOptions,
12+
): Promise<McapTypes.DecompressHandlers> {
13+
await ensureZstdRuntime(options.wasmBinary);
1714

1815
return {
1916
lz4: (buffer, decompressedSize) => {

src/infra/workers/bag.worker.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,14 @@ class BagWorker implements IWorkerSerializedSourceWorker {
6969
throw new Error("Invalid arguments for BagWorker");
7070
}
7171

72-
this._source = new BagIterableSource(sourceArgs);
72+
const zstdWasmBinary = args.zstdWasmBinary;
73+
if (!(zstdWasmBinary instanceof ArrayBuffer)) {
74+
throw new Error(
75+
"BagWorker: zstdWasmBinary required (pass wasm bytes from main thread for inline workers)",
76+
);
77+
}
78+
79+
this._source = new BagIterableSource(sourceArgs, { wasmBinary: zstdWasmBinary });
7380
const init = await this._source.initialize();
7481
this._initialization = init;
7582
this._qualityScan.initialize(this._source, init, args.autoDataQualityScan === true);

src/infra/workers/mcap.worker.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,16 @@ class McapWorkerImpl implements IWorkerSerializedSourceWorker {
103103
throw new Error("McapWorker: neither url nor file provided");
104104
}
105105

106+
const zstdWasmBinary = args.zstdWasmBinary;
107+
if (!(zstdWasmBinary instanceof ArrayBuffer)) {
108+
throw new Error(
109+
"McapWorker: zstdWasmBinary required (pass wasm bytes from main thread for inline workers)",
110+
);
111+
}
112+
106113
const decompressHandlers = await workerPerf.timeAsync(
107114
"initialize.loadDecompressHandlers",
108-
() => loadDecompressHandlers(),
115+
() => loadDecompressHandlers({ wasmBinary: zstdWasmBinary }),
109116
);
110117

111118
const mcapReadable = {

0 commit comments

Comments
 (0)