Notes on WASM and host interaction performance (zero-copy buffering)

These are some notes from some digging I did yesterday.
What I was trying to find out how much copying of buffers is involved with a WASM module interacting with IndexedDB or the network (via fetch).
Anyway, below are my unedited notes.


WASM having its own bunch of memory in the browser means there may be more copying involved.

In JS, you can just access part of rust memory.
You get back a pointer from the WASM into its own memory. You can then construct a Uint8Array from that pointer, length & the ArrayBuffer that is backing WASM’s memory.

This will not involve a copy. If the WASM side continues changing the array, the values update accordingly in the Uint8Array, because it actually accesses the backing ArrayBuffer.

This can be used to e.g. speed up storing Uint8Arrays in the IndexedDB object store (IDBObjectStore.put() - Web APIs | MDN), if you just use that Uint8Array. IndexedDB will still do another copy, but that one you won’t get around, since it happens via JS <-> IndexedDB too.

Unfortunately IndexedDB doesn’t support getting data off disk directly into a pre-existing ArrayBuffer this way. the IDBObjectStore’s get method will always create new ArrayBuffers, and you need to copy the data over from there to WASM. This copy wouldn’t be necessary in JS since you can work with it there directly.

What is (maybe?) possible is sacrifice the zero-copy-on-write and instead gain zero-copy-on-read via storing Blobs in IndexedDB instead of storing Uint8Arrays, and then you can blob.stream().getReader({ type: "byob" }).read(new Uint8Array(wasmMemory, offset, length)).

A good example for an API that does support this zero-copy fetching is crypto.getRandomValues. Its first an only parameter is the Uint8Array which is to be populated with random values.
This makes it possible to just pass it some WASM memory to initialize random bytes into.

More APIs that allow zero-copy into WASM are fetch requests: It’s possible to read the data stream directly into WASM memory via response.body.getReader().getReader({ type: "byob" }) and then using the resulting ReadableStreamBYOBReader - Web APIs | MDN and its read method with a Uint8Array parameter to write to.

Similarily to IndexedDB, when receiving messages via WebSockets, binary data can be recieved as Blobs, and again, one can call .stream().getReader({ type: "byob" }).read(new Uint8Array(wasmMemory, pointer, length)) on it to transfer it into wasm memory directly.
Sending messages via WebSockets can be done zero-copy easily, too, simply via calling someWebSocket.send(new Uint8Array(wasmMemory, pointer, length)).

Unfortunately the BYOB reader (ReadableStreamBYOBReader) is not implemented in Safari.

5 Likes

:unamused: :apple:

haha, thanks for the information.

1 Like

You’re welcome. Are you building something using WASM in the browser?