nodejs/node · #66052

stream: trim per-stream costs in webstreams

mcollina · merged Sep 20, 20263 files · 103 + / 150
lib/internal/webstreams/readablestream.js69 + / 94
@@ -253,6 +253,13 @@ class ReadableStream {    */   constructor(source = kEmptyObject, strategy = kEmptyObject) {     markTransferMode(this, false, true);+    // Internal construction (tee, transform streams, adapters, transfer):+    // the caller sets up the controller, so every ReadableStream shares+    // one hidden class and no per-instance prototype swap is needed.+    if (source === kSkipThrow) {+      this[kState] = createReadableStreamState();+      return;+    }     validateObject(source, 'source', kValidateObjectAllowObjects);     validateObject(strategy, 'strategy', kValidateObjectAllowObjectsAndNull);     this[kState] = createReadableStreamState();@@ -718,22 +725,8 @@ ObjectDefineProperties(ReadableStream, {   from: kEnumerableProperty, }); -function InternalTransferredReadableStream() {-  ObjectSetPrototypeOf(this, ReadableStream.prototype);-  markTransferMode(this, false, true);-  this[kType] = 'ReadableStream';-  this[kState] = createReadableStreamState();-}--ObjectSetPrototypeOf(InternalTransferredReadableStream.prototype, ReadableStream.prototype);-ObjectSetPrototypeOf(InternalTransferredReadableStream, ReadableStream);- function TransferredReadableStream() {-  const stream = new InternalTransferredReadableStream();--  stream.constructor = ReadableStream;--  return stream;+  return new ReadableStream(kSkipThrow); }  TransferredReadableStream.prototype[kDeserialize] = () => {};@@ -1350,57 +1343,29 @@ ObjectDefineProperties(ReadableByteStreamController.prototype, {   [SymbolToStringTag]: getNonWritablePropertyDescriptor(ReadableByteStreamController.name), }); -function InternalReadableStream(start, pull, cancel, highWaterMark, size) {-  ObjectSetPrototypeOf(this, ReadableStream.prototype);-  markTransferMode(this, false, true);-  this[kType] = 'ReadableStream';-  this[kState] = createReadableStreamState();-  const controller = new ReadableStreamDefaultController(kSkipThrow);+function createReadableStream(start, pull, cancel, highWaterMark = 1, size = defaultSizeAlgorithm) {+  const stream = new ReadableStream(kSkipThrow);   setupReadableStreamDefaultController(-    this,-    controller,+    stream,+    new ReadableStreamDefaultController(kSkipThrow),     start,     pull,     cancel,     highWaterMark,     size);-}--ObjectSetPrototypeOf(InternalReadableStream.prototype, ReadableStream.prototype);-ObjectSetPrototypeOf(InternalReadableStream, ReadableStream);--function createReadableStream(start, pull, cancel, highWaterMark = 1, size = defaultSizeAlgorithm) {-  const stream = new InternalReadableStream(start, pull, cancel, highWaterMark, size);--  // For spec compliance the InternalReadableStream must be a ReadableStream-  stream.constructor = ReadableStream;   return stream; } -function InternalReadableByteStream(start, pull, cancel) {-  ObjectSetPrototypeOf(this, ReadableStream.prototype);-  markTransferMode(this, false, true);-  this[kType] = 'ReadableStream';-  this[kState] = createReadableStreamState();-  const controller = new ReadableByteStreamController(kSkipThrow);+function createReadableByteStream(start, pull, cancel) {+  const stream = new ReadableStream(kSkipThrow);   setupReadableByteStreamController(-    this,-    controller,+    stream,+    new ReadableByteStreamController(kSkipThrow),     start,     pull,     cancel,     0,     undefined);-}--ObjectSetPrototypeOf(InternalReadableByteStream.prototype, ReadableStream.prototype);-ObjectSetPrototypeOf(InternalReadableByteStream, ReadableStream);--function createReadableByteStream(start, pull, cancel) {-  const stream = new InternalReadableByteStream(start, pull, cancel);--  // For spec compliance the InternalReadableByteStream must be a ReadableStream-  stream.constructor = ReadableStream;   return stream; } @@ -1630,6 +1595,13 @@ function readableStreamPipeTo(   // tells us that the promise must be rejected even   // when error is undefine.   function finalize(rejected, error) {+    // The pipe is the only observer of the reader's and writer's promise+    // records (including the ready hook installed by parkOnReady), and+    // it is done with them: dropping them lets release skip the+    // pending-promise probes and the rejections nothing would handle.+    writer[kState].ready = undefined;+    writer[kState].close = undefined;+    reader[kState].close = undefined;     writableStreamDefaultWriterRelease(writer);     readableStreamReaderGenericRelease(reader);     if (signal !== undefined)@@ -1727,12 +1699,6 @@ function readableStreamPipeTo(       PromisePrototypeThen(promise, undefined, action);   } -  function watchClosed(stream, promise, action) {-    if (stream[kState].state === 'closed')-      action();-    else-      PromisePrototypeThen(promise, action, () => {});-  }    // The pump loop is callback-driven to avoid per-iteration promise   // allocations. At most one read is in flight at a time, so one read@@ -1863,15 +1829,34 @@ function readableStreamPipeTo(    pump(); -  watchErrored(source, readerClosedPromise(reader).promise, (error) => {+  function onSourceErrored(error) {     if (!preventAbort) {       return shutdownWithAnAction(         () => writableStreamAbort(dest, error),         true,         error);     }     shutdown(true, error);-  });+  }++  function onSourceClosed() {+    if (!preventClose) {+      return shutdownWithAnAction(+        () => writableStreamDefaultWriterCloseWithErrorPropagation(writer));+    }+    shutdown();+  }++  // The spec installs the source-errored watcher before the dest-errored+  // one and the source-closed watcher last; a source that is already+  // errored is handled before the dest watcher is installed, and an+  // already-closed source after it, as before.+  if (source[kState].state === 'errored') {+    onSourceErrored(source[kState].storedError);+  } else if (source[kState].state !== 'closed') {+    PromisePrototypeThen(+      readerClosedPromise(reader).promise, onSourceClosed, onSourceErrored);+  }    watchErrored(dest, writerClosedPromise(writer).promise, (error) => {     if (!preventCancel) {@@ -1883,13 +1868,8 @@ function readableStreamPipeTo(     shutdown(true, error);   }); -  watchClosed(source, readerClosedPromise(reader).promise, () => {-    if (!preventClose) {-      return shutdownWithAnAction(-        () => writableStreamDefaultWriterCloseWithErrorPropagation(writer));-    }-    shutdown();-  });+  if (source[kState].state === 'closed')+    onSourceClosed();    if (writableStreamCloseQueuedOrInFlight(dest) ||       dest[kState].state === 'closed') {@@ -2899,29 +2879,27 @@ function setupReadableStreamDefaultController(    const startResult = startAlgorithm(); +  const started = () => {+    controller[kState].started = true;+    assert(!controller[kState].pulling);+    assert(!controller[kState].pullAgain);+    readableStreamDefaultControllerCallPullIfNeeded(controller);+  };+   if (startResult === null ||       (typeof startResult !== 'object' && typeof startResult !== 'function')) {     // Non-thenable start result: fulfillment is guaranteed and no .then-    // lookup on the result is observable, so run the post-start step-    // directly at the exact microtask position the promise reaction-    // would have had, skipping two promise allocations.-    queueMicrotask(() => {-      controller[kState].started = true;-      assert(!controller[kState].pulling);-      assert(!controller[kState].pullAgain);-      readableStreamDefaultControllerCallPullIfNeeded(controller);-    });+    // lookup on the result is observable, so the post-start step runs at+    // the exact microtask position the promise reaction would have had.+    queueMicrotask(started);     return;   } +  // The wrapper promise matches the reference implementation's+  // promiseResolvedWith(), whose extra microtask hops WPT relies on.   PromisePrototypeThen(     new Promise((r) => r(startResult)),-    () => {-      controller[kState].started = true;-      assert(!controller[kState].pulling);-      assert(!controller[kState].pullAgain);-      readableStreamDefaultControllerCallPullIfNeeded(controller);-    },+    started,     (error) => readableStreamDefaultControllerError(controller, error)); } @@ -3783,26 +3761,23 @@ function setupReadableByteStreamController(    const startResult = startAlgorithm(); +  const started = () => {+    controller[kState].started = true;+    assert(!controller[kState].pulling);+    assert(!controller[kState].pullAgain);+    readableByteStreamControllerCallPullIfNeeded(controller);+  };++  // See setupReadableStreamDefaultController.   if (startResult === null ||       (typeof startResult !== 'object' && typeof startResult !== 'function')) {-    // See setupReadableStreamDefaultController.-    queueMicrotask(() => {-      controller[kState].started = true;-      assert(!controller[kState].pulling);-      assert(!controller[kState].pullAgain);-      readableByteStreamControllerCallPullIfNeeded(controller);-    });+    queueMicrotask(started);     return;   }    PromisePrototypeThen(     new Promise((r) => r(startResult)),-    () => {-      controller[kState].started = true;-      assert(!controller[kState].pulling);-      assert(!controller[kState].pullAgain);-      readableByteStreamControllerCallPullIfNeeded(controller);-    },+    started,     (error) => readableByteStreamControllerError(controller, error)); } 
lib/internal/webstreams/util.js9 + / 7
@@ -179,12 +179,12 @@ class Queue {   // Single-slot entries (readable byte controller chunk records).    push(entry) {+    if (this.length === this.list.length)+      this.grow();     const tail = this.tail;     this.list[tail] = entry;     this.tail = (tail + 1) & this.capacityMask;     this.length++;-    if (this.tail === this.head)-      this.grow();   }    shift() {@@ -207,14 +207,14 @@ class Queue {   // never need to wrap.    pushPair(value, size) {+    if (this.length * 2 === this.list.length)+      this.grow();     const tail = this.tail;     const list = this.list;     list[tail] = value;     list[tail + 1] = size;     this.tail = (tail + 2) & this.capacityMask;     this.length++;-    if (this.tail === this.head)-      this.grow();   }    // Returns the dequeued value; the size of the same entry is left in@@ -237,9 +237,11 @@ class Queue {     return this.list[this.head];   } -  // The ring is completely full (the post-push tail caught up with the-  // head): double the capacity, re-linearizing from the head so index-  // arithmetic stays trivial.+  // The ring is completely full (the tail has caught up with the head, so+  // the next push would overwrite the oldest entry): double the capacity,+  // re-linearizing from the head so index arithmetic stays trivial.+  // Growing before the push rather than after it lets the initial 8-slot+  // ring hold four (value, size) pairs without reallocating.   grow() {     const list = this.list;     const capacity = list.length;
lib/internal/webstreams/writablestream.js25 + / 49
@@ -183,6 +183,13 @@ class WritableStream {    */   constructor(sink = kEmptyObject, strategy = kEmptyObject) {     markTransferMode(this, false, true);+    // Internal construction (transform streams, adapters, transfer):+    // the caller sets up the controller, so every WritableStream shares+    // one hidden class and no per-instance prototype swap is needed.+    if (sink === kSkipThrow) {+      this[kState] = createWritableStreamState();+      return;+    }     validateObject(sink, 'sink', kValidateObjectAllowObjects);     validateObject(strategy, 'strategy', kValidateObjectAllowObjectsAndNull);     const type = sink?.type;@@ -351,22 +358,8 @@ ObjectDefineProperties(WritableStream.prototype, {   [SymbolToStringTag]: getNonWritablePropertyDescriptor(WritableStream.name), }); -function InternalTransferredWritableStream() {-  ObjectSetPrototypeOf(this, WritableStream.prototype);-  markTransferMode(this, false, true);-  this[kType] = 'WritableStream';-  this[kState] = createWritableStreamState();-}--ObjectSetPrototypeOf(InternalTransferredWritableStream.prototype, WritableStream.prototype);-ObjectSetPrototypeOf(InternalTransferredWritableStream, WritableStream);- function TransferredWritableStream() {-  const stream = new InternalTransferredWritableStream();--  stream.constructor = WritableStream;--  return stream;+  return new WritableStream(kSkipThrow); }  TransferredWritableStream.prototype[kDeserialize] = () => {};@@ -559,33 +552,18 @@ ObjectDefineProperties(WritableStreamDefaultController.prototype, {   [SymbolToStringTag]: getNonWritablePropertyDescriptor(WritableStreamDefaultController.name), }); -function InternalWritableStream(start, write, close, abort, highWaterMark, size) {-  ObjectSetPrototypeOf(this, WritableStream.prototype);-  markTransferMode(this, false, true);-  this[kType] = 'WritableStream';-  this[kState] = createWritableStreamState();--  const controller = new WritableStreamDefaultController(kSkipThrow);+function createWritableStream(start, write, close, abort, highWaterMark = 1, size = defaultSizeAlgorithm) {+  const stream = new WritableStream(kSkipThrow);   setupWritableStreamDefaultController(-    this,-    controller,+    stream,+    new WritableStreamDefaultController(kSkipThrow),     start,     write,     close,     abort,     highWaterMark,     size,   );-}--ObjectSetPrototypeOf(InternalWritableStream.prototype, WritableStream.prototype);-ObjectSetPrototypeOf(InternalWritableStream, WritableStream);--function createWritableStream(start, write, close, abort, highWaterMark = 1, size = defaultSizeAlgorithm) {-  const stream = new InternalWritableStream(start, write, close, abort, highWaterMark, size);--  // For spec compliance the InternalWritableStream must be a WritableStream-  stream.constructor = WritableStream;   return stream; } @@ -1401,29 +1379,27 @@ function setupWritableStreamDefaultController(    const startResult = startAlgorithm(); +  const started = () => {+    assert(stream[kState].state === 'writable' ||+           stream[kState].state === 'erroring');+    controller[kState].started = true;+    writableStreamDefaultControllerAdvanceQueueIfNeeded(controller);+  };+   if (startResult === null ||       (typeof startResult !== 'object' && typeof startResult !== 'function')) {     // Non-thenable start result: fulfillment is guaranteed and no .then-    // lookup on the result is observable, so run the post-start step-    // directly at the exact microtask position the promise reaction-    // would have had, skipping two promise allocations.-    queueMicrotask(() => {-      assert(stream[kState].state === 'writable' ||-             stream[kState].state === 'erroring');-      controller[kState].started = true;-      writableStreamDefaultControllerAdvanceQueueIfNeeded(controller);-    });+    // lookup on the result is observable, so the post-start step runs at+    // the exact microtask position the promise reaction would have had.+    queueMicrotask(started);     return;   } +  // The wrapper promise matches the reference implementation's+  // promiseResolvedWith(), whose extra microtask hops WPT relies on.   PromisePrototypeThen(     new Promise((r) => r(startResult)),-    () => {-      assert(stream[kState].state === 'writable' ||-             stream[kState].state === 'erroring');-      controller[kState].started = true;-      writableStreamDefaultControllerAdvanceQueueIfNeeded(controller);-    },+    started,     (error) => {       assert(stream[kState].state === 'writable' ||              stream[kState].state === 'erroring');