-
Notifications
You must be signed in to change notification settings - Fork 161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Piping to writable streams with HWM 0? #1158
Comments
Although adding such a mechanism might be useful, I want to make sure we've figured out the root issue here. In particular I worry that maybe we got the semantics of HWM wrong for WritableStream, if there's an expressive gap here. But, I'm having a hard time articulating the expressive gap. Let's say we had a WritableStream with HWM = 0 that called My best guess was something like "I currently have nothing in the queue, and I intend to consume whatever you give me ASAP, but please don't give me more than 1 chunk". But... that seems like the right semantics for HWM = 1. So I admit I'm confused. |
I agree, it's tricky. I'm also still trying to wrap my head around it, it's not very concrete yet. 😛 I'm mainly coming at this from the This may also be necessary if/when we flesh out writable byte streams (#495), and (by extension) transform streams with readable/writable byte stream end(s). If we want "reverse BYOB" or "please use this buffer" semantics, then it would be nice if a BYOB request from the readable end could be forwarded to the writable end. But then the writable end is almost required to have HWM = 0, so it can wait for a read request to provide a buffer (instead of allocating one by itself). 🤔 ...Or maybe "reverse BYOB" is too weird, and a "regular" BYOB writer is fine. I don't know yet. 😅
With HWM = 1, the stream is asking to always put at least one chunk in its queue, and to try and hold back if it's not done processing that latest chunk yet. Here, most of the time With HWM = 0, the stream is asking not to put anything its queue, except for the case where it wants a chunk. Here, most of the time They're similar, but with HWM = 1 the "default" state is for |
The goal of avoiding the queue bloat from TransformStream is a good one, but the details of how to do it are tricky. I think it may be sufficient to have a |
Yeah, these make sense. But, could you phrase the transform stream case--which appears to be something in between---in this way? I.e. what is the writable end of such a no-queueing transform stream asking for? How does that different from what the the HWM = 1 case is asking for? |
"I have no queue, so I couldn't consume a chunk before, but I'm ready to consume one now (because I have a taker for it)". (I know it technically has a queue, but the intent of HWM = 0 seems to be for it to remain empty ideally, so for all intents and purposes it's acting like it has no queue).
How about This would be useful for realtime streams where chunks are expensive and prebuffering undesirable (pulling too soon may yield a less fresh realtime frame). |
Sorry, still confused...
compared to
makes it sound like HWM = 0 is perfect for this use case. When the sink wants a chunk, it will signal that through writer.ready. |
Yes it is perfect. I was merely trying to articulate the expressive gap (What is that WritableStream trying to express?) We're on the same page I think. |
Well, it feels like there's still a missing capability here, as noted by the OP and even your comment in #1157 indicating that some way to get less buffering would be helpful. I'm just confused as to what that missing capability is still, i.e. I cannot understand what the proposed controller.requestData() would give that isn't already given by using HWM = 0 or HWM = 1. |
I was just bikeshedding on But you're right, there is a (side) issue in #1157 (comment) with effectively implementing a FrameDropper TransformStream. Today, it requires |
I think the OP does a good job of explaining the problem with HWM =1: it sprinkles chunks into the pipe early, just because HWM = 1 says to do it. I see no inherent reason a TransformStream has to require this to operate, when it could For sources with time-sensitive chunks — which is any source that may drop chunks as a solution to back-pressure (a camera or WebTransport datagram receiver) — operating on already-queued chunks means potentially operating on old chunks. Example. |
So maybe my confusion is this. @MattiasBuelens said
I assume this is referring to the current spec? However, I can't figure out in what scenario with HWM = 0 |
No, this is describing the desired semantics, what I would like HWM = 0 to do. "When the sink eventually wants a chunk" means "when In the current spec, when HWM = 0, |
Oh! That explains the confusion. What do you think of @ricea's suggestion in #1158 (comment) for that purpose? Or even just changing the ready condition from I think that's less expressive than a dedicated method, but it might be a better general fix if right now HWM = 0 is just a footgun. |
I don't know if I'd want
But if you have good arguments for adding it to the queuing strategy, I'm all ears. 🙂
That would mean a pipe operation would never fill exactly up to the HWM, you'd always go over the HWM, right? 🤔 I'm still leaning towards a controller method like
|
True. I guess another alternative is special-casing 0 HWM, since in that case you don't want to exactly fill up to the HWM (because doing so stalls the stream). But that's kind of icky... |
Yeah, that's also what I was thinking. If we consider HWM = 0 to be broken, we could special case it. I think we would only need to change a single abstract op:
Alternatively, if want zero-size chunks to also cause backpressure, we could change it to "if controller.[[stream]].[[writeRequests]] is not empty" or something. Still, not sure how I feel about this. Might be difficult to explain this behavior to developers. But then again, so would explaining the need for |
Is this different from just making HWM 0 equivalent to HWM 1? |
You're right, that change would make them equivalent. That's not what we want. Woops! 😅 Okay, then it's back to the explicit |
I believe it's different in the case where the queue total size is between 0 and 1. But the fundamental issue where it causes a chunk to end up in the queue remains; see below.
I was hoping there'd be some way to get the benefit here, without a new API, and with some way of "fixing" HWM = 0 so that it's less of a footgun and more useful. But going back to the description of the desired behavior,
I guess we might need something different after all. If we try to use the HWM, we're intricately tied to the queue; the only way to switch between "not ready" and "wants a chunk" mode is by manipulating the contents of the queue, presumably by dropping it from containing a chunk to not containing a chunk. But that means the queue must contain a chunk during the "not ready" state, which is what we want to avoid! So I am convinced that we cannot do anything just by tweaking the HWM/queue comparison mechanism. That leaves two options:
How do these compare in expressiveness? At first I thought the explicit method was more expressive, since you could combine it with any arbitrary HWM. But I wonder if that's actually true... does it make sense to use it with non-0 HWMs? How do each compare for realistic use cases? It almost feels like |
I don't think so, because releasing back pressure (which is built on queues) works fine then (there's water on the wheel). This doesn't feel like a different strategy to me, so much as a way for WritableStream to signal for data (sorta) like one can with ReadableStream. |
I think I see two options for the API:
I am leaning towards the explicit options, because the implicit version may require sink authors to artificially extend the time before resolving the write() promise just to stop another chunk from being written. The implicit version would only do anything when |
Right now, piping to a writable stream with
{ highWaterMark: 0 }
stalls indefinitely:This makes it impossible to pipe through a
TransformStream
without increasing the total queue size of a pipe chain by at least one chunk:This is unfortunate, since there are many use cases for synchronous
TransformStream
s that shouldn't need buffering (i.e. every call totransform()
immediately results in at least oneenqueue()
):mapTransform(fn)
, similar toarray.map(fn)
:TextEncoderStream
andTextDecoderStream
from Encoding.Prior discussions on this topic noted that this is not possible.
writer.desiredSize
is always<= 0
, sowriter.ready
is always pending:But that got me thinking. A
ReadableStream
's source can bepull()
ed as a result ofreader.read()
, even ifcontroller.desiredSize <= 0
. Maybe aWritableStream
's sink should then also be able to release backpressure even ifwriter.desiredSize <= 0
? 🤔We could add a method on
WritableStreamDefaultController
(controller.pull()
?controller.releaseBackpressure()
?controller.notifyReady()
?) that would have the result of immediately resolving the currentwriter.ready
promise. Internally, we would do something likeWritableStreamUpdateBackpressure(stream, false)
. My hope is that we can then use this insideTransformStreamSetBackpressure()
, so that pulling from the readable end of a transform stream would also resolveready
on the writable end....Or am I missing something very obvious? 😛
The text was updated successfully, but these errors were encountered: