Intent to Implement: WebSocketStream

904 views
Skip to first unread message

Adam Rice

unread,
Jul 11, 2019, 11:07:16 PM7/11/19
to blink-dev
ri...@chromium.org,yhi...@chromium.org https://github.com/ricea/websocketstream-explainer/blob/master/README.md https://docs.google.com/document/d/1XuxEshh5VYBYm1qRVKordTamCOsR-uGQBCYFcHXP4L0/edit https://github.com/w3ctag/design-reviews/issues/394 The WebSocket API provides a JavaScript interface to the RFC6455 WebSocket protocol. While it has served well, it is awkward from an ergonomics perspective and is missing the important feature of backpressure. The intent of the WebSocketStream API is to resolve these deficiencies by integrating streams with the WebSocket API. Currently applying backpressure to received messages is not possible with the WebSocket API. When messages arrive faster than the page can handle them, the render process will either fill up memory buffering those messages, become unresponsive due to 100% CPU usage, or both. Applying backpressure to sent messages is possible but involves polling the bufferedAmount property which is inefficient and unergonomic.
The main risk is that it fails to become an interoperable part of the web platform if other browsers do not implement it. Firefox: No public signals Edge: No public signals Safari: No public signals Web developers: No signals A major focus of the new API is improving ergonomics over the existing WebSocket API. Everything except for correct backpressure behaviour can be polyfilled. Developers who are sensitive to backpressure may prefer to feature-detect and fall back to application-level backpressure if the feature is not available. Security posture is the same as the existing WebSocket API.
The necessary probes will be included in the code so that existing WebSocket debugging facilities should work as-is. Yes The feature is easy to support everywhere existing Blink WebSocket support exists. Blink WebSockets are supported on every Blink platform. No The feature will be tested in web platform tests. https://chromestatus.com/feature/5189728691290112
This intent message was generated by Chrome Platform Status.

J Decker

unread,
Jul 12, 2019, 7:53:03 AM7/12/19
to blink-dev
--
You received this message because you are subscribed to the Google Groups "blink-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to blink-dev+...@chromium.org.
To view this discussion on the web visit https://groups.google.com/a/chromium.org/d/msgid/blink-dev/CAC_ixdxfRa%3DBa_wRhq5g1MRq1brKxWJs93SY33F1FBrBKJf4%2BA%40mail.gmail.com.

you say 
ws.onmessage = evt => process(evt.data);
 the [] example won’t wait for process() to complete before calling it again; 

How do you suspend in process in order for onmessage to fire again?  You'd have to get back to the event dispatch.

---

Personally; and I'm not sure what to blame it on, call it being a conservative; I don't see that promises do anything other than wrap two callbacks instead of 1, and just make code more complex for no reason; I'd be happier with a change to more like addEventListener like `websock.on( "message", ... );` 

----

While addressing shortcomings, could also implement an API to expose the ping functionality of the websocket protocol; a successful ping response means your message queue is empty... it does add extra latency between large packets.


J Decker

unread,
Jul 12, 2019, 8:19:23 AM7/12/19
to blink-dev
Maybe I should have just replied as an issue... could edit that.  
I have this c library I use; https://www.npmjs.com/package/sack.vfs#websocket-module   and not that it directly relates, 
but I did add a callback on server/client sockets to get event notification as fragments completed.  I didn't forward that to JS yet.

And aren't there other options for perMessageDeflateAllow, perMessageDeflate  something?
I had misread the RFC, so I implemented masking as an option; until I found things only worked one way (client mask, server no-mask).

Also, the callbacks could be specified in the constructor option object ( another feature I don't do there ) but the tcp/udp sockets and I think HTTP(S) constructors can specify all event handlers it all in one option object (websocks is sort of a 4-way promise).



Adam Rice

unread,
Jul 12, 2019, 9:29:06 AM7/12/19
to J Decker, blink-dev
How do you suspend in process in order for onmessage to fire again?  You'd have to get back to the event dispatch.

I'm assuming here that process() is asynchronous, so it will suspend using await, or by using Promises directly.

Personally; and I'm not sure what to blame it on, call it being a conservative; I don't see that promises do anything other than wrap two callbacks instead of 1, and just make code more complex for no reason; I'd be happier with a change to more like addEventListener like `websock.on( "message", ... );` 

Promises, particular in combination with async/await, make it possible to write code in a more "straight-line" style than callbacks, which many people find more intuitive. They also have the benefit that they always work the same way--you don't have to worry about whether each API you use has re-entrant behaviour or not.

You can expect that every asynchronous API from now on will use promises--see https://w3ctag.github.io/design-principles/#promises.

While addressing shortcomings, could also implement an API to expose the ping functionality of the websocket protocol; a successful ping response means your message queue is empty... it does add extra latency between large packets.

Ping is definitely something I'm considering, but coming up with a good API for it is surprisingly tricky. It may be added to WebSocketStream as a future extension, but it won't be included in the first version.

Adam Rice

unread,
Jul 12, 2019, 9:46:49 AM7/12/19
to J Decker, blink-dev
Maybe I should have just replied as an issue... could edit that.  
I have this c library I use; https://www.npmjs.com/package/sack.vfs#websocket-module   and not that it directly relates, 
but I did add a callback on server/client sockets to get event notification as fragments completed.  I didn't forward that to JS yet.

In Chromium we treat frames as an implementation detail and make no effort to preserve frame boundaries.

And aren't there other options for perMessageDeflateAllow, perMessageDeflate  something?

The server controls whether or not permessage-deflate is used during the handshake. So I think having an additional control in client-side JavaScript would be redundant.

Also, the callbacks could be specified in the constructor option object ( another feature I don't do there ) but the tcp/udp sockets and I think HTTP(S) constructors can specify all event handlers it all in one option object (websocks is sort of a 4-way promise).

This would make WebSockets work differently from everything else in the platform, making them harder to adopt.

Thanks for your comments. I will try to incorporate your feedback into the explainer.

Ben Kelly

unread,
Jul 12, 2019, 10:39:43 AM7/12/19
to Adam Rice, blink-dev
Just curious, are you working with any partner sites to identify use cases and evaluate the effectiveness of the new API?

--

J Decker

unread,
Jul 12, 2019, 12:05:27 PM7/12/19
to blink-dev
On Fri, Jul 12, 2019 at 6:46 AM Adam Rice <ri...@chromium.org> wrote:
Maybe I should have just replied as an issue... could edit that.  
I have this c library I use; https://www.npmjs.com/package/sack.vfs#websocket-module   and not that it directly relates, 
but I did add a callback on server/client sockets to get event notification as fragments completed.  I didn't forward that to JS yet.

In Chromium we treat frames as an implementation detail and make no effort to preserve frame boundaries.

I do too.  However, it was a simple place to add a progress callback for up/downloading multi-meg file blobs; just giving how much was sent of how much total in the frame.
Just didn't make it to JS because no other implementation (and there are many websock providers in the node ecosystem, which would also either need to fork and provide a separate websockstream or additional interfaces in the existing one to match the API for JS end-to-end compatibility. 
 

And aren't there other options for perMessageDeflateAllow, perMessageDeflate  something?

The server controls whether or not permessage-deflate is used during the handshake. So I think having an additional control in client-side JavaScript would be redundant.

The client HTTP request has the option, and that's the first packet sent without the server being able to specify deflate or not, the server can reply that it accepts (or not reply) or not, and then whether it also will send deflate  ( perMessageDeflate is write, allow is read).

J Decker

unread,
Jul 12, 2019, 12:19:27 PM7/12/19
to blink-dev
On Fri, Jul 12, 2019 at 9:05 AM J Decker <d3c...@gmail.com> wrote:
On Fri, Jul 12, 2019 at 6:46 AM Adam Rice <ri...@chromium.org> wrote:
In Chromium we treat frames as an implementation detail and make no effort to preserve frame boundaries.

I do too.....

Hmm Maybe it's just on receive assembly now anyway; the output is all transcoded and 'sent' when send() returns.... and the buffers don't know the content.


 

Adam Rice

unread,
Jul 17, 2019, 12:23:44 AM7/17/19
to Ben Kelly, blink-dev
Just curious, are you working with any partner sites to identify use cases and evaluate the effectiveness of the new API?

That's the plan. Actually, we're looking for more partners to try it out once we have an implementation. 

fer...@gmail.com

unread,
Aug 5, 2019, 11:56:17 AM8/5/19
to blink-dev
> Applying backpressure to sent messages is possible but involves polling the bufferedAmount property which is inefficient and unergonomic.

For this particular case of writable backpressure, there's a simple solution already deployed for WebRTC data channels (which have a nearly identical API as the WebSocket API). See https://bugs.chromium.org/p/webrtc/issues/detail?id=4616 for an explanation fo the "onbufferedamountlow" event. This prevents the need to poll the bufferedAmount property.

As for the issue of readable backpressure, this is still not addressed for WebRTC data channels. There's an open issue about it here: https://bugs.chromium.org/p/webrtc/issues/detail?id=4616

In designing this new interface, can we please ensure that it also works for WebRTC data channels? We should be unifying around a single interface for these APIs, not diverging them even further.

lennar...@gmail.com

unread,
Aug 6, 2019, 9:55:08 AM8/6/19
to blink-dev, ptha...@google.com
Hey Adam, this is great work. Quite similar to what I proposed for WebRTC data channels as well.

I don't think WebTransport should or will be limited to QUIC, so I don't understand why this API cannot be incorporated into it. It would be the perfect opportunity to combine efforts here and the WebTransport folks could use someone who has experience with the streams API since they are migrating their API to use the streams API as well. So, I think it's about weighing up a quick improvement vs. a mid-term win. And, personally, I'd prefer the mid-term win instead of having two largely similar APIs at the end. People have settled with polling bufferedAmount for now. It's not great but they can wait a little longer and benefit from a less fragmented API surface. (/cc Peter)

That being said, again, it's great to see progress in this area and you can ping me in IRC (lgrahl) if you want to discuss anything in detail.

Cheers
Lennart

Adam Rice

unread,
Aug 6, 2019, 10:20:13 AM8/6/19
to fer...@gmail.com, blink-dev
For this particular case of writable backpressure, there's a simple solution already deployed for WebRTC data channels (which have a nearly identical API as the WebSocket API). See https://bugs.chromium.org/p/webrtc/issues/detail?id=4616 for an explanation fo the "onbufferedamountlow" event. This prevents the need to poll the bufferedAmount property.

I recall discussing this approach in the past for WebSockets, but I didn't know that WebRTC had implemented it. I suspect that it leads to convoluted logic, but I'd be happy to be proved wrong.

In designing this new interface, can we please ensure that it also works for WebRTC data channels? We should be unifying around a single interface for these APIs, not diverging them even further.

I'm confident this design can be used for WebRTC data channels, but I'm concentrating on proving it for WebSockets first.

I'd rather make a clean break rather than piecemeal improvements to the existing API, as usability will only get worse and there are things in the existing API we simply can't fix (ie. binaryType defaulting to "blob").

--
You received this message because you are subscribed to the Google Groups "blink-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to blink-dev+...@chromium.org.

Adam Rice

unread,
Aug 6, 2019, 10:35:36 AM8/6/19
to lennar...@gmail.com, blink-dev, ptha...@google.com
Hey Adam, this is great work. Quite similar to what I proposed for WebRTC data channels as well.

For the case of streaming individual messages, I'm excited about what @martinthompson proposed here, which is that we can use a subclass of the body mixin from the fetch standard as a message representation. This avoids the mental confusion of talking about streams-of-streams, and also brings for free the swiss-army-knife nature of the Response object--you can create a message from anything (including a stream) and read a message as anything (including a stream).

This will not be a version 1 feature. I currently envision that we'll need bring-your-own-buffer binary streams first.

I don't think WebTransport should or will be limited to QUIC, so I don't understand why this API cannot be incorporated into it. It would be the perfect opportunity to combine efforts here and the WebTransport folks could use someone who has experience with the streams API since they are migrating their API to use the streams API as well. So, I think it's about weighing up a quick improvement vs. a mid-term win. And, personally, I'd prefer the mid-term win instead of having two largely similar APIs at the end. People have settled with polling bufferedAmount for now. It's not great but they can wait a little longer and benefit from a less fragmented API surface. (/cc Peter)

I don't think it's an and/or choice. WebTransport is solving a significantly harder problem, and will have a larger API as a result. In the meantime, developers can play with the WebSocketStream API (available behind a flag Real Soon Now) and we find out where the bumps are.

One possible outcome of this is that we don't ship WebSocketStream and we fold what we learned into the WebTransport API. But I like to think WebSocketStream will be independently useful as a small API that solves an immediate problem.



--
You received this message because you are subscribed to the Google Groups "blink-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to blink-dev+...@chromium.org.

Domenic Denicola

unread,
Aug 6, 2019, 10:43:26 AM8/6/19
to Adam Rice, lennar...@gmail.com, blink-dev, ptha...@google.com
From: Adam Rice <ri...@chromium.org>

> I don't think it's an and/or choice. WebTransport is solving a significantly harder problem, and will have a larger API as a result. In the meantime, developers can play with the WebSocketStream API (available behind a flag Real Soon Now) and we find out where the bumps are.
>
> One possible outcome of this is that we don't ship WebSocketStream and we fold what we learned into the WebTransport API. But I like to think WebSocketStream will be independently useful as a small API that solves an immediate problem.

I want to second this assessment in general. As long as web sockets exist on the platform, and are used by web developers, it's still worthwhile giving them a modern API, with read backpressure non-hacky write backpressure, and good defaults. I think that API should *parallel* any WebTransport API, and I anticipate that Adam will be working with Peter and others on ensuring alignment. But working on a better web socket API is purely a win for the web platform and web developers. The only potential downside is opportunity cost vs. spending Adam's time on other features, and I trust him to weight that appropriately.

lennar...@gmail.com

unread,
Aug 6, 2019, 11:26:45 AM8/6/19
to blink-dev, lennar...@gmail.com, ptha...@google.com, j...@mozilla.com
I recall discussing this approach in the past for WebSockets, but I didn't know that WebRTC had implemented it. I suspect that it leads to convoluted logic, but I'd be happy to be proved wrong.

Unfortunately, you're correct on that. I haven't seen anyone getting it right first try. But that is also due to bugs in (all?) existing implementations. In any case, streams are much easier to work with.
 
I'm confident this design can be used for WebRTC data channels, but I'm concentrating on proving it for WebSockets first.
 
Please, keep in close contact with Jan-Ivar and me about that. (We thought we'd first introduce this to data channels and then suggest a port to WebSockets but I'm more than happy to do it the other way around.)

For the case of streaming individual messages, I'm excited about what @martinthompson proposed here, which is that we can use a subclass of the body mixin from the fetch standard as a message representation. This avoids the mental confusion of talking about streams-of-streams, and also brings for free the swiss-army-knife nature of the Response object--you can create a message from anything (including a stream) and read a message as anything (including a stream).

That sounds like a pretty good idea (hoping there are no caveats). :)

I don't think it's an and/or choice. WebTransport is solving a significantly harder problem, and will have a larger API as a result. In the meantime, developers can play with the WebSocketStream API (available behind a flag Real Soon Now) and we find out where the bumps are.

One possible outcome of this is that we don't ship WebSocketStream and we fold what we learned into the WebTransport API. But I like to think WebSocketStream will be independently useful as a small API that solves an immediate problem.

I'm still not 100% convinced about that aspect but I trust your and Domenic's assessment.

Cheers
Lennart



On Tuesday, 6 August 2019 16:35:36 UTC+2, Adam Rice wrote:
Hey Adam, this is great work. Quite similar to what I proposed for WebRTC data channels as well.

For the case of streaming individual messages, I'm excited about what @martinthompson proposed here, which is that we can use a subclass of the body mixin from the fetch standard as a message representation. This avoids the mental confusion of talking about streams-of-streams, and also brings for free the swiss-army-knife nature of the Response object--you can create a message from anything (including a stream) and read a message as anything (including a stream).

This will not be a version 1 feature. I currently envision that we'll need bring-your-own-buffer binary streams first.

I don't think WebTransport should or will be limited to QUIC, so I don't understand why this API cannot be incorporated into it. It would be the perfect opportunity to combine efforts here and the WebTransport folks could use someone who has experience with the streams API since they are migrating their API to use the streams API as well. So, I think it's about weighing up a quick improvement vs. a mid-term win. And, personally, I'd prefer the mid-term win instead of having two largely similar APIs at the end. People have settled with polling bufferedAmount for now. It's not great but they can wait a little longer and benefit from a less fragmented API surface. (/cc Peter)

I don't think it's an and/or choice. WebTransport is solving a significantly harder problem, and will have a larger API as a result. In the meantime, developers can play with the WebSocketStream API (available behind a flag Real Soon Now) and we find out where the bumps are.

One possible outcome of this is that we don't ship WebSocketStream and we fold what we learned into the WebTransport API. But I like to think WebSocketStream will be independently useful as a small API that solves an immediate problem.



On Tue, 6 Aug 2019 at 22:55, <lennar...@gmail.com> wrote:
Hey Adam, this is great work. Quite similar to what I proposed for WebRTC data channels as well.

I don't think WebTransport should or will be limited to QUIC, so I don't understand why this API cannot be incorporated into it. It would be the perfect opportunity to combine efforts here and the WebTransport folks could use someone who has experience with the streams API since they are migrating their API to use the streams API as well. So, I think it's about weighing up a quick improvement vs. a mid-term win. And, personally, I'd prefer the mid-term win instead of having two largely similar APIs at the end. People have settled with polling bufferedAmount for now. It's not great but they can wait a little longer and benefit from a less fragmented API surface. (/cc Peter)

That being said, again, it's great to see progress in this area and you can ping me in IRC (lgrahl) if you want to discuss anything in detail.

Cheers
Lennart

On Friday, 12 July 2019 05:07:16 UTC+2, Adam Rice wrote:
ri...@chromium.org,yhi...@chromium.org https://github.com/ricea/websocketstream-explainer/blob/master/README.md https://docs.google.com/document/d/1XuxEshh5VYBYm1qRVKordTamCOsR-uGQBCYFcHXP4L0/edit https://github.com/w3ctag/design-reviews/issues/394 The WebSocket API provides a JavaScript interface to the RFC6455 WebSocket protocol. While it has served well, it is awkward from an ergonomics perspective and is missing the important feature of backpressure. The intent of the WebSocketStream API is to resolve these deficiencies by integrating streams with the WebSocket API. Currently applying backpressure to received messages is not possible with the WebSocket API. When messages arrive faster than the page can handle them, the render process will either fill up memory buffering those messages, become unresponsive due to 100% CPU usage, or both. Applying backpressure to sent messages is possible but involves polling the bufferedAmount property which is inefficient and unergonomic.
The main risk is that it fails to become an interoperable part of the web platform if other browsers do not implement it. Firefox: No public signals Edge: No public signals Safari: No public signals Web developers: No signals A major focus of the new API is improving ergonomics over the existing WebSocket API. Everything except for correct backpressure behaviour can be polyfilled. Developers who are sensitive to backpressure may prefer to feature-detect and fall back to application-level backpressure if the feature is not available. Security posture is the same as the existing WebSocket API.
The necessary probes will be included in the code so that existing WebSocket debugging facilities should work as-is. Yes The feature is easy to support everywhere existing Blink WebSocket support exists. Blink WebSockets are supported on every Blink platform. No The feature will be tested in web platform tests. https://chromestatus.com/feature/5189728691290112
This intent message was generated by Chrome Platform Status.

--
You received this message because you are subscribed to the Google Groups "blink-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to blin...@chromium.org.

Peter Thatcher

unread,
Aug 12, 2019, 10:08:15 PM8/12/19
to Lennart Grahl, blink-dev, Jan-Ivar Bruaroey
I took a quick look. It seem like a WebSocketStream.connection is very similar to a BidirectionalStream in WebTransport.  Both have a .readable (ReadableStream) and a .writable (WritableStream).  So there appears to be alignment there, and JS code using one could probably use the other fairly interchangeably.

That's based on the latest PR here: https://github.com/WICG/web-transport/pull/46


Jerry Liu

unread,
Aug 13, 2019, 2:19:38 PM8/13/19
to Peter Thatcher, Lennart Grahl, blink-dev, Jan-Ivar Bruaroey
So why don't we move websocket handling to web worker instead? In that way, main thread won't be blocked by the flood of messages.

Also, pressure controlling is very important to real message systems. I don't think serious websocket use cases will ignore this in their application layer. If server have totally no idea whether clients can consume the messages, then it also means data will be pending inside server's memory due to bandwidth differences, which may be a problem to the message system itself.

'Peter Thatcher' via blink-dev <blin...@chromium.org> 于2019年8月13日周二 上午10:08写道:
To unsubscribe from this group and stop receiving emails from it, send an email to blink-dev+...@chromium.org.
To view this discussion on the web visit https://groups.google.com/a/chromium.org/d/msgid/blink-dev/CAJrXDUGFL-_3QTP8m15eYyJHPAtCFWCzGOEmbsYU%3D9E-%2BzQsBg%40mail.gmail.com.

Adam Rice

unread,
Aug 15, 2019, 2:43:22 AM8/15/19
to Jerry Liu, Peter Thatcher, Lennart Grahl, blink-dev, Jan-Ivar Bruaroey
So why don't we move websocket handling to web worker instead? In that way, main thread won't be blocked by the flood of messages.

Yes, sites can avoid blocking the main thread by creating their WebSockets in a web worker instead. However, this doesn't solve the memory exhaustion problem. Also, using a web worker may not be viable for all application architectures. If all messages need to be forwarded to the main thread anyway, then using a web worker won't help.

Also, pressure controlling is very important to real message systems. I don't think serious websocket use cases will ignore this in their application layer. If server have totally no idea whether clients can consume the messages, then it also means data will be pending inside server's memory due to bandwidth differences, which may be a problem to the message system itself.

I think even when it can be implemented in the application layer, TCP flow control is preferable as it takes into account window sizes and network conditions. It also makes more efficient use of network bandwidth.

I see WebSockets as a low-level primitive where semantics are left to the application layer. But in the case of backpressure, it's a capability we have but are not exposing to JavaScript. I don't think applications should be required to reimplement something which already exists.

Reply all
Reply to author
Forward
0 new messages