-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Broad spectrum performance optimizations for zio-test
#9404
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
base: series/2.x
Are you sure you want to change the base?
Broad spectrum performance optimizations for zio-test
#9404
Conversation
# Conflicts: # core/shared/src/main/scala/zio/ZIO.scala # project/MimaSettings.scala # test/shared/src/main/scala/zio/test/TestClock.scala # test/shared/src/main/scala/zio/test/TestReporters.scala # test/shared/src/main/scala/zio/test/TestServices.scala # test/shared/src/main/scala/zio/test/package.scala
# 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
# Conflicts: # test/shared/src/main/scala/zio/test/TestAspect.scala
| private def warningEmptyGen[R, E, A]( | ||
| f: (() => Boolean) => ZIO[R, E, A] | ||
| )(implicit trace: Trace): ZIO[R, E, A] = | ||
| ZIO.suspendSucceedUnsafe { implicit 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 noticed there isn't a ZIO.runtimeWith[A] method, though if you used that here loggers could correctly be overridden.
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.
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?
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.
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 |
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.
Should we use a def to avoid the field?
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 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]] |
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.
| case Left(_) => SortedSet.empty[Fiber.Runtime[Any, Any]] | |
| case _: Left[?] => SortedSet.empty[Fiber.Runtime[Any, Any]] |
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.
Moved this case at the bottom with a wildcard case _: instead
| } | ||
|
|
||
| private[this] def makeService(): ScheduledExecutorService = { | ||
| private[this] def makeService(): ScheduledThreadPoolExecutor = { |
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 the type annotation name change necessary?
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.
So the reason is because above we have this:
private[this] val service = makeService() // Type of `service` also changed from ScheduledExecutorService -> ScheduledThreadPoolExecutorand 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) |
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.
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.
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.
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
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 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) { |
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.
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?
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.
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:
zio/core/shared/src/main/scala/zio/internal/FiberRuntime.scala
Lines 444 to 464 in c519b61
| 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) | |
| } |
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.
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 => |
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.
Ah okay, that seems fine to me.
# Conflicts: # test/shared/src/main/scala/zio/test/ZIOSpecAbstract.scala
| 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"))), |
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.
pre-existing: I feel like these should render the failure, because IIRC in this case or the die it does not.
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 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 |
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.
Generally, should we be using val or def for these locally scoped implicits?
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.
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) |
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 think we could use println, which would be much more lightweight in this context.
|
@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. |
This PR optimizes
zio-testacross most of its components with the main focus on:TestClock's performance. Codebases that rely heavily on it should see a significant decrease in test execution time with these changes