whatwg / streams

Streams Standard
https://streams.spec.whatwg.org/
Other
1.35k stars 160 forks source link

Make the internal queue observable #1167

Open amn opened 3 years ago

amn commented 3 years ago

Since desiredSize exists, and is updated internally (being a property of the stream's controller) anyway, wouldn't exposing the queue for [obviously read-only] observation, on a stream itself, be a safe and useful feature?

By observability of the queue I don't just mean arbitrary stream consumers being able to read its queue size, but also ability to register notifications about changes to the size of the queue.

The first one is arguably achievable with some ObservableReadableStream (inapt name, since we're only talking about observing the queue, but for the sake of example) subclass:

class ObservableReadableStream extends ReadableStream {
    constructor(underlyingSource, ...rest) {
        super({ ...underlyingSource, start: controller => {
            Object.defineProperty(this, "desiredSize", { get: () => controller.desiredSize });
            underlyingSource.start(controller);
        }, ...rest);
    }
}

Implementing event notifications for when the queue changes size, would be more involved -- the only way I can think of is to plug into whatever would be calling controller.enqueue for a specific stream, for when the queue grows in size, but also into read calls on obtained reader, which seems to be complicated by the fact one doesn't necessarily have control over the class of reader returned by getReader and that a reader may also be obtained with a new ReadableStream...Reader(stream). Point being, I am not at all certain "monkey patching" readable streams even by sub-classing is elegant or simple.

One could, in theory, implement observability with a a readable stream wrapping an underlying stream, merely emitting queue size change events for the underlying stream, and otherwise simply providing underlying stream data without change. But I am afraid as elegant as it may sound from a certain angle, it'd be a non-negligible overhead cost for what one'd be getting.

Since a queue is a limited resource (may consume usable memory), allowing its (again, read-only) introspection from arbitrary locations can't hurt but may save a lot of boiler-plating on part of applications which want to have wider access to queue sizes on streams they use and/or even expose these sizes to users, e.g. as telemetry.

Am I onto something, or have I missed something very obvious?

ricea commented 3 years ago

This would make the internal behaviour of pipes visible, which is something we've tried to avoid for optimisation purposes. I suspect interaction with transferable streams will also be problematic for similar reasons. These concerns apply more strongly to platform streams rather than those implemented in JavaScript.

I feel it also breaks encapsulation in a way that is undesirable. From my point of view, the inability to peek at the queue is a feature, not a bug.

amn commented 3 years ago

I understand your point from the implementation perspective, however I did stress explicitly that the ability to peek at the queue is at any rate made "uninvasive" in the sense that the queue is not disturbed, merely observed. Furthermore, since the queue is exposed to at least the constructor of the stream -- through the initialization object being passed a controller with the desiredSize property, the ability to observe changes in queue size can also only be made observable to the stream constructor (whether through the subclass calling super constructor or the caller calling new ReadableStream(...).

It doesn't have to leak to "arbitrary" stream user context like whatever may access the stream. It may only be made available to the stream creator/owner on the same controller object that already makes desiredSize available. Except that there is no way to reliably observe changes to the size of the queue.

Performance is obviously important, more so for streams -- I agree. But if the caller wants to observe the queue anyway, can it be made so that the penalty is only paid for that particular stream, when observation is enabled? One could utilize the ....Observer pattern -- employed with MutationObserver, ResizeObserver, IntersectionObserver etc. An argument can be made the pattern is employed precisely to otherwise not perturb default behaviour of respectively, document nodes, element dimensions, and inter-element layout interactions etc. In the same way, attaching an observer to a stream via observer.observe(stream) (and, conversely, unobserve) could be implemented in a way where only observed streams bear the cost?

Like I said, the queue is a finite and variable-sized resource, much like the rest of the objects controlled by an application directly or indirectly. Since desiredSize is already exposed anyway, why not expose ability to observe change to said size? I mean it seems that the queue can already be peeked at (through the controller).

Practical example would be a stream where difference between rates of enqueueing (writing) and dequeuing (reading) data could be useful to expose to the end user. For instance, with a stream that encapsulates chunks generated by a MediaRecorder object, for reading by a consumer that then dequeue the contents for sending over the network. If the network is slow or intermittently unavailable, the queue will grow and is at the risk of overflowing. At least knowing how close one is to such a condition -- by assessing the size of the queue as it changes -- is arguably a useful metric to offer for a media application. Of course, one may argue that flow control can be implemented outside the stream (i.e. not enqueuing more chunks for a time period) -- but then I'd argue much of the point of using a stream in the first place, would be gone.

youennf commented 2 years ago

This would make the internal behaviour of pipes visible

Would it be fine if this would only be visible to a stream reader/writer for instance?