Optimise zio.stream.ZChannel.mergeAllWith code#9383
Conversation
kyri-petrou
left a comment
There was a problem hiding this comment.
Some minor comments, otherwise LGTM!
| n0 = n.toLong | ||
| bufferSize0 = bufferSize | ||
| mergeStrategy0 = mergeStrategy | ||
| outgoing = Queue.unsafe.bounded[ZIO[Env, Either[OutErr, OutDone], OutElem]](bufferSize0, fiberId)(Unsafe.unsafe) | ||
| cancelers = Queue.unsafe.unbounded[Promise[Nothing, Unit]](fiberId)(Unsafe.unsafe) | ||
| lastDone = Ref.unsafe.make[Option[OutDone]](None)(Unsafe.unsafe) | ||
| errorSignal = Promise.unsafe.make[Nothing, Unit](fiberId)(Unsafe.unsafe) | ||
| permits = Semaphore.unsafe.make(n0)(Unsafe.unsafe) |
There was a problem hiding this comment.
What do you think about not using a for-comprehension and use local vals for these and avoid all the unnecessary maps?
There was a problem hiding this comment.
I'm not sure to understand your suggestion 🤔
AFAIR, using v = ... in a for-comprehension doesn't add any additional map call:
for {
a <- ZIO.succeed("a")
b = "b"
c = "c"
d <- ZIO.succeed("d")
} yield (a, b, c, d)should produce (something close to):
ZIO
.succeed("a")
.flatMap { a =>
val b = "b"
val c = "c"
ZIO.succeed("d").map(d => (a, b, c, d))
}no? 🤔
There was a problem hiding this comment.
Unfortunately, no :( It's Scala version-specific, but in Scala 3 it will produce something along these lines:
ZIO
.succeed("a")
.map { a =>
val b = "b"
val c = "c"
(a, b, c)
}.flatMap { case (a, b, c) =>
ZIO.succeed("d").map(d => (a, b, c, d))
}I think in Scala 2 it's very similar or even worse
There was a problem hiding this comment.
Is your val suggestion looking something like this:
(SingleProducerAsyncInput.make[InErr, InElem, InDone] <*> ZIO.fiberId).flatMap { case (input, fiberId) =>
val incoming = ZChannel.fromInput(input)
val n0 = n.toLong
val bufferSize0 = bufferSize
val mergeStrategy0 = mergeStrategy
val outgoing =
Queue.unsafe.bounded[ZIO[Env, Either[OutErr, OutDone], OutElem]](bufferSize0, fiberId)(Unsafe.unsafe)
val cancelers = Queue.unsafe.unbounded[Promise[Nothing, Unit]](fiberId)(Unsafe.unsafe)
val lastDone = Ref.unsafe.make[Option[OutDone]](None)(Unsafe.unsafe)
val errorSignal = Promise.unsafe.make[Nothing, Unit](fiberId)(Unsafe.unsafe)
val permits = Semaphore.unsafe.make(n0)(Unsafe.unsafe)
for {
_ <- scope.addFinalizer(outgoing.shutdown)
_ <- scope.addFinalizer(cancelers.shutdown)
...
}? 🤔
There was a problem hiding this comment.
@kyri-petrou Made an attempt here: #9387 (Merge in the current PR)
LMKWYT
| for { | ||
| _ <- permits | ||
| .withPermit(latch.succeed(()) *> raceIOs) | ||
| .interruptible | ||
| .forkIn(childScope) | ||
| _ <- latch.await | ||
| } yield () |
There was a problem hiding this comment.
Maybe we should use just use *> here
zio.stream.ZChannel.mergeAllWith codezio.stream.ZChannel.mergeAllWith code
| val errorSignal = Promise.unsafe.make[Nothing, Unit](fiberId)(Unsafe.unsafe) | ||
| val permits = Semaphore.unsafe.make(n0)(Unsafe.unsafe) | ||
|
|
||
| type Pull[A] = ZIO[Env, Either[OutErr, OutDone], A] |
There was a problem hiding this comment.
As I was writing this, I felt like the toPull stuff should pull chunks instead of single elements. The cost is a little bit of housekeeping for error handling. The chunk is already somewhere in memory for the by-element pull, so it makes no difference for correctness, however it makes a huge difference for performance.
There was a problem hiding this comment.
I'm a bit busy right now. I'll explore your idea later. Thanks for your review! 🙂
Edit:
Took 2s to have a look if we were able to have a Chunk[A] here but I didn't see how because toPullInAlt returns a ZIO[Env, Either[OutErr, OutDone], OutElem] and not ZIO[Env, Either[OutErr, OutDone], Chunk[OutElem]] 🤔
Could you make give it a try on top of my PR, please?
There was a problem hiding this comment.
I had a look, I think to have any real effect we will need chunk-aware channels and update the pipelines:
new ZPipeline(
ZChannel
.identity[Nothing, Chunk[In], Any]
.concatMap(ZChannel.writeChunk(_))
.mapOutZIOPar(n, bufferSize)(f)
.mapOut(Chunk.single)
)to something like:
new ZPipeline(
ZChannel
.identity[Nothing, Chunk[In], Any]
.mapOutChunkZIOPar(n, bufferSize)(f)
)with the caveat that it breaks chunk structure.
final def mapOutChunkZIOPar[Env1 <: Env, OutErr1 >: OutErr, InElem0, OutElem0, OutElem2](n: Int, bufferSize: Int = 16)(
f: OutElem0 => ZIO[Env1, OutErr1, OutElem2]
)(implicit
ev0: Chunk[InElem0] <:< InElem,
ev1: OutElem <:< Chunk[OutElem0],
trace: Trace
): ZChannel[Env1, InErr, Chunk[InElem0], InDone, OutErr1, Chunk[OutElem2], OutDone] = ???There was a problem hiding this comment.
Let's do this in another PR when this one is done. It's already a complex one.
| val bufferSize0 = bufferSize | ||
| val mergeStrategy0 = mergeStrategy | ||
| val outgoing = | ||
| Queue.unsafe.bounded[ZIO[Env, Either[OutErr, OutDone], OutElem]](bufferSize0, fiberId)(Unsafe.unsafe) |
There was a problem hiding this comment.
by the by, I think this could be
| Queue.unsafe.bounded[ZIO[Env, Either[OutErr, OutDone], OutElem]](bufferSize0, fiberId)(Unsafe.unsafe) | |
| Queue.unsafe.bounded[Exit[Either[OutErr, OutDone], OutElem]](bufferSize0, fiberId)(Unsafe.unsafe) |
then consumer can avoid the flatten and just pattern match:
outgoing.take.map {
case s: Success[OutElem] => ZChannel.write(s.value) *> write
case f: Failure[Either[OutErr, OutDone]] => ...
}
There was a problem hiding this comment.
Thanks for spotting this! Change done :)
We could go even further and use a specialized data structure which could only be something like:
sealed trait Result[V, E, F]
object Result {
final case class Value[V](v: V) extends Result[V, Nothing, Nothing]
final case class Error[E](e: E) extends Result[Nothing, E, Nothing]
final case class Fatal[E](e: E) extends Result[Nothing, Nothing, E]
}That'd be more lightweight than Exit[Either[OutErr, OutDone], OutElem].
I'll give it a try
Edit:
Made a POC with this Result idea: #9445
Please let me know what you think :)
There was a problem hiding this comment.
Nice one 👌 Another iteration on this that was exploited previously is to avoid wrapping the OutElem values, turning the queue elements into OutElem | Result.
There was a problem hiding this comment.
Does OutElem | Result work with Scala2? 🤔
393e543 to
efaf921
Compare
0753ef3 to
c753cf0
Compare
kyri-petrou
left a comment
There was a problem hiding this comment.
Sorry it took so long to review this, I review ZStream-related changes line-by-line which takes quite a bit of time.
Looks good to me overall, just some minor comments below. Main comment would be to use AtomicReference instead of Ref since we're initializing it with a null value
| .map(new SingleProducerAsyncInput(_)) | ||
|
|
||
| object unsafe { | ||
| def make[Err, Elem, Done](fiberId: FiberId)(implicit trace: Trace): SingleProducerAsyncInput[Err, Elem, Done] = { |
There was a problem hiding this comment.
2 minor comments here:
- To keep things consistent, add an implicit
unsafe: Unsaferequirement for this method. This way you won't need to pass it explicitly below either - Change the implementation of
SingleProducerAsyncInput.maketoZIO.fiberIdWith(unsafe.make(_)(trace, Unsafe))
There was a problem hiding this comment.
Done.
I removed the implicit trace: Trace from the unsafe.make function to make it consistent with Promise.unsafe.make, etc.
| def value(v: OutElem): Result = Value(v) | ||
| def error(e: OutErr): Result = Error(e) | ||
| def done(d: OutDone): Result = Done(d) | ||
| def fatal(cause: Cause[Nothing]): Result = Fatal(cause) |
There was a problem hiding this comment.
Do we need these methods? Can't we use the apply method of the case classes directly?
| outgoing.take.flatten.foldCause( | ||
| cause => | ||
| ZIO.fiberIdWith { fiberId => | ||
| ZIO.suspendSucceed { |
There was a problem hiding this comment.
You don't need to suspend here, ZIO.fiberIdWith already takes care of that
| val mergeStrategy0 = mergeStrategy | ||
| val outgoing = Queue.unsafe.bounded[Result](bufferSize0, fiberId)(Unsafe.unsafe) | ||
| val cancelers = Queue.unsafe.unbounded[Promise[Nothing, Unit]](fiberId)(Unsafe.unsafe) | ||
| val lastDone = Ref.unsafe.make[OutDone](null.asInstanceOf[OutDone])(Unsafe.unsafe) |
There was a problem hiding this comment.
I'm really reluctant on passing null to anything that can end up in a ZIO effect containing a null value. I think it's better to use AtomicReference(null) directly
There was a problem hiding this comment.
Done. Please have a look. I made the choice to wrap the usage of the AtomicReference in ZIO.succeed and not in Exit.succeed to avoid executing the wrapped code at the wrong moment in time. I can be wrong tho 🤔
There was a problem hiding this comment.
Might be unnecessary indeed but better be safe than sorry on that front
| val cancelers = Queue.unsafe.unbounded[Promise[Nothing, Unit]](fiberId)(Unsafe.unsafe) | ||
| val lastDone = Ref.unsafe.make[OutDone](null.asInstanceOf[OutDone])(Unsafe.unsafe) | ||
| val errorSignal = Promise.unsafe.make[Nothing, Unit](fiberId)(Unsafe.unsafe) | ||
| val permits = Semaphore.unsafe.make(n0)(Unsafe.unsafe) |
There was a problem hiding this comment.
Very minor: You can use Unsafe instead of Unsafe.unsafe
| _ <- scope.addFinalizer(outgoing.shutdown) | ||
| _ <- scope.addFinalizer(cancelers.shutdown) |
There was a problem hiding this comment.
@kyri-petrou I wonder if we shouldn't wrap these 2 calls in an ZIO.uninterruptible 🤔
114f471 to
eadc4ea
Compare
|
Interestingly, the |
* Review: Remove as many `.map` calls as possible * Update streams/shared/src/main/scala/zio/stream/ZChannel.scala * Remove Option in `lastDone` `Ref` * `SingleProducerAsyncInput.unsafe.make` * `fiberIdWith` * success first * fmt * Clean
2593190 to
d4f7909
Compare
Found why. Mistake from me. Should be fixed now 🙂 |
similar change to those made in this PR #9383
similar change to those made in this PR #9383
similar change to those made in this PR #9383
similar change to those made in this PR #9383
similar change to those made in this PR #9383
similar change to those made in this PR #9383
No description provided.