Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@guizmaii
Copy link
Member

@guizmaii guizmaii commented Dec 9, 2024

No description provided.

@guizmaii guizmaii requested a review from kyri-petrou December 9, 2024 01:05
@guizmaii guizmaii self-assigned this Dec 9, 2024
Copy link
Contributor

@kyri-petrou kyri-petrou left a 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!

Comment on lines 1947 to 1954
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)
Copy link
Contributor

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?

Copy link
Member Author

@guizmaii guizmaii Dec 10, 2024

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? 🤔

Copy link
Contributor

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

Copy link
Member Author

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)
          ...
}

? 🤔

Copy link
Member Author

@guizmaii guizmaii Dec 10, 2024

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

Comment on lines 1986 to 1992
for {
_ <- permits
.withPermit(latch.succeed(()) *> raceIOs)
.interruptible
.forkIn(childScope)
_ <- latch.await
} yield ()
Copy link
Contributor

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@guizmaii guizmaii changed the title Simplify zio.stream.ZChannel.mergeAllWith code Optimise zio.stream.ZChannel.mergeAllWith code Dec 11, 2024
@guizmaii guizmaii requested a review from kyri-petrou December 12, 2024 14:18
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]
Copy link
Member

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.

Copy link
Member Author

@guizmaii guizmaii Dec 31, 2024

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?

Copy link
Member

@regiskuckaertz regiskuckaertz Dec 31, 2024

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] = ???

Copy link
Member Author

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)
Copy link
Member

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

Suggested change
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]] => ...
}

Copy link
Member Author

@guizmaii guizmaii Dec 31, 2024

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 :)

Copy link
Member

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.

Copy link
Member Author

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? 🤔

@guizmaii guizmaii force-pushed the simplify_ZChannel_mergeAllWith branch from 393e543 to efaf921 Compare December 31, 2024 01:35
@guizmaii guizmaii force-pushed the simplify_ZChannel_mergeAllWith branch from 0753ef3 to c753cf0 Compare December 31, 2024 21:23
Copy link
Contributor

@kyri-petrou kyri-petrou left a 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] = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 minor comments here:

  1. To keep things consistent, add an implicit unsafe: Unsafe requirement for this method. This way you won't need to pass it explicitly below either
  2. Change the implementation of SingleProducerAsyncInput.make to ZIO.fiberIdWith(unsafe.make(_)(trace, Unsafe))

Copy link
Member Author

@guizmaii guizmaii Jan 5, 2025

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.

Comment on lines 1949 to 1952
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)
Copy link
Contributor

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?

Copy link
Member Author

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 {
Copy link
Contributor

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

Copy link
Member Author

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)
Copy link
Contributor

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

Copy link
Member Author

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 🤔

Copy link
Contributor

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)
Copy link
Contributor

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 2031 to 2029
_ <- scope.addFinalizer(outgoing.shutdown)
_ <- scope.addFinalizer(cancelers.shutdown)
Copy link
Member Author

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 🤔

@guizmaii guizmaii force-pushed the simplify_ZChannel_mergeAllWith branch 2 times, most recently from 114f471 to eadc4ea Compare January 7, 2025 23:04
@guizmaii
Copy link
Member Author

guizmaii commented Jan 7, 2025

Interestingly, the AtomicReference change breaks the tests 🤔
I'll need to have a look when I have some time

@guizmaii guizmaii force-pushed the simplify_ZChannel_mergeAllWith branch from 2593190 to d4f7909 Compare January 11, 2025 01:18
@guizmaii
Copy link
Member Author

Interestingly, the AtomicReference change breaks the tests 🤔

Found why. Mistake from me. Should be fixed now 🙂

@kyri-petrou kyri-petrou merged commit ec61369 into series/2.x Jan 18, 2025
18 checks passed
@kyri-petrou kyri-petrou deleted the simplify_ZChannel_mergeAllWith branch January 18, 2025 15:40
guizmaii added a commit that referenced this pull request Feb 6, 2025
similar change to those made in this PR #9383
guizmaii added a commit that referenced this pull request Feb 6, 2025
similar change to those made in this PR #9383
guizmaii added a commit that referenced this pull request Feb 6, 2025
similar change to those made in this PR #9383
guizmaii added a commit that referenced this pull request Feb 6, 2025
similar change to those made in this PR #9383
guizmaii added a commit that referenced this pull request Feb 6, 2025
similar change to those made in this PR #9383
guizmaii added a commit that referenced this pull request Feb 6, 2025
similar change to those made in this PR #9383
guizmaii added a commit that referenced this pull request Feb 7, 2025
similar change to those made in this PR #9383
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants