This is an automated email from the ASF dual-hosted git repository.

He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git


The following commit(s) were added to refs/heads/main by this push:
     new 2610cda2af perf: batch async boundary elements (#3288)
2610cda2af is described below

commit 2610cda2af448c7fa23fa97fc937e711b74e03fd
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Sun Jul 5 23:18:20 2026 +0800

    perf: batch async boundary elements (#3288)
    
    Motivation:
    Async stream islands send each element across the internal actor boundary 
as a separate boundary event, which adds per-element allocation and mailbox 
traffic.
    
    Modification:
    Batch elements produced during an interpreter run for internal stream 
boundaries and flush them when the interpreter parks. Let the input boundary 
implement the internal BoundarySubscriber directly, cap output batch size at 
1024 elements, flush pending batch elements before completion or upstream 
failure, and keep cancellation and already-terminated paths clearing pending 
elements. Store initialized boundary lists in arrays, add a fused baseline to 
the async-boundary benchmark, and ad [...]
    
    Result:
    Internal async boundary crossings allocate fewer boundary events and actor 
messages while preserving element order, demand bounds, terminal ordering, and 
supervision behavior.
    
    Tests:
    - rtk scalafmt --mode diff-ref=origin/main -- passed
    - rtk scalafmt --list --mode diff-ref=origin/main -- passed
    - rtk git diff --check -- passed
    - rtk sbt "stream-tests / Test / testOnly 
org.apache.pekko.stream.FusingSpec -- -z \"drain asynchronous boundary batches 
before failing\"" -- passed, 1 test
    - rtk sbt "stream-tests / Test / testOnly 
org.apache.pekko.stream.FusingSpec 
org.apache.pekko.stream.scaladsl.PublisherSinkSpec 
org.apache.pekko.stream.io.FileSinkSpec 
org.apache.pekko.stream.scaladsl.FlowMapWithResourceSpec" "stream / 
mimaReportBinaryIssues" -- passed, 53 tests and MiMa
    - rtk bash -lc 'set -o pipefail; sbt "bench-jmh/Jmh/run -wi 1 -i 3 -w 5s -r 
5s -f 1 -prof gc .*AsyncBoundaryThroughputBenchmark.*" 2>&1 | tee 
/tmp/pekko-async-boundary-final-squashed.log' -- passed
    - rtk qodercli --help -- passed, confirmed -p/--print, --output-format, 
--cwd, and --attachment
    - qodercli stdout review of /tmp/project-review.diff -- passed, no must-fix 
findings
    - Independent subAgent review of /tmp/project-review.diff -- passed, no 
must-fix findings
    
    References:
    None - internal stream boundary throughput optimization
---
 .../stream/AsyncBoundaryThroughputBenchmark.scala  |   2 +-
 .../scala/org/apache/pekko/stream/FusingSpec.scala | 186 ++++++++++++++++
 .../pr-2916-boundary-event-allocation.excludes     |   6 +
 .../stream/impl/fusing/ActorGraphInterpreter.scala | 245 +++++++++++++++++----
 4 files changed, 398 insertions(+), 41 deletions(-)

diff --git 
a/bench-jmh/src/main/scala/org/apache/pekko/stream/AsyncBoundaryThroughputBenchmark.scala
 
b/bench-jmh/src/main/scala/org/apache/pekko/stream/AsyncBoundaryThroughputBenchmark.scala
index d508d074c3..b93ec299ca 100644
--- 
a/bench-jmh/src/main/scala/org/apache/pekko/stream/AsyncBoundaryThroughputBenchmark.scala
+++ 
b/bench-jmh/src/main/scala/org/apache/pekko/stream/AsyncBoundaryThroughputBenchmark.scala
@@ -52,7 +52,7 @@ class AsyncBoundaryThroughputBenchmark {
 
   implicit val system: ActorSystem = 
ActorSystem("AsyncBoundaryThroughputBenchmark", config)
 
-  @Param(Array("1", "3", "10"))
+  @Param(Array("0", "1", "3", "10"))
   var asyncBoundaries = 0
 
   var source: Source[Int, NotUsed] = _
diff --git 
a/stream-tests/src/test/scala/org/apache/pekko/stream/FusingSpec.scala 
b/stream-tests/src/test/scala/org/apache/pekko/stream/FusingSpec.scala
index 542dee5be8..03648f78c5 100644
--- a/stream-tests/src/test/scala/org/apache/pekko/stream/FusingSpec.scala
+++ b/stream-tests/src/test/scala/org/apache/pekko/stream/FusingSpec.scala
@@ -19,15 +19,20 @@ import duration._
 
 import org.apache.pekko
 import pekko.Done
+import pekko.stream.QueueOfferResult
 import pekko.stream.impl.UnfoldResourceSource
 import pekko.stream.impl.fusing.GraphInterpreter
 import pekko.stream.scaladsl._
 import pekko.stream.stage.GraphStage
+import pekko.stream.testkit.TestPublisher
 import pekko.stream.testkit.StreamSpec
 import pekko.stream.testkit.Utils.TE
+import pekko.stream.testkit.scaladsl.TestSink
 
 class FusingSpec extends StreamSpec {
 
+  val asyncBoundaryInputBuffer = Attributes.inputBuffer(16, 16)
+
   def actorRunningStage = {
     GraphInterpreter.currentInterpreter.context
   }
@@ -47,6 +52,187 @@ class FusingSpec extends StreamSpec {
         .sorted should ===(0 to 198 by 2)
     }
 
+    "preserve elements across repeated asynchronous boundary batches" in {
+      val elements = 1 to 5000
+
+      Source(elements)
+        .map(identity)
+        .async
+        .addAttributes(asyncBoundaryInputBuffer)
+        .runWith(Sink.seq)
+        .futureValue should ===(elements)
+    }
+
+    "preserve elements across chained asynchronous boundary batches" in {
+      val elements = 1 to 5000
+
+      Source(elements)
+        .map(identity)
+        .async
+        .addAttributes(asyncBoundaryInputBuffer)
+        .map(identity)
+        .async
+        .addAttributes(asyncBoundaryInputBuffer)
+        .runWith(Sink.seq)
+        .futureValue should ===(elements)
+    }
+
+    "flush asynchronous boundary batches when async input suspends the 
interpreter" in {
+      val elements = 1 to 64
+      val (queue, downstream) = Source
+        .fromGraph(Source.queue[Int](elements.size))
+        .async
+        .addAttributes(ActorAttributes.syncProcessingLimit(1) and 
asyncBoundaryInputBuffer)
+        .toMat(TestSink[Int]())(Keep.both)
+        .run()
+
+      downstream.request(elements.size)
+      elements.foreach { elem =>
+        queue.offer(elem) should ===(QueueOfferResult.Enqueued)
+      }
+      downstream.expectNextN(elements)
+
+      queue.complete()
+      downstream.expectComplete()
+    }
+
+    "drain asynchronous boundary batches before completing" in {
+      val elements = 1 to 64
+
+      Source(elements)
+        .async
+        .addAttributes(asyncBoundaryInputBuffer)
+        .runWith(TestSink[Int]())
+        .request(elements.size)
+        .expectNextN(elements)
+        .expectComplete()
+    }
+
+    "drain asynchronous boundary batches before failing" in {
+      val elements = 1 to 64
+      val ex = TE("boom")
+      val upstream = TestPublisher.probe[Int]()
+      val downstream = Source
+        .fromPublisher(upstream)
+        .async
+        .addAttributes(asyncBoundaryInputBuffer)
+        .runWith(TestSink[Int]())
+
+      downstream.request(elements.size + 1)
+
+      var sent = 0
+      while (sent < elements.size) {
+        val request = upstream.expectRequest()
+        val send = math.min(request, elements.size - sent).toInt
+        elements.slice(sent, sent + send).foreach(upstream.sendNext)
+        sent += send
+      }
+
+      upstream.sendError(ex)
+      downstream.expectNextN(elements)
+      downstream.expectError(ex)
+    }
+
+    "not emit elements after an asynchronous boundary failure" in {
+      val ex = TE("boom")
+      val probe = Source(1 to 64)
+        .concat(Source.failed(ex))
+        .async
+        .addAttributes(asyncBoundaryInputBuffer)
+        .runWith(TestSink[Int]())
+
+      probe.request(65)
+
+      var expected = 1
+      var errorSignalled = false
+      while (!errorSignalled && expected <= 64) {
+        probe.expectNextOrError(expected, ex) match {
+          case Right(_) =>
+            expected += 1
+          case Left(_) =>
+            errorSignalled = true
+        }
+      }
+      if (!errorSignalled) probe.expectError(ex)
+    }
+
+    "propagate cancellation with asynchronous boundary elements in flight" in {
+      val upstream = TestPublisher.probe[Int]()
+      val downstream = Source
+        .fromPublisher(upstream)
+        .async
+        .addAttributes(asyncBoundaryInputBuffer)
+        .runWith(TestSink[Int]())
+
+      downstream.request(16)
+      upstream.expectRequest() should be >= 16L
+      (1 to 16).foreach(upstream.sendNext)
+
+      downstream.expectNext(1)
+      downstream.cancel()
+
+      upstream.expectCancellation()
+    }
+
+    "not exceed downstream demand across asynchronous boundary batches" in {
+      val downstream = Source(1 to 1000)
+        .async
+        .addAttributes(asyncBoundaryInputBuffer)
+        .runWith(TestSink[Int]())
+
+      downstream.request(1)
+      downstream.expectNext(1)
+      downstream.expectNoMessage(100.millis)
+
+      downstream.request(2)
+      downstream.expectNext(2, 3)
+      downstream.expectNoMessage(100.millis)
+
+      downstream.cancel()
+    }
+
+    "preserve resuming supervision across asynchronous boundary batches" in {
+      Source(List(1, 2, -1, 3, 4))
+        .async
+        .addAttributes(asyncBoundaryInputBuffer)
+        .map { elem =>
+          require(elem > 0)
+          elem
+        }
+        
.withAttributes(ActorAttributes.supervisionStrategy(Supervision.resumingDecider))
+        .runWith(Sink.seq)
+        .futureValue should ===(Seq(1, 2, 3, 4))
+    }
+
+    "preserve restarting supervision across asynchronous boundary batches" in {
+      Source(List(1, 3, -1, 5, 7))
+        .async
+        .addAttributes(asyncBoundaryInputBuffer)
+        .scan(0) { (old, current) =>
+          require(current > 0)
+          old + current
+        }
+        
.withAttributes(ActorAttributes.supervisionStrategy(Supervision.restartingDecider))
+        .runWith(Sink.seq)
+        .futureValue should ===(Seq(0, 1, 4, 0, 5, 12))
+    }
+
+    "preserve stopping supervision across asynchronous boundary batches" in {
+      val ex = TE("boom")
+      Source(List(1, 2, -1, 3, 4))
+        .async
+        .addAttributes(asyncBoundaryInputBuffer)
+        .map {
+          case -1   => throw ex
+          case elem => elem
+        }
+        
.withAttributes(ActorAttributes.supervisionStrategy(Supervision.stoppingDecider))
+        .runWith(TestSink[Int]())
+        .request(5)
+        .expectNext(1, 2)
+        .expectError(ex)
+    }
+
     "use multiple actors when there are asynchronous boundaries in the 
subflows (manual)" in {
       val async = Flow[Int].map(x => { testActor ! actorRunningStage; x 
}).async
       Source(0 to 9)
diff --git 
a/stream/src/main/mima-filters/2.0.x.backwards.excludes/pr-2916-boundary-event-allocation.excludes
 
b/stream/src/main/mima-filters/2.0.x.backwards.excludes/pr-2916-boundary-event-allocation.excludes
index d8a5fbb51a..4b4d0fc10d 100644
--- 
a/stream/src/main/mima-filters/2.0.x.backwards.excludes/pr-2916-boundary-event-allocation.excludes
+++ 
b/stream/src/main/mima-filters/2.0.x.backwards.excludes/pr-2916-boundary-event-allocation.excludes
@@ -28,3 +28,9 @@ 
ProblemFilters.exclude[Problem]("org.apache.pekko.stream.impl.fusing.ActorGraphI
 
ProblemFilters.exclude[Problem]("org.apache.pekko.stream.impl.fusing.GraphInterpreterShell*Abort*")
 
ProblemFilters.exclude[Problem]("org.apache.pekko.stream.impl.fusing.GraphInterpreterShell*AsyncInput*")
 
ProblemFilters.exclude[Problem]("org.apache.pekko.stream.impl.fusing.GraphInterpreterShell*ResumeShell*")
+
+# Optimize internal async boundary batching
+ProblemFilters.exclude[FinalMethodProblem]("org.apache.pekko.stream.impl.fusing.ActorGraphInterpreter#BatchingActorInputBoundary.onNext")
+ProblemFilters.exclude[FinalMethodProblem]("org.apache.pekko.stream.impl.fusing.ActorGraphInterpreter#BatchingActorInputBoundary.onError")
+ProblemFilters.exclude[FinalMethodProblem]("org.apache.pekko.stream.impl.fusing.ActorGraphInterpreter#BatchingActorInputBoundary.onComplete")
+ProblemFilters.exclude[FinalMethodProblem]("org.apache.pekko.stream.impl.fusing.ActorGraphInterpreter#BatchingActorInputBoundary.onSubscribe")
diff --git 
a/stream/src/main/scala/org/apache/pekko/stream/impl/fusing/ActorGraphInterpreter.scala
 
b/stream/src/main/scala/org/apache/pekko/stream/impl/fusing/ActorGraphInterpreter.scala
index a7156ff29e..53dc14b6ca 100644
--- 
a/stream/src/main/scala/org/apache/pekko/stream/impl/fusing/ActorGraphInterpreter.scala
+++ 
b/stream/src/main/scala/org/apache/pekko/stream/impl/fusing/ActorGraphInterpreter.scala
@@ -53,6 +53,11 @@ import org.reactivestreams.Subscription
   object Resume extends DeadLetterSuppression with 
NoSerializationVerificationNeeded
   object Snapshot extends NoSerializationVerificationNeeded
 
+  private[stream] trait BoundarySubscriber extends Subscriber[Any] {
+    def next(elem: AnyRef): Unit
+    def batch(elements: Array[AnyRef]): Unit
+  }
+
   sealed abstract class BoundaryEvent extends DeadLetterSuppression with 
NoSerializationVerificationNeeded {
     def shell: GraphInterpreterShell
 
@@ -83,7 +88,7 @@ import org.reactivestreams.Subscription
     override def execute(): Unit = {
       if (GraphInterpreter.Debug)
         println(s"${boundary.shell.interpreter.Name}  onError 
port=${boundary.internalPortName}")
-      boundary.onError(cause)
+      boundary.receiveError(cause)
     }
 
     override def shell: GraphInterpreterShell = boundary.shell
@@ -95,7 +100,7 @@ import org.reactivestreams.Subscription
     override def execute(): Unit = {
       if (GraphInterpreter.Debug)
         println(s"${boundary.shell.interpreter.Name}  onComplete 
port=${boundary.internalPortName}")
-      boundary.onComplete()
+      boundary.receiveComplete()
     }
 
     override def shell: GraphInterpreterShell = boundary.shell
@@ -107,7 +112,20 @@ import org.reactivestreams.Subscription
     override def execute(): Unit = {
       if (GraphInterpreter.Debug)
         println(s"${boundary.shell.interpreter.Name} onNext $e 
port=${boundary.internalPortName}")
-      boundary.onNext(e)
+      boundary.receiveNext(e)
+    }
+
+    override def shell: GraphInterpreterShell = boundary.shell
+    override def logic: GraphStageLogic = boundary
+    override def cancel(): Unit = ()
+  }
+
+  final class OnNextBatch(boundary: BatchingActorInputBoundary, elements: 
Array[AnyRef]) extends SimpleBoundaryEvent {
+    override def execute(): Unit = {
+      if (GraphInterpreter.Debug)
+        println(
+          s"${boundary.shell.interpreter.Name} onNextBatch 
size=${elements.length} port=${boundary.internalPortName}")
+      boundary.receiveNextBatch(elements)
     }
 
     override def shell: GraphInterpreterShell = boundary.shell
@@ -121,7 +139,7 @@ import org.reactivestreams.Subscription
       if (GraphInterpreter.Debug)
         println(s"${boundary.shell.interpreter.Name}  onSubscribe 
port=${boundary.internalPortName}")
       boundary.shell.subscribeArrived()
-      boundary.onSubscribe(subscription)
+      boundary.receiveSubscribe(subscription)
     }
 
     override def shell: GraphInterpreterShell = boundary.shell
@@ -136,7 +154,8 @@ import org.reactivestreams.Subscription
       publisher: Publisher[Any],
       val internalPortName: String)
       extends UpstreamBoundaryStageLogic[Any]
-      with OutHandler {
+      with OutHandler
+      with BoundarySubscriber {
 
     if (size <= 0) throw new IllegalArgumentException("buffer size cannot be 
zero")
     if ((size & (size - 1)) != 0) throw new IllegalArgumentException("buffer 
size must be a power of two")
@@ -158,29 +177,32 @@ import org.reactivestreams.Subscription
 
     def setActor(actor: ActorRef): Unit = this.actor = actor
 
-    override def preStart(): Unit = {
-      publisher.subscribe(new Subscriber[Any] {
-        override def onError(t: Throwable): Unit = {
-          ReactiveStreamsCompliance.requireNonNullException(t)
-          actor ! new OnError(BatchingActorInputBoundary.this, t)
-        }
+    override def preStart(): Unit = publisher.subscribe(this)
 
-        override def onSubscribe(s: Subscription): Unit = {
-          ReactiveStreamsCompliance.requireNonNullSubscription(s)
-          actor ! new OnSubscribe(BatchingActorInputBoundary.this, s)
-        }
+    final override def onError(t: Throwable): Unit = {
+      ReactiveStreamsCompliance.requireNonNullException(t)
+      actor ! new OnError(this, t)
+    }
 
-        override def onComplete(): Unit = {
-          actor ! new OnComplete(BatchingActorInputBoundary.this)
-        }
+    final override def onSubscribe(s: Subscription): Unit = {
+      ReactiveStreamsCompliance.requireNonNullSubscription(s)
+      actor ! new OnSubscribe(this, s)
+    }
 
-        override def onNext(t: Any): Unit = {
-          ReactiveStreamsCompliance.requireNonNullElement(t)
-          actor ! new OnNext(BatchingActorInputBoundary.this, t)
-        }
-      })
+    final override def onComplete(): Unit =
+      actor ! new OnComplete(this)
+
+    final override def onNext(t: Any): Unit = {
+      ReactiveStreamsCompliance.requireNonNullElement(t)
+      next(t.asInstanceOf[AnyRef])
     }
 
+    final override def next(elem: AnyRef): Unit =
+      actor ! new OnNext(this, elem)
+
+    final override def batch(elements: Array[AnyRef]): Unit =
+      actor ! new OnNextBatch(this, elements)
+
     @InternalStableApi
     private def dequeue(): Any = {
       val elem = inputBuffer(nextInputElementCursor)
@@ -214,7 +236,7 @@ import org.reactivestreams.Subscription
     }
 
     @InternalStableApi
-    def onNext(elem: Any): Unit = {
+    def receiveNext(elem: Any): Unit = {
       if (!upstreamCompleted) {
         if (inputBufferElements == size) throw new 
IllegalStateException("Input buffer overrun")
         inputBuffer((nextInputElementCursor + inputBufferElements) & 
IndexMask) = elem.asInstanceOf[AnyRef]
@@ -223,7 +245,20 @@ import org.reactivestreams.Subscription
       }
     }
 
-    def onError(e: Throwable): Unit =
+    @InternalStableApi
+    def receiveNextBatch(elements: Array[AnyRef]): Unit = {
+      var i = 0
+      while (i < elements.length) {
+        receiveNext(elements(i))
+        i += 1
+      }
+    }
+
+    @InternalStableApi
+    def onNextBatch(elements: Array[AnyRef]): Unit =
+      receiveNextBatch(elements)
+
+    def receiveError(e: Throwable): Unit =
       if (!upstreamCompleted || downstreamCanceled.isEmpty) {
         upstreamCompleted = true
         clear()
@@ -236,16 +271,16 @@ import org.reactivestreams.Subscription
       if (!(upstreamCompleted || downstreamCanceled.isDefined) && (upstream ne 
null)) {
         upstream.cancel()
       }
-      if (!isClosed(out)) onError(e)
+      if (!isClosed(out)) receiveError(e)
     }
 
-    def onComplete(): Unit =
+    def receiveComplete(): Unit =
       if (!upstreamCompleted) {
         upstreamCompleted = true
         if (inputBufferElements == 0) complete(out)
       }
 
-    def onSubscribe(subscription: Subscription): Unit = {
+    def receiveSubscribe(subscription: Subscription): Unit = {
       ReactiveStreamsCompliance.requireNonNullSubscription(subscription)
       if (downstreamCanceled.isDefined) {
         upstreamCompleted = true
@@ -400,6 +435,7 @@ import org.reactivestreams.Subscription
     def getActor: ActorRef = this.actor
 
     private var subscriber: Subscriber[Any] = _
+    private var boundarySubscriber: BoundarySubscriber = _
     private var downstreamDemand: Long = 0L
     // This flag is only used if complete/fail is called externally since this 
op turns into a Finished one inside the
     // interpreter (i.e. inside this op this flag has no effects since if it 
is completed the op will not be invoked)
@@ -409,15 +445,78 @@ import org.reactivestreams.Subscription
     // when upstream failed before we got the exposed publisher
     private var upstreamCompleted: Boolean = false
 
+    private var firstBatchedElement: AnyRef = _
+    private var batchedElements: Array[AnyRef] = _
+    private var batchedElementIndex = 0
+
+    private final val InitialBatchSize = 16
+    private final val MaxBatchSize = 1024
+
     private def onNext(elem: Any): Unit = {
       downstreamDemand -= 1
-      tryOnNext(subscriber, elem)
+      if (boundarySubscriber eq null) tryOnNext(subscriber, elem)
+      else enqueueBatchElement(elem)
+    }
+
+    private def enqueueBatchElement(elem: Any): Unit = {
+      ReactiveStreamsCompliance.requireNonNullElement(elem)
+      val element = elem.asInstanceOf[AnyRef]
+      if (batchedElementIndex == MaxBatchSize) flushBatch()
+      val index = batchedElementIndex
+      if (index == 0) {
+        firstBatchedElement = element
+      } else {
+        var elements = batchedElements
+        if (elements eq null) {
+          elements = new Array[AnyRef](InitialBatchSize)
+          batchedElements = elements
+        } else if (index == elements.length) {
+          val newElements = new Array[AnyRef](index << 1)
+          System.arraycopy(elements, 0, newElements, 0, index)
+          elements = newElements
+          batchedElements = elements
+        }
+        if (index == 1) elements(0) = firstBatchedElement
+        elements(index) = element
+      }
+      batchedElementIndex = index + 1
+    }
+
+    def flushBatch(): Unit = {
+      val count = batchedElementIndex
+      if (count == 0) return
+
+      val boundary = boundarySubscriber
+      if (boundary eq null) {
+        clearBatch()
+        return
+      }
+
+      if (count == 1) boundary.next(firstBatchedElement)
+      else {
+        val elements = new Array[AnyRef](count)
+        System.arraycopy(batchedElements, 0, elements, 0, count)
+        boundary.batch(elements)
+      }
+
+      clearBatch()
+    }
+
+    private def clearBatch(): Unit = {
+      val count = batchedElementIndex
+      if (count != 0) {
+        firstBatchedElement = null
+        if (batchedElements ne null)
+          java.util.Arrays.fill(batchedElements, 0, count, null)
+        batchedElementIndex = 0
+      }
     }
 
     private def complete(): Unit = {
       // No need to complete if had already been cancelled, or we closed 
earlier
       if (!(upstreamCompleted || downstreamCompleted)) {
         upstreamCompleted = true
+        flushBatch()
         publisher.shutdown(None)
         if (subscriber ne null) tryOnComplete(subscriber)
       }
@@ -426,10 +525,11 @@ import org.reactivestreams.Subscription
     def fail(e: Throwable): Unit = {
       // No need to fail if had already been cancelled, or we closed earlier
       if (!(downstreamCompleted || upstreamCompleted)) {
+        flushBatch()
         upstreamCompleted = true
         publisher.shutdown(Some(e))
         if ((subscriber ne null) && !e.isInstanceOf[SpecViolation]) 
tryOnError(subscriber, e)
-      }
+      } else clearBatch()
     }
 
     setHandler(in, this)
@@ -460,6 +560,10 @@ import org.reactivestreams.Subscription
       publisher.takePendingSubscribers().foreach { sub =>
         if (subscriber eq null) {
           subscriber = sub
+          boundarySubscriber = sub match {
+            case boundary: BoundarySubscriber => boundary
+            case _                            => null
+          }
           val subscription = new Subscription with 
SubscriptionWithCancelException {
             override def request(elements: Long): Unit = actor ! new 
RequestMore(ActorOutputBoundary.this, elements)
             override def cancel(cause: Throwable): Unit = actor ! new 
Cancel(ActorOutputBoundary.this, cause)
@@ -487,8 +591,10 @@ import org.reactivestreams.Subscription
     }
 
     def cancel(cause: Throwable): Unit = {
+      clearBatch()
       downstreamCompletionCause = Some(cause)
       subscriber = null
+      boundarySubscriber = null
       publisher.shutdown(Some(new ActorPublisher.NormalShutdownException))
       cancel(in, cause)
     }
@@ -503,7 +609,10 @@ import org.reactivestreams.Subscription
  * INTERNAL API
  */
 @InternalApi private[pekko] object GraphInterpreterShell {
-  import ActorGraphInterpreter.BoundaryEvent
+  import ActorGraphInterpreter.{ ActorOutputBoundary, 
BatchingActorInputBoundary, BoundaryEvent }
+
+  private val EmptyOutputBoundaries = new Array[ActorOutputBoundary](0)
+  private val EmptyInputBoundaries = new Array[BatchingActorInputBoundary](0)
 
   /**
    * @param promise Will be completed upon processing the event, or failed if 
processing the event throws
@@ -522,6 +631,7 @@ import org.reactivestreams.Subscription
       if (!shell.waitingForShutdown) {
         shell.interpreter.runAsyncInput(logic, evt, promise, handler)
         if (eventLimit == 1 && shell.interpreter.isSuspended) {
+          shell.flushOutputs()
           shell.sendResume(true)
           0
         } else shell.runBatch(eventLimit - 1)
@@ -587,8 +697,8 @@ import org.reactivestreams.Subscription
   // TODO: really needed?
   private var subscribesPending = 0
 
-  private var inputs: List[BatchingActorInputBoundary] = Nil
-  private var outputs: List[ActorOutputBoundary] = Nil
+  private var inputs: Array[BatchingActorInputBoundary] = EmptyInputBoundaries
+  private var outputs: Array[ActorOutputBoundary] = EmptyOutputBoundaries
 
   /*
    * Limits the number of events processed by the interpreter before scheduling
@@ -618,21 +728,47 @@ import org.reactivestreams.Subscription
       eventLimit: Int): Int = {
     this.self = self
     this.enqueueToShortCircuit = enqueueToShortCircuit
+    val logicCount = logics.length
     var i = 0
-    while (i < logics.length) {
-      logics(i) match {
+    var inputBoundaryCount = 0
+    var outputBoundaryCount = 0
+    while (i < logicCount) {
+      val logic = logics(i)
+      logic match {
+        case _: BatchingActorInputBoundary => inputBoundaryCount += 1
+        case _: ActorOutputBoundary        => outputBoundaryCount += 1
+        case _                             =>
+      }
+      i += 1
+    }
+
+    val initializedInputs =
+      if (inputBoundaryCount == 0) EmptyInputBoundaries else new 
Array[BatchingActorInputBoundary](inputBoundaryCount)
+    val initializedOutputs =
+      if (outputBoundaryCount == 0) EmptyOutputBoundaries else new 
Array[ActorOutputBoundary](outputBoundaryCount)
+
+    i = 0
+    var inputIndex = 0
+    var outputIndex = 0
+    while (i < logicCount) {
+      val logic = logics(i)
+      logic match {
         case in: BatchingActorInputBoundary =>
           in.setActor(self)
           subscribesPending += 1
-          inputs ::= in
+          initializedInputs(inputIndex) = in
+          inputIndex += 1
         case out: ActorOutputBoundary =>
           out.setActor(self)
           out.subscribePending()
-          outputs ::= out
+          initializedOutputs(outputIndex) = out
+          outputIndex += 1
         case _ =>
       }
       i += 1
     }
+    inputs = initializedInputs
+    outputs = initializedOutputs
 
     interpreter.init(subMat)
     runBatch(eventLimit)
@@ -666,6 +802,7 @@ import org.reactivestreams.Subscription
     try {
       val usingShellLimit = shellEventLimit < actorEventLimit
       val remainingQuota = interpreter.execute(Math.min(actorEventLimit, 
shellEventLimit))
+      flushOutputs()
       if (interpreter.isCompleted) {
         // Cannot stop right away if not completely subscribed
         if (canShutDown) interpreterCompleted = true
@@ -684,6 +821,16 @@ import org.reactivestreams.Subscription
     }
   }
 
+  private def flushOutputs(): Unit = {
+    val outputBoundaries = outputs
+    val count = outputBoundaries.length
+    var i = 0
+    while (i < count) {
+      outputBoundaries(i).flushBatch()
+      i += 1
+    }
+  }
+
   /**
    * Attempts to abort execution, by first propagating the reason given until 
either
    *  - the interpreter successfully finishes
@@ -700,7 +847,13 @@ import org.reactivestreams.Subscription
     // This should handle termination while interpreter is running. If the 
upstream have been closed already this
     // call has no effect and therefore does the right thing: nothing.
     try {
-      inputs.foreach(_.onInternalError(reason))
+      val inputBoundaries = inputs
+      val inputsCount = inputBoundaries.length
+      var inputIndex = 0
+      while (inputIndex < inputsCount) {
+        inputBoundaries(inputIndex).onInternalError(reason)
+        inputIndex += 1
+      }
       interpreter.execute(abortLimit)
       interpreter.finish()
     } catch {
@@ -711,8 +864,20 @@ import org.reactivestreams.Subscription
       interpreterCompleted = true
       // Will only have an effect if the above call to the interpreter failed 
to emit a proper failure to the downstream
       // otherwise this will have no effect
-      outputs.foreach(_.fail(reason))
-      inputs.foreach(_.cancel(reason))
+      val outputBoundaries = outputs
+      val count = outputBoundaries.length
+      var i = 0
+      while (i < count) {
+        outputBoundaries(i).fail(reason)
+        i += 1
+      }
+      val inputBoundaries = inputs
+      val inputsCount = inputBoundaries.length
+      var inputIndex = 0
+      while (inputIndex < inputsCount) {
+        inputBoundaries(inputIndex).cancel(reason)
+        inputIndex += 1
+      }
     }
   }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to