Skip to content

Why does the read method of ReadableStreamBYOBReader class, need to _transfer_ the buffer it is provided? #1348

@amn

Description

@amn

What is the issue with the Streams Standard?

The issue seems to be that the read method of ReadableStreamBYOBReader class, effectively mandates a transferable buffer1 but for reasons which may be "arbitrary"? Transferable buffers are not a given (anymore?) -- buffers backing WebAssembly.Memory objects, for example, are not transferable, so cannot be used with read.

My user story

I only learned of the particular property of read very recently when I tried to amend an existing stream processing pipeline that relies on a WebAssembly (WASM) routine to process a stream chunk by chunk, this time to eliminate [unnecessary] buffer copying which was a performance bottle-neck. I had used the stream's iterator implicitly with a for await loop that vended ArrayBuffer objects (chunks), which had to be copied into the WASM instance memory (memory) buffer using e.g. new Uint8Array(memory.buffer).set(new Uint8Array(chunk)), which was the unnecessary copying part I now wanted to eliminate. So I thought if I used a bring-your-own-buffer (BYOB) stream reader (reader) instead, I could instead "load" the WASM instance memory buffer directly from the stream. But it wasn't meant to be.

WASM and BYOB don't mesh well

Turns out, because buffers backing WebAssembly.Memory objects, are inherently not transferable (rooted probably in WASM designers wanting to allow for additional potential constraints in a given WASM implementation like guard pages to contain stack overflow errors and what not), a construct like reader.read(new DataView(memory.buffer)) would invariably throw an error as it unsuccessfully attempts to detach the buffer backing the view it is supplied, in due accordance with step 10 in the "Streams Standard" specification.

This makes BYOB unusable with specifically WASM like that, which is IMO a great detriment as pairing two features that largely should help with performance bottlenecks, is currently a "no-go".

Misplaced requirement?

To that end, I'd like to ask -- why mandate in the specification that the buffer be transferred (and thus must be transferable)? What also seems a bit "arbitrary" to me, everything considered, is that there's no clear reason why step 10 is even necessary per the spec. -- if it is omitted and _bufferResult_.[[Value]] is instead the actual buffer backing the view supplied, _view_.[[ViewedArrayBuffer]], then the rest of the steps still appear perfectly valid, and since this is a specification, after all, it would have allowed for implementations to avoid transferring and thus detaching (effectively invalidating) the buffer provided to read. Buffers backing WebAssembly.Memory would be usable with read. There must be something I am missing with my interpretations here?

If TypedArray.set can "do" it, why can't read?

To add to my argument where I am trying to point at the requirement for "transferring" the buffer supplied, being a peculiarity that IMO could have been left out but isn't, we can consider the case of [the specification of] TypedArray.set, for instance, as an example of an API that also does effectively load a [BYOB] buffer, but whose specification does not warrant a "transfer" (there is the parable 23.a using the term "transfer" but there's no indication the word refers to the same concept) and instead simply specifies a "loop over bytes and copy each". And so I've got to ask, in light of this -- why couldn't the specification of read for ReadableStreamBYOBReader have been written in the same way, so that we could "bring our own" WASM memory buffers without issues?

Related

  • How to make ReadableStreamByobReader.read_with_u8_array() work? wasm-bindgen/wasm-bindgen#3579, where the accepted answer appears to explain why BYOB works the way it does (transferring the buffer it is given) but does so specifically in context of Rust and wasmbindgen, and so doesn't necessarily explain BYOB design unless you either have a pair of Rust glasses on, which I don't, or unless you understand why Rust should have a say on how the Strems Standard should be defined

  • Wasm needs a better memory management story WebAssembly/design#1397, which, again, attempts to tackle the issue from "the WASM side", asking for rationale behind some of the decisions in WASM, but which may have stemmed in part because there's the relative inability to use WASM with e.g. BYOB; I, for one, don't think there's necessarily anything wrong with the way the "memory" concept was defined in WASM; instead, I ask why transferring of [BYOB] buffers is necessary to define on the level of specification, which effectively makes using [BYOB] with specifically WASM currently impossible (at least using ReadableStreamBYOBReader)

  • Zero-copy pass ArrayBuffer from JS-land to WebAssembly-land WebAssembly/design#1162, which is mostly relevant due to the comment that sheds light on why specifically WASM buffers that back up memory, have to be the way they are

Footnotes

  1. https://streams.spec.whatwg.org/#readable-byte-stream-controller-pull-into

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions