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

Skip to content

Conversation

@kyri-petrou
Copy link
Contributor

@kyri-petrou kyri-petrou commented Dec 13, 2024

This PR optimizes zio-test across most of its components with the main focus on:

  • Reducing the number of fibers created when invoking tests by using the scheduled executor directly for logging messages instead of forking fibers
  • Optimizes TestClock's performance. Codebases that rely heavily on it should see a significant decrease in test execution time with these changes
  • Reduce allocations and unnecessary flatmaps / effect evaluations wherever possible

# Conflicts:
#	test-sbt/jvm/src/main/scala/zio/test/sbt/ZTestRunnerJVM.scala
#	test-sbt/shared/src/main/scala/zio/test/sbt/ZTestEventHandlerSbt.scala
#	test/shared/src/main/scala/zio/test/ZIOSpecAbstract.scala
# Conflicts:
#	test/shared/src/main/scala/zio/test/TestRandom.scala
#	test/shared/src/main/scala/zio/test/ZIOSpecAbstract.scala
#	test/shared/src/main/scala/zio/test/package.scala
# Conflicts:
#	test-junit-engine/src/main/scala/zio/test/junit/ZIOTestClassRunner.scala
#	test/shared/src/main/scala/zio/test/Spec.scala
#	test/shared/src/main/scala/zio/test/TestSuccess.scala
#	test/shared/src/main/scala/zio/test/package.scala
@kyri-petrou kyri-petrou marked this pull request as ready for review November 26, 2025 07:37
private def warningEmptyGen[R, E, A](
f: (() => Boolean) => ZIO[R, E, A]
)(implicit trace: Trace): ZIO[R, E, A] =
ZIO.suspendSucceedUnsafe { implicit unsafe =>
Copy link
Collaborator

Choose a reason for hiding this comment

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

I noticed there isn't a ZIO.runtimeWith[A] method, though if you used that here loggers could correctly be overridden.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So the thing here is that we really don't want them to be overridden, because the majority of users disable loggers in tests. But this warning must be printed out to the user.

This however begs the question: Why use a logger and not print directly with println? Frankly I can't think of a good enough answer to that, so perhaps we should change it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah okay, that seems fine to me.

val tag: Tag[Annotations] = Tag[Annotations]

final case class Test(ref: Ref.Atomic[TestAnnotationMap]) extends Annotations {
private implicit val unsafe0: Unsafe = Unsafe
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we use a def to avoid the field?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I ended up inlining it where it was needed, it was only used in 3 places anyways.

get(TestAnnotation.fibers).flatMap {
case Left(_) => ZIO.succeed(SortedSet.empty[Fiber.Runtime[Any, Any]])
get(TestAnnotation.fibers).map {
case Left(_) => SortedSet.empty[Fiber.Runtime[Any, Any]]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
case Left(_) => SortedSet.empty[Fiber.Runtime[Any, Any]]
case _: Left[?] => SortedSet.empty[Fiber.Runtime[Any, Any]]

Copy link
Contributor Author

@kyri-petrou kyri-petrou Nov 29, 2025

Choose a reason for hiding this comment

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

Moved this case at the bottom with a wildcard case _: instead

}

private[this] def makeService(): ScheduledExecutorService = {
private[this] def makeService(): ScheduledThreadPoolExecutor = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is the type annotation name change necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So the reason is because above we have this:

private[this] val service = makeService() // Type of `service` also changed from ScheduledExecutorService -> ScheduledThreadPoolExecutor

and then we have usages of service.schedule. I wanted to avoid the interface dispatch call so I changed the type to the concrete class instead. Since this is a private val it won't cause any issues

val f = ZIO
.logWarning(suspendedWarning)
.zipRight(suspendedWarningState.set(SuspendedWarningData.done))
val cancel = Clock.globalScheduler.schedule(() => Runtime.default.unsafe.run(f), 5.seconds)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we add support for running synchronously until we reach an async boundary? I suppose it should be the same scheduler, but it seems slightly incorrect to use the default runtime here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

f in this case is a synchronous effect (logging a message and setting the state of the Ref.

This brings me back to my previous comment whether we do need to use the logger or whether we can simply use println

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we could use println, which would be much more lightweight in this context.

.onExecutor(Runtime.defaultExecutor)
_ <- (child.interrupt *> fiber.interrupt).forkDaemon.onExecutor(Runtime.defaultExecutor)
} yield ()
_ <- ZIO.whenZIODiscard(child.hasChildrenAlive) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you explain to me when this can happen? Won't the exitValue be non-null (since we already awaited promise completion, and thus never have children alive?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Won't the exitValue be non-null

Not necessarily. The promise will be completed when the effect has evaluated and produced a result, not when the Fiber completes. It is possible for the effect to be evaluated but for the fiber not to have completed yet in the case that there are children that need to be interrupted. See here:

val interruption = interruptAllChildren()
if (interruption eq null) {
if (inbox.isEmpty) {
finalExit = exit
if (supervisor ne Supervisor.none) supervisor.onEnd(finalExit, self)(Unsafe)
// No more messages to process, so we will allow the fiber to end life:
self.setExitValue(exit)
} else {
// There are messages, possibly added by the final op executed by
// the fiber. To be safe, we should execute those now before we
// allow the fiber to end life:
tell(FiberMessage.Resume(exit))
}
effect = null
} else {
effect = interruption.flatMap(_ => exit)(id.location)
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, that makes sense.

private def warningEmptyGen[R, E, A](
f: (() => Boolean) => ZIO[R, E, A]
)(implicit trace: Trace): ZIO[R, E, A] =
ZIO.suspendSucceedUnsafe { implicit unsafe =>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah okay, that seems fine to me.

# Conflicts:
#	test/shared/src/main/scala/zio/test/ZIOSpecAbstract.scala
hearnadam
hearnadam previously approved these changes Dec 24, 2025
else ZIO.fail(TestFailure.die(new RuntimeException("did not fail as expected"))),
_ => ZIO.fail(TestFailure.die(new RuntimeException("did not fail as expected")))
if (assertion(failure)) TestSuccess.succeedEmptyExit
else Exit.fail(TestFailure.die(new RuntimeException("did not fail as expected"))),
Copy link
Collaborator

Choose a reason for hiding this comment

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

pre-existing: I feel like these should render the failure, because IIRC in this case or the die it does not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to figure out a way to render the failure in a meaningful way and couldn't, so instead I decided to re-raise the failure and document this behaviour. I think that should be better UX-wise than what we currently have.

case _ => ZIO.fail(())
}
private def freeze: IO[Unit, collection.Map[FiberId, Fiber.Status.Suspended]] = {
implicit val trace: Trace = Trace.empty
Copy link
Collaborator

Choose a reason for hiding this comment

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

Generally, should we be using val or def for these locally scoped implicits?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah good question. Ideally we'd want to inline them, but since that's not possible in a way that can be cross-compiled I think the next-best thing is to use a def and rely on the JVM to inline the method body.

Although I think we should probably do this in a separate PR as there will be a lot of places that we'd need to do that

val f = ZIO
.logWarning(suspendedWarning)
.zipRight(suspendedWarningState.set(SuspendedWarningData.done))
val cancel = Clock.globalScheduler.schedule(() => Runtime.default.unsafe.run(f), 5.seconds)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we could use println, which would be much more lightweight in this context.

@kyri-petrou
Copy link
Contributor Author

@hearnadam I added non-effectful logging of warning messages (see 0f32e0f).

I kept more-or-less the original format of the log message (without adding timestamps etc) just because I didn't want to change the behaviour too much.

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.

2 participants