From 8eb22f601152d3226d6728d32ede9bfb8ac1a1bf Mon Sep 17 00:00:00 2001 From: Brendan Maguire <1093243+brendanmaguire@users.noreply.github.com> Date: Mon, 4 Apr 2022 18:09:02 +0100 Subject: [PATCH 1/7] Add `GenTemporal#cachedRealTime` This method provides a nested effect which calculates time using a cached offset and `monotonic`. This is much faster than calling `realTime` repeatedly and is useful for some applications where precise time is not required --- .../cats/effect/kernel/GenTemporal.scala | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala index 843bd8c353..f605a519c5 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala @@ -21,7 +21,7 @@ import cats.data._ import cats.syntax.all._ import scala.concurrent.TimeoutException -import scala.concurrent.duration.FiniteDuration +import scala.concurrent.duration.{Duration, FiniteDuration} /** * A typeclass that encodes the notion of suspending fibers for a given duration. Analogous to @@ -136,6 +136,52 @@ trait GenTemporal[F[_], E] extends GenConcurrent[F, E] with Clock[F] { start(f.cancel) *> raiseError[A](ev(new TimeoutException(duration.toString))) } } + + /** + * Returns a nested effect which returns the time in a much faster way than + * `Clock[F]#realTime`. This is achieved by caching the real time when the outer effect is run + * and, when the inner effect is run, the offset is used in combination with + * `Clock[F]#monotonic` to give an approximation of the real time. + * + * This should only be used in situations where precise time does not need to be guaranteed. + * + * @param refreshPeriod + * The period of time after which the cached real time will be refreshed. Note that it will + * only be refreshed upon execution of the nested effect + * @see + * [[timeout]] for a variant which respects backpressure and does not leak fibers + */ + def cachedRealTime(refreshPeriod: Duration): F[F[FiniteDuration]] = { + val cacheValuesF = flatMap(realTime)(realTimeNow => + map(monotonic)(cacheRefreshTime => (cacheRefreshTime, realTimeNow - cacheRefreshTime))) + + // Take two measurements and keep the one with the minimum offset. This will no longer be + // required when `IO.unyielding` is merged (see #2633) + val minCacheValuesF = flatMap(cacheValuesF) { + case cacheValues1 @ (_, offset1) => + map(cacheValuesF) { + case cacheValues2 @ (_, offset2) if offset2 < offset1 => cacheValues2 + case _ => cacheValues1 + } + } + + val cacheValuesRefF = flatMap(minCacheValuesF)(ref) + + map(cacheValuesRefF) { cacheValuesRef => + flatMap(monotonic) { timeNow => + flatMap(cacheValuesRef.access) { + case ((cacheRefreshTime, offset), setCacheValues) => + if (timeNow >= cacheRefreshTime + refreshPeriod) + flatMap(minCacheValuesF) { + case cacheValues @ (cacheRefreshTime, offset) => + map(setCacheValues(cacheValues)) { _ => cacheRefreshTime + offset } + } + else + pure(timeNow + offset) + } + } + } + } } object GenTemporal { From 05fcc38fe456c3785c8e9419452747f7f249ce06 Mon Sep 17 00:00:00 2001 From: Brendan Maguire <1093243+brendanmaguire@users.noreply.github.com> Date: Tue, 5 Apr 2022 09:28:42 +0100 Subject: [PATCH 2/7] Update kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala Co-authored-by: Daniel Spiewak --- .../src/main/scala/cats/effect/kernel/GenTemporal.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala index f605a519c5..e34b147591 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala @@ -143,7 +143,10 @@ trait GenTemporal[F[_], E] extends GenConcurrent[F, E] with Clock[F] { * and, when the inner effect is run, the offset is used in combination with * `Clock[F]#monotonic` to give an approximation of the real time. * - * This should only be used in situations where precise time does not need to be guaranteed. + * This should generally be used in situations where precise "to the millisecond" alignment to the + * system real clock is not needed. In particular, if the system clock is updated (e.g. via an NTP sync), + * the inner effect will not observe that update until up to `refreshPeriod`. This is an acceptable + * tradeoff in most practical scenarios, particularly with frequent sequencing of the inner effect. * * @param refreshPeriod * The period of time after which the cached real time will be refreshed. Note that it will From cf66e2f84f684e604c36382c02885a25b9e4880b Mon Sep 17 00:00:00 2001 From: Brendan Maguire <1093243+brendanmaguire@users.noreply.github.com> Date: Tue, 5 Apr 2022 10:18:12 +0100 Subject: [PATCH 3/7] Update kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala Co-authored-by: Daniel Spiewak --- .../src/main/scala/cats/effect/kernel/GenTemporal.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala index e34b147591..b25a68cf2d 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala @@ -141,7 +141,9 @@ trait GenTemporal[F[_], E] extends GenConcurrent[F, E] with Clock[F] { * Returns a nested effect which returns the time in a much faster way than * `Clock[F]#realTime`. This is achieved by caching the real time when the outer effect is run * and, when the inner effect is run, the offset is used in combination with - * `Clock[F]#monotonic` to give an approximation of the real time. + * `Clock[F]#monotonic` to give an approximation of the real time. The practical benefit of this is a reduction + * in the number of syscalls, since `realTime` will only be sequenced once per `refreshTime` window, and it + * tends to be (on most platforms) multiple orders of magnitude slower than `monotonic`. * * This should generally be used in situations where precise "to the millisecond" alignment to the * system real clock is not needed. In particular, if the system clock is updated (e.g. via an NTP sync), From 7f2c360f6e6b73375f0269ca5c5dcd7a27a7bed9 Mon Sep 17 00:00:00 2001 From: Brendan Maguire <1093243+brendanmaguire@users.noreply.github.com> Date: Tue, 5 Apr 2022 10:27:45 +0100 Subject: [PATCH 4/7] PR Changes --- .../cats/effect/kernel/GenTemporal.scala | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala index b25a68cf2d..10216f2470 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala @@ -141,45 +141,46 @@ trait GenTemporal[F[_], E] extends GenConcurrent[F, E] with Clock[F] { * Returns a nested effect which returns the time in a much faster way than * `Clock[F]#realTime`. This is achieved by caching the real time when the outer effect is run * and, when the inner effect is run, the offset is used in combination with - * `Clock[F]#monotonic` to give an approximation of the real time. The practical benefit of this is a reduction - * in the number of syscalls, since `realTime` will only be sequenced once per `refreshTime` window, and it - * tends to be (on most platforms) multiple orders of magnitude slower than `monotonic`. + * `Clock[F]#monotonic` to give an approximation of the real time. The practical benefit of + * this is a reduction in the number of syscalls, since `realTime` will only be sequenced once + * per `refreshTime` window, and it tends to be (on most platforms) multiple orders of + * magnitude slower than `monotonic`. * - * This should generally be used in situations where precise "to the millisecond" alignment to the - * system real clock is not needed. In particular, if the system clock is updated (e.g. via an NTP sync), - * the inner effect will not observe that update until up to `refreshPeriod`. This is an acceptable - * tradeoff in most practical scenarios, particularly with frequent sequencing of the inner effect. + * This should generally be used in situations where precise "to the millisecond" alignment to + * the system real clock is not needed. In particular, if the system clock is updated (e.g. + * via an NTP sync), the inner effect will not observe that update until up to `ttl`. This is + * an acceptable tradeoff in most practical scenarios, particularly with frequent sequencing + * of the inner effect. * - * @param refreshPeriod + * @param ttl * The period of time after which the cached real time will be refreshed. Note that it will * only be refreshed upon execution of the nested effect * @see * [[timeout]] for a variant which respects backpressure and does not leak fibers */ - def cachedRealTime(refreshPeriod: Duration): F[F[FiniteDuration]] = { - val cacheValuesF = flatMap(realTime)(realTimeNow => - map(monotonic)(cacheRefreshTime => (cacheRefreshTime, realTimeNow - cacheRefreshTime))) + def cachedRealTime(ttl: Duration): F[F[FiniteDuration]] = { + implicit val self = this + + val cacheValuesF = for { + realTimeNow <- realTime + cacheRefreshTime <- monotonic + } yield (cacheRefreshTime, realTimeNow - cacheRefreshTime) // Take two measurements and keep the one with the minimum offset. This will no longer be // required when `IO.unyielding` is merged (see #2633) - val minCacheValuesF = flatMap(cacheValuesF) { - case cacheValues1 @ (_, offset1) => - map(cacheValuesF) { - case cacheValues2 @ (_, offset2) if offset2 < offset1 => cacheValues2 - case _ => cacheValues1 - } + val minCacheValuesF = (cacheValuesF, cacheValuesF) mapN { + case (cacheValues1 @ (_, offset1), cacheValues2 @ (_, offset2)) => + if (offset1 < offset2) cacheValues1 else cacheValues2 } - val cacheValuesRefF = flatMap(minCacheValuesF)(ref) - - map(cacheValuesRefF) { cacheValuesRef => - flatMap(monotonic) { timeNow => - flatMap(cacheValuesRef.access) { - case ((cacheRefreshTime, offset), setCacheValues) => - if (timeNow >= cacheRefreshTime + refreshPeriod) - flatMap(minCacheValuesF) { + minCacheValuesF.flatMap(ref).map { cacheValuesRef => + monotonic.flatMap { timeNow => + cacheValuesRef.get.flatMap { + case (cacheRefreshTime, offset) => + if (timeNow >= cacheRefreshTime + ttl) + minCacheValuesF.flatMap { case cacheValues @ (cacheRefreshTime, offset) => - map(setCacheValues(cacheValues)) { _ => cacheRefreshTime + offset } + cacheValuesRef.set(cacheValues).map(_ => cacheRefreshTime + offset) } else pure(timeNow + offset) From 47f5e0aba8f686d8de0f5c7c880f77e31e005caf Mon Sep 17 00:00:00 2001 From: Brendan Maguire <1093243+brendanmaguire@users.noreply.github.com> Date: Tue, 5 Apr 2022 10:30:26 +0100 Subject: [PATCH 5/7] Update kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala --- .../shared/src/main/scala/cats/effect/kernel/GenTemporal.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala index 10216f2470..9fbadf30bd 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala @@ -155,8 +155,6 @@ trait GenTemporal[F[_], E] extends GenConcurrent[F, E] with Clock[F] { * @param ttl * The period of time after which the cached real time will be refreshed. Note that it will * only be refreshed upon execution of the nested effect - * @see - * [[timeout]] for a variant which respects backpressure and does not leak fibers */ def cachedRealTime(ttl: Duration): F[F[FiniteDuration]] = { implicit val self = this From c15037f9a462437d1723982bf1578bc52b7aaeb3 Mon Sep 17 00:00:00 2001 From: Brendan Maguire <1093243+brendanmaguire@users.noreply.github.com> Date: Wed, 6 Apr 2022 07:40:30 +0100 Subject: [PATCH 6/7] Update kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala Co-authored-by: Daniel Spiewak --- .../shared/src/main/scala/cats/effect/kernel/GenTemporal.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala index 9fbadf30bd..ae7e1da12a 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala @@ -143,7 +143,7 @@ trait GenTemporal[F[_], E] extends GenConcurrent[F, E] with Clock[F] { * and, when the inner effect is run, the offset is used in combination with * `Clock[F]#monotonic` to give an approximation of the real time. The practical benefit of * this is a reduction in the number of syscalls, since `realTime` will only be sequenced once - * per `refreshTime` window, and it tends to be (on most platforms) multiple orders of + * per `ttl` window, and it tends to be (on most platforms) multiple orders of * magnitude slower than `monotonic`. * * This should generally be used in situations where precise "to the millisecond" alignment to From 9608fb92e1dafb87e01fcd7fb9d8ce60c2347881 Mon Sep 17 00:00:00 2001 From: Brendan Maguire <1093243+brendanmaguire@users.noreply.github.com> Date: Wed, 6 Apr 2022 07:45:26 +0100 Subject: [PATCH 7/7] PR Change --- .../main/scala/cats/effect/kernel/GenTemporal.scala | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala index ae7e1da12a..87b42b7ace 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala @@ -143,8 +143,8 @@ trait GenTemporal[F[_], E] extends GenConcurrent[F, E] with Clock[F] { * and, when the inner effect is run, the offset is used in combination with * `Clock[F]#monotonic` to give an approximation of the real time. The practical benefit of * this is a reduction in the number of syscalls, since `realTime` will only be sequenced once - * per `ttl` window, and it tends to be (on most platforms) multiple orders of - * magnitude slower than `monotonic`. + * per `ttl` window, and it tends to be (on most platforms) multiple orders of magnitude + * slower than `monotonic`. * * This should generally be used in situations where precise "to the millisecond" alignment to * the system real clock is not needed. In particular, if the system clock is updated (e.g. @@ -159,10 +159,9 @@ trait GenTemporal[F[_], E] extends GenConcurrent[F, E] with Clock[F] { def cachedRealTime(ttl: Duration): F[F[FiniteDuration]] = { implicit val self = this - val cacheValuesF = for { - realTimeNow <- realTime - cacheRefreshTime <- monotonic - } yield (cacheRefreshTime, realTimeNow - cacheRefreshTime) + val cacheValuesF = (realTime, monotonic) mapN { + case (realTimeNow, cacheRefreshTime) => (cacheRefreshTime, realTimeNow - cacheRefreshTime) + } // Take two measurements and keep the one with the minimum offset. This will no longer be // required when `IO.unyielding` is merged (see #2633)