-
Couldn't load subscription status.
- Fork 1.4k
Optimise zio.stream.ZChannel.mergeAllWith code
#9383
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should use just use *> here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does OutElem | Result work with Scala2? 🤔
393e543 to
efaf921
Compare
0753ef3 to
c753cf0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need these methods? Can't we use the apply method of the case classes directly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No we don't :)
| outgoing.take.flatten.foldCause( | ||
| cause => | ||
| ZIO.fiberIdWith { fiberId => | ||
| ZIO.suspendSucceed { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need to suspend here, ZIO.fiberIdWith already takes care of that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very minor: You can use Unsafe instead of Unsafe.unsafe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| _ <- scope.addFinalizer(outgoing.shutdown) | ||
| _ <- scope.addFinalizer(cancelers.shutdown) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.