Fix issues
diff --git a/ui/config/jest.unittest.config.js b/ui/config/jest.unittest.config.js index 7c81890..9557c7f 100644 --- a/ui/config/jest.unittest.config.js +++ b/ui/config/jest.unittest.config.js
@@ -46,6 +46,9 @@ // Lezer .grammar imports: stub out (Vite compiles them in production via // @lezer/generator, but no test exercises the parser). '\\.grammar$': __dirname + '/grammar_mock.js', + // ui/src/wasm/* shims point at emcc-emitted glue via a Vite alias. Jest + // doesn't replicate the alias and no test runs wasm — stub them out. + '/wasm/[^/]+$': __dirname + '/wasm_mock.js', // Some TS files use ESM-style explicit ".js" extensions on relative // imports of sibling TS sources (e.g. `import './language.js'`). Jest's // resolver doesn't map .js -> .ts, so strip the extension here.
diff --git a/ui/config/wasm_mock.js b/ui/config/wasm_mock.js new file mode 100644 index 0000000..355cb0d --- /dev/null +++ b/ui/config/wasm_mock.js
@@ -0,0 +1,22 @@ +// Copyright (C) 2026 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Stub for `ui/src/wasm/*` imports under jest. Vite aliases these to the +// emcc-emitted glue in `ui/src/gen/*` at build time; jest doesn't run that +// alias, and no unit test exercises the wasm runtime. A factory that throws +// makes accidental invocations obvious. +const factory = () => { + throw new Error('wasm runtime is not available in jest'); +}; +module.exports = {default: factory, __esModule: true};
diff --git a/ui/src/base/proto_utils_wasm.ts b/ui/src/base/proto_utils_wasm.ts index e9f83c6..439b6b7 100644 --- a/ui/src/base/proto_utils_wasm.ts +++ b/ui/src/base/proto_utils_wasm.ts
@@ -14,6 +14,7 @@ import protos from '../protos'; import {assetSrc} from './assets'; +import {defer} from './deferred'; import {errResult, okResult, type Result} from './result'; import {utf8Decode, utf8Encode} from './string_utils'; import WasmModuleGen from '../wasm/proto_utils'; @@ -128,13 +129,16 @@ // emscripten uses sync-loading, which works only in Workers. const resp = await fetch(assetSrc('proto_utils.wasm')); const wasmBinary = await resp.arrayBuffer(); - const instance = await WasmModuleGen({ + const deferredRuntimeInitialized = defer<void>(); + const instance = WasmModuleGen({ noInitialRun: true, locateFile: (s: string) => s, print: (s: string) => console.log(s), printErr: (s: string) => console.error(s), + onRuntimeInitialized: () => deferredRuntimeInitialized.resolve(), wasmBinary, }); + await deferredRuntimeInitialized; const bufAddr = instance.ccall('proto_utils_buf', 'number', [], []) >>> 0; const bufSize = instance.ccall('proto_utils_buf_size', 'number', [], []) >>> 0;
diff --git a/ui/src/engine/index.ts b/ui/src/engine/index.ts index 8370dac..802e71a 100644 --- a/ui/src/engine/index.ts +++ b/ui/src/engine/index.ts
@@ -30,5 +30,5 @@ // Receives the boostrap message from the frontend with the MessagePort. selfWorker.onmessage = (msg: MessageEvent) => { const port = msg.data as MessagePort; - void wasmBridge.initialize(port); + wasmBridge.initialize(port); };
diff --git a/ui/src/engine/wasm_bridge.ts b/ui/src/engine/wasm_bridge.ts index ce26e21..f0c6467 100644 --- a/ui/src/engine/wasm_bridge.ts +++ b/ui/src/engine/wasm_bridge.ts
@@ -13,6 +13,7 @@ // limitations under the License. import {assertExists, assertTrue} from '../base/assert'; +import {defer} from '../base/deferred'; import {PerfettoWasmModule} from '../wasm/perfetto_wasm_module'; // The 64-bit variant of TraceProcessor wasm is always built in all build @@ -46,11 +47,7 @@ // - [JS] postMessage() (this file) export class WasmBridge { private aborted: boolean; - // Resolves once the wasm runtime is fully initialized AND the rpc init - // ccall has completed. All callers that need to touch `connection` must - // await this first. - private readonly ready: Promise<PerfettoWasmModule>; - private connection!: PerfettoWasmModule; + private connection: PerfettoWasmModule; private reqBufferAddr = 0; private lastStderr: string[] = []; private messagePort?: MessagePort; @@ -58,39 +55,33 @@ constructor() { this.aborted = false; + const deferredRuntimeInitialized = defer<void>(); this.useMemory64 = hasMemory64Support(); const initModule = this.useMemory64 ? TraceProcessor64 : TraceProcessor32; - this.ready = initModule({ + this.connection = initModule({ locateFile: (s: string) => s, print: (line: string) => console.log(line), printErr: (line: string) => this.appendAndLogErr(line), - }).then((module) => { - this.connection = module; - const fn = module.addFunction(this.onReply.bind(this), 'vpi'); + onRuntimeInitialized: () => deferredRuntimeInitialized.resolve(), + }); + + deferredRuntimeInitialized.then(() => { + const fn = this.connection.addFunction(this.onReply.bind(this), 'vpi'); this.reqBufferAddr = this.wasmPtrCast( - // ccall: 'pointer' is what emscripten accepts at runtime, but DT's - // typings only model 'number'/'string'/'array'/'boolean'. Use - // 'number' here — emscripten treats them as equivalent for ccall - // arg/return marshalling. - module.ccall( + this.connection.ccall( 'trace_processor_rpc_init', - /* return=*/ 'number', - /* args=*/ ['number', 'number'], + /* return=*/ 'pointer' as 'number', + /* args=*/ ['pointer', 'number'] as ('number')[], [fn, REQ_BUF_SIZE], ), ); - return module; }); } - async initialize(port: MessagePort) { + initialize(port: MessagePort) { // Ensure that initialize() is called only once. assertTrue(this.messagePort === undefined); this.messagePort = port; - // Wait until the wasm runtime + rpc buffer are ready before wiring the - // port handler — otherwise queued messages would land in onMessage() - // before `this.connection` exists. - await this.ready; // Note: setting .onmessage implicitly calls port.start() and dispatches the // queued messages. addEventListener('message') doesn't. this.messagePort.onmessage = this.onMessage.bind(this);
diff --git a/ui/src/frontend/trace_converter.ts b/ui/src/frontend/trace_converter.ts index 5f5c9be..6fddc0e 100644 --- a/ui/src/frontend/trace_converter.ts +++ b/ui/src/frontend/trace_converter.ts
@@ -86,7 +86,7 @@ } } - const worker = new Worker(assetSrc('traceconv_bundle.js')); + const worker = new Worker(assetSrc('traceconv_bundle.js'), {type: 'module'}); worker.onmessage = handleOnMessage; worker.postMessage(msg); return promise;
diff --git a/ui/src/traceconv/index.ts b/ui/src/traceconv/index.ts index 56dc8d4..dcf8092 100644 --- a/ui/src/traceconv/index.ts +++ b/ui/src/traceconv/index.ts
@@ -14,6 +14,7 @@ import {addErrorHandler, type ErrorDetails, reportError} from '../base/logging'; import {assertExists} from '../base/assert'; +import {defer} from '../base/deferred'; import type {time} from '../base/time'; import traceconv from '../wasm/traceconv'; @@ -79,12 +80,15 @@ } async function runTraceconv(trace: Blob, args: string[]) { - const module = await traceconv({ + const deferredRuntimeInitialized = defer<void>(); + const module = traceconv({ noInitialRun: true, locateFile: (s: string) => s, print: updateStatus, printErr: updateStatus, + onRuntimeInitialized: () => deferredRuntimeInitialized.resolve(), }); + await deferredRuntimeInitialized; module.FS.mkdir('/fs'); module.FS.mount( assertExists(module.FS.filesystems.WORKERFS),
diff --git a/ui/src/wasm/proto_utils.d.ts b/ui/src/wasm/proto_utils.d.ts index 58d862d..ad66f66 100644 --- a/ui/src/wasm/proto_utils.d.ts +++ b/ui/src/wasm/proto_utils.d.ts
@@ -17,5 +17,8 @@ // Type shim for the emcc-emitted 'proto_utils' wasm glue. The runtime path is // resolved by Vite (alias 'src/wasm/*' -> 'src/gen/*') to the actual .js // file in the gen dir; tsc resolves this .d.ts and never reads the .js. -declare const factory: EmscriptenModuleFactory<PerfettoWasmModule>; +// Our emcc build uses WASM_ASYNC_COMPILATION=0, so the factory returns the +// Module synchronously — not Promise<Module> as @types/emscripten's +// EmscriptenModuleFactory would suggest. +declare const factory: (opts?: Partial<PerfettoWasmModule>) => PerfettoWasmModule; export default factory;
diff --git a/ui/src/wasm/trace_processor.d.ts b/ui/src/wasm/trace_processor.d.ts index 7c997c3..ffd5e2b 100644 --- a/ui/src/wasm/trace_processor.d.ts +++ b/ui/src/wasm/trace_processor.d.ts
@@ -17,5 +17,8 @@ // Type shim for the emcc-emitted 'trace_processor' wasm glue. The runtime path is // resolved by Vite (alias 'src/wasm/*' -> 'src/gen/*') to the actual .js // file in the gen dir; tsc resolves this .d.ts and never reads the .js. -declare const factory: EmscriptenModuleFactory<PerfettoWasmModule>; +// Our emcc build uses WASM_ASYNC_COMPILATION=0, so the factory returns the +// Module synchronously — not Promise<Module> as @types/emscripten's +// EmscriptenModuleFactory would suggest. +declare const factory: (opts?: Partial<PerfettoWasmModule>) => PerfettoWasmModule; export default factory;
diff --git a/ui/src/wasm/trace_processor_memory64.d.ts b/ui/src/wasm/trace_processor_memory64.d.ts index 24779ac..7193fae 100644 --- a/ui/src/wasm/trace_processor_memory64.d.ts +++ b/ui/src/wasm/trace_processor_memory64.d.ts
@@ -17,5 +17,8 @@ // Type shim for the emcc-emitted 'trace_processor_memory64' wasm glue. The runtime path is // resolved by Vite (alias 'src/wasm/*' -> 'src/gen/*') to the actual .js // file in the gen dir; tsc resolves this .d.ts and never reads the .js. -declare const factory: EmscriptenModuleFactory<PerfettoWasmModule>; +// Our emcc build uses WASM_ASYNC_COMPILATION=0, so the factory returns the +// Module synchronously — not Promise<Module> as @types/emscripten's +// EmscriptenModuleFactory would suggest. +declare const factory: (opts?: Partial<PerfettoWasmModule>) => PerfettoWasmModule; export default factory;
diff --git a/ui/src/wasm/traceconv.d.ts b/ui/src/wasm/traceconv.d.ts index fb47fc2..5d51039 100644 --- a/ui/src/wasm/traceconv.d.ts +++ b/ui/src/wasm/traceconv.d.ts
@@ -17,5 +17,8 @@ // Type shim for the emcc-emitted 'traceconv' wasm glue. The runtime path is // resolved by Vite (alias 'src/wasm/*' -> 'src/gen/*') to the actual .js // file in the gen dir; tsc resolves this .d.ts and never reads the .js. -declare const factory: EmscriptenModuleFactory<PerfettoWasmModule>; +// Our emcc build uses WASM_ASYNC_COMPILATION=0, so the factory returns the +// Module synchronously — not Promise<Module> as @types/emscripten's +// EmscriptenModuleFactory would suggest. +declare const factory: (opts?: Partial<PerfettoWasmModule>) => PerfettoWasmModule; export default factory;
diff --git a/ui/vite.config.mjs b/ui/vite.config.mjs index 864fcd5..e7a7d8f 100644 --- a/ui/vite.config.mjs +++ b/ui/vite.config.mjs
@@ -206,15 +206,19 @@ // Per-bundle config: input file, output dir (relative to ui/out), and output // filename. Most bundles follow the standard convention; service_worker and // chrome_extension differ. +// Bundles loaded into a Worker that needs `import.meta.url` (emscripten wasm +// glue) MUST be ES modules — IIFE would rewrite import.meta.url into a +// `document.currentScript` polyfill that crashes inside a worker. +// Main-thread bundles loaded by a classic <script> tag stay as IIFE. const BUNDLE_CONFIGS = { - frontend: {dir: 'dist_version', entry: 'index.ts'}, - engine: {dir: 'dist_version', entry: 'index.ts'}, - traceconv: {dir: 'dist_version', entry: 'index.ts'}, - bigtrace: {dir: 'dist_version/bigtrace', entry: 'index.ts'}, - open_perfetto_trace: {dir: 'dist/open_perfetto_trace', entry: 'index.ts'}, - chrome_extension: {dir: 'chrome_extension', entry: 'index.ts'}, + frontend: {dir: 'dist_version', entry: 'index.ts', format: 'iife'}, + engine: {dir: 'dist_version', entry: 'index.ts', format: 'es'}, + traceconv: {dir: 'dist_version', entry: 'index.ts', format: 'es'}, + bigtrace: {dir: 'dist_version/bigtrace', entry: 'index.ts', format: 'iife'}, + open_perfetto_trace: {dir: 'dist/open_perfetto_trace', entry: 'index.ts', format: 'iife'}, + chrome_extension: {dir: 'chrome_extension', entry: 'index.ts', format: 'iife'}, service_worker: {dir: 'dist', entry: 'service_worker.ts', - fileName: 'service_worker.js'}, + fileName: 'service_worker.js', format: 'iife'}, }; const bundleCfg = BUNDLE_CONFIGS[BUNDLE]; if (!bundleCfg) { @@ -298,7 +302,7 @@ input: {[BUNDLE]: inputPath}, treeshake: NO_TREESHAKE ? false : undefined, output: { - format: 'iife', + format: bundleCfg.format, name: BUNDLE, entryFileNames, // With cssCodeSplit:false Vite emits the CSS as "style.css" by