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

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
42d566e
Optimize Promise
hearnadam Feb 10, 2025
a95ae72
missing import
hearnadam Feb 10, 2025
9972107
Merge branch 'series/2.x' into optimize-promise-pr
hearnadam Mar 5, 2025
a2b54e9
fix merge/address feedback
hearnadam Mar 5, 2025
6859183
Invoke waiters in order
hearnadam Mar 5, 2025
4dc3766
format
hearnadam Mar 5, 2025
0842795
fix version
hearnadam Mar 5, 2025
f3f7a94
fix PromiseSpec on 212
hearnadam Mar 5, 2025
78f1631
fix PromiseBenchmarks on 212
hearnadam Mar 5, 2025
e56d509
fix jvmopts
hearnadam Mar 6, 2025
f62ab66
optimize Single/Zero waiter
hearnadam Mar 6, 2025
201d774
mima
hearnadam Mar 6, 2025
1f98962
format
hearnadam Mar 6, 2025
511d573
attempt to improve test
hearnadam Mar 6, 2025
e028bef
Merge branch 'series/2.x' into optimize-promise-pr
hearnadam Mar 6, 2025
ee30a74
update test
hearnadam Mar 7, 2025
349202d
fix: complete
hearnadam Mar 9, 2025
071bfbb
PromiseBench: 16 -> 8 waiters
hearnadam Mar 10, 2025
fad1bf8
bring back linked list
hearnadam Mar 10, 2025
aa48507
formatting
hearnadam Mar 10, 2025
9265c1b
remove unused imports
hearnadam Mar 10, 2025
a4fe187
Use Unsafe directly
hearnadam Mar 10, 2025
e6cdde3
fix scala3
hearnadam Mar 10, 2025
7e2dc3d
Merge branch 'series/2.x' into optimize-promise-pr
hearnadam Mar 10, 2025
b3d30fd
Merge branch 'series/2.x' into optimize-promise-pr
hearnadam Mar 30, 2025
4689adf
fix formatting settings.json
hearnadam Mar 30, 2025
63bfeb9
update benches
hearnadam Mar 14, 2025
4f05b1e
Revert "fix formatting settings.json"
hearnadam Mar 31, 2025
e70a5a3
fix scala 3 formatting
hearnadam Mar 31, 2025
a34e5d8
updates
hearnadam Apr 5, 2025
c854562
fmt
hearnadam Apr 5, 2025
73dc413
cleanup
hearnadam Apr 5, 2025
4ddbb30
protected size
hearnadam Apr 5, 2025
7eb086e
Revert "protected size"
hearnadam Apr 5, 2025
870076c
Merge branch 'series/2.x' into optimize-promise-pr
hearnadam Apr 26, 2025
c818f4c
fix merge
hearnadam Apr 26, 2025
9b906aa
fix merge
hearnadam May 10, 2025
613b7c0
Merge branch 'series/2.x' into optimize-promise-pr
kyri-petrou May 14, 2025
207bd27
address in person review
hearnadam May 14, 2025
dd855c3
format
hearnadam May 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
],
"files.watcherExclude": {
"**/target": true
}
}
}
9 changes: 6 additions & 3 deletions benchmarks/src/main/scala/zio/BenchmarkUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ object BenchmarkUtil extends Runtime[Any] { self =>
Unsafe.unsafe(implicit unsafe => rt.unsafe.run(zio).getOrThrowFiberFailure())
}

override val unsafe = super.unsafe
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I noticed some benchmarks allocate new unsafe apis.


private object NoFiberRootsRuntime extends Runtime[Any] {
val environment = Runtime.default.environment
val fiberRefs = Runtime.default.fiberRefs
val runtimeFlags = RuntimeFlags(RuntimeFlag.CooperativeYielding, RuntimeFlag.Interruption)
override val unsafe = super.unsafe
val environment = Runtime.default.environment
val fiberRefs = Runtime.default.fiberRefs
val runtimeFlags = RuntimeFlags(RuntimeFlag.CooperativeYielding, RuntimeFlag.Interruption)
}
}
103 changes: 90 additions & 13 deletions benchmarks/src/main/scala/zio/PromiseBenchmarks.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package zio

import cats.effect.kernel.Deferred
import cats.syntax.traverse._
import cats.instances.list._
import cats.effect.unsafe.implicits.global
import cats.effect.{IO => CIO}
import cats.syntax.foldable._
import org.openjdk.jmh.annotations.{Scope => JScope, _}
import zio.BenchmarkUtil._

Expand All @@ -11,35 +14,109 @@ import java.util.concurrent.TimeUnit
@State(JScope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
@Measurement(iterations = 5, timeUnit = TimeUnit.SECONDS, time = 3)
@Warmup(iterations = 5, timeUnit = TimeUnit.SECONDS, time = 3)
@Measurement(iterations = 5, timeUnit = TimeUnit.SECONDS, time = 10)
@Warmup(iterations = 5, timeUnit = TimeUnit.SECONDS, time = 10)
@Fork(value = 3)
class PromiseBenchmarks {

val size = 100000
val n = 100000
val waiters: Int = 8

val ints: List[Int] = List.range(0, size)
def createWaitersZIO(promise: Promise[Nothing, Unit]): ZIO[Any, Nothing, Seq[Fiber[Nothing, Unit]]] =
ZIO.foreach(Vector.range(0, waiters))(_ => promise.await.forkDaemon)

def createWaitersCats(promise: Deferred[CIO, Unit]) =
List.range(0, waiters).traverse(_ => promise.get.start)

@Benchmark
def zioPromiseAwaitDone(): Unit = {
def zioPromiseDoneAwait(): Unit = {

val io = ZIO.foreachDiscard(ints) { _ =>
Promise.make[Nothing, Unit].flatMap { promise =>
promise.succeed(()) *> promise.await
}
}
val io =
Promise
.make[Nothing, Unit]
.flatMap { promise =>
promise.done(Exit.unit) *> promise.await
}
.repeatN(n)

unsafeRun(io)
}

@Benchmark
def catsPromiseAwaitDone(): Unit = {
def catsPromiseDoneAwait(): Unit = {

val io = catsForeachDiscard(List.range(1, size)) { _ =>
val io =
Deferred[CIO, Unit].flatMap { promise =>
promise.complete(()).flatMap(_ => promise.get)
}.replicateA_(n)

io.unsafeRunSync()
}

@Benchmark
def zioPromiseMultiAwaitDone(): Unit = {
val io = Promise
.make[Nothing, Unit]
.flatMap { promise =>
for {
fibers <- createWaitersZIO(promise)
_ <- promise.done(Exit.unit)
_ <- ZIO.foreachDiscard(fibers)(_.await)
} yield ()
}
}
.repeatN(1023)

unsafeRun(io)
}

@Benchmark
def catsPromiseMultiAwaitDone(): Unit = {
val io =
Deferred[CIO, Unit].flatMap { promise =>
for {
fibers <- createWaitersCats(promise)
_ <- promise.complete(())
_ <- fibers.traverse_(_.join)
} yield ()
}.replicateA_(1023)

io.unsafeRunSync()
}

@Benchmark
def zioPromiseMultiAwaitMultiDone(): Unit = {
def createCompleters(promise: Promise[Nothing, Unit], latch: Promise[Nothing, Unit]) =
ZIO.foreach(Vector.range(0, waiters))(_ => (latch.await *> promise.done(Exit.unit)).forkDaemon)

val io = {
for {
latch <- Promise.make[Nothing, Unit]
promise <- Promise.make[Nothing, Unit]
waiters <- createWaitersZIO(promise)
fibers <- createCompleters(promise, latch)
_ <- latch.done(Exit.unit)
result <- promise.await
} yield result
}.repeatN(1023)

unsafeRun(io)
}

@Benchmark
def catsPromiseMultiAwaitMultiDone(): Unit = {
def createCompleters(promise: Deferred[CIO, Unit], latch: Deferred[CIO, Unit]) =
List.range(0, waiters).traverse(_ => (latch.get *> promise.complete(())).start)

val io = {
for {
latch <- Deferred[CIO, Unit]
promise <- Deferred[CIO, Unit]
waiters <- createWaitersCats(promise)
fibers <- createCompleters(promise, latch)
_ <- latch.complete(())
result <- promise.get
} yield result
}.replicateA_(1023)

io.unsafeRunSync()
}
Expand Down
34 changes: 34 additions & 0 deletions core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,40 @@ object FiberRuntimeSpec extends ZIOBaseSpec {
}
}
),
suite("async")(
test("async callback after interruption is ignored") {
ZIO.suspendSucceed {
val executed = Ref.unsafe.make(0)
val cb = Ref.unsafe.make[Option[ZIO[Any, Nothing, Unit] => Unit]](None)
val latch = Promise.unsafe.make[Nothing, Unit](FiberId.None)
val async = ZIO.async[Any, Nothing, Unit] { k =>
cb.unsafe.set(Some(k))
latch.unsafe.done(Exit.unit)
}
val increment = executed.update(_ + 1)
for {
fiber <- async.fork
_ <- latch.await
exit <- fiber.interrupt
callback <- cb.get.some
state1 <- fiber.poll
_ <- ZIO.succeed(callback(increment))
state2 <- fiber.poll
executedBefore <- executed.get
_ <- ZIO.succeed(callback(increment))
state3 <- fiber.poll
executedAfter <- executed.get
} yield assertTrue(
state1 == Some(exit),
state2 == Some(exit),
state3 == Some(exit),
executedBefore == 0,
executedAfter == 0,
exit.isInterrupted
)
}
} @@ TestAspect.nonFlaky(10)
),
suite("runtime metrics")(
test("Failures are counted once for the fiber that caused them and exits are not") {
val nullErrors = ZIO.foreachParDiscard(1 to 2)(_ => ZIO.attempt(throw new NullPointerException))
Expand Down
73 changes: 72 additions & 1 deletion core-tests/shared/src/test/scala/zio/PromiseSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ object PromiseSpec extends ZIOBaseSpec {

import ZIOTag._

private def empty[E, A]: Promise.internal.Pending[E, A] =
Promise.internal.State.empty[E, A].asInstanceOf[Promise.internal.Pending[E, A]]

val n = 10000

def spec: Spec[Any, TestFailure[Any]] = suite("PromiseSpec")(
test("complete a promise using succeed") {
for {
Expand Down Expand Up @@ -120,6 +125,72 @@ object PromiseSpec extends ZIOBaseSpec {
_ <- p.fail("failure")
d <- p.isDone
} yield assert(d)(isTrue)
} @@ zioTag(errors)
} @@ zioTag(errors),
test("waiter stack safety") {
for {
p <- Promise.make[Nothing, Unit]
fibers <- ZIO.foreach(1 to n)(_ => p.await.forkDaemon)
_ <- p.complete(Exit.unit)
_ <- ZIO.foreach(fibers)(_.await)
} yield assertCompletes
},
suite("State")(
suite("add")(
test("stack safety") {
(0 to 100000).foldLeft(empty[Nothing, Unit])((acc, _) => acc.add(_ => ()))
assertCompletes
}
),
suite("complete")(
test("one") {
var increment = 0
val state = empty[Nothing, Unit].add(_ => increment += 1)
state.complete(ZIO.unit)
assert(increment)(equalTo(1))
},
test("multiple") {
var increment = 0
val state = (0 until n).foldLeft(empty[Nothing, Unit])((acc, _) => acc.add(_ => increment += 1))
state.complete(ZIO.unit)
assert(increment)(equalTo(n))
}
),
suite("remove")(
test("one") {
var increment = 0
val cb = (_: IO[Nothing, Unit]) => increment += 1
val state = empty[Nothing, Unit].add(cb)
val removed = state.remove(cb)
removed.complete(ZIO.unit)
assert(removed)(equalTo(empty[Nothing, Unit])) &&
assert(increment)(equalTo(0))
},
test("multiple") {
var fired = 0
val cb = (_: IO[Nothing, Unit]) => ()
val toRemove = (_: IO[Nothing, Unit]) => fired += 1
val state =
(0 until n).foldLeft(empty[Nothing, Unit])((acc, i) => if (i < 5) acc.add(cb) else acc.add(toRemove))
val removed = state.remove(toRemove)
removed.complete(ZIO.unit)
assert(removed.size)(equalTo(5)) &&
assert(fired)(equalTo(0))
}
),
suite("complete")(
test("one") {
var completed = 0
val state = empty[Nothing, Unit].add(_ => completed += 1)
state.complete(ZIO.unit)
assert(completed)(equalTo(1))
},
test("multiple") {
var completed = List.empty[Int]
val state = (0 until n).foldLeft(empty[Nothing, Unit])((acc, i) => acc.add(_ => completed = i :: completed))
state.complete(ZIO.unit)
assert(completed)(equalTo(List.range(0, n)))
}
)
)
)
}
26 changes: 10 additions & 16 deletions core/shared/src/main/scala-2/zio/ZIOCompanionVersionSpecific.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import zio.ZIO.Async
import zio.stacktracer.TracingImplicits.disableAutoTrace

import java.io.IOException
import java.util.concurrent.atomic.AtomicReference

private[zio] trait ZIOCompanionVersionSpecific {

Expand Down Expand Up @@ -44,22 +45,15 @@ private[zio] trait ZIOCompanionVersionSpecific {
blockingOn: => FiberId = FiberId.None
)(implicit trace: Trace): ZIO[R, E, A] =
ZIO.suspendSucceed {
val cancelerRef = new java.util.concurrent.atomic.AtomicReference[URIO[R, Any]](ZIO.unit)

ZIO
.Async[R, E, A](
trace,
{ k =>
val result = register(k(_))

result match {
case Left(canceler) => cancelerRef.set(canceler); null.asInstanceOf[ZIO[R, E, A]]
case Right(done) => done
}
},
() => blockingOn
)
.onInterrupt(cancelerRef.get())
val state = new AtomicReference[URIO[R, Any]](Exit.unit) with ((ZIO[R, E, A] => Unit) => ZIO[R, E, A]) {
def apply(k: ZIO[R, E, A] => Unit): ZIO[R, E, A] =
register(k(_)) match {
case Left(canceler) => set(canceler); null.asInstanceOf[ZIO[R, E, A]]
case Right(done) => done
}
}

ZIO.Async[R, E, A](trace, state, () => blockingOn).onInterrupt(state.get())
}

/**
Expand Down
26 changes: 10 additions & 16 deletions core/shared/src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import zio.ZIO.Async
import zio.stacktracer.TracingImplicits.disableAutoTrace

import java.io.IOException
import java.util.concurrent.atomic.AtomicReference
import scala.annotation.targetName

private[zio] transparent trait ZIOCompanionVersionSpecific {
Expand Down Expand Up @@ -51,22 +52,15 @@ private[zio] transparent trait ZIOCompanionVersionSpecific {
blockingOn: => FiberId = FiberId.None
)(implicit trace: Trace): ZIO[R, E, A] =
ZIO.suspendSucceed {
val cancelerRef = new java.util.concurrent.atomic.AtomicReference[URIO[R, Any]](ZIO.unit)

ZIO
.Async[R, E, A](
trace,
{ k =>
val result = register(using Unsafe)(k(_))

result match {
case Left(canceler) => cancelerRef.set(canceler); null.asInstanceOf[ZIO[R, E, A]]
case Right(done) => done
}
},
() => blockingOn
)
.onInterrupt(cancelerRef.get())
val state = new AtomicReference[URIO[R, Any]](Exit.unit) with ((ZIO[R, E, A] => Unit) => ZIO[R, E, A]) {
def apply(k: ZIO[R, E, A] => Unit): ZIO[R, E, A] =
register(using Unsafe)(k(_)) match {
case Left(canceler) => set(canceler); null.asInstanceOf[ZIO[R, E, A]]
case Right(done) => done
}
}

ZIO.Async[R, E, A](trace, state, () => blockingOn).onInterrupt(state.get())
}

/**
Expand Down
Loading