From 96e73c0c0b55be3dd02b31622c4fffdee7096abd Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 20 Mar 2022 20:34:18 +0000 Subject: [PATCH 01/14] Use Performance API for high-precision time on JS --- .../unsafe/SchedulerCompanionPlatform.scala | 18 +++++++++++++++++- project/CI.scala | 12 ++++++++++-- .../cats/effect/unsafe/SchedulerSpec.scala | 13 +++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala b/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala index 53fdfd9025..1e7144e34a 100644 --- a/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala +++ b/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala @@ -17,7 +17,9 @@ package cats.effect.unsafe import scala.concurrent.duration._ +import scala.scalajs.js import scala.scalajs.js.timers +import scala.util.Try private[unsafe] abstract class SchedulerCompanionPlatform { this: Scheduler.type => private[this] val maxTimeout = Int.MaxValue.millis @@ -38,8 +40,22 @@ private[unsafe] abstract class SchedulerCompanionPlatform { this: Scheduler.type def nowMillis() = System.currentTimeMillis() def monotonicNanos() = System.nanoTime() - override def nowMicros(): Long = nowMillis() * 1000 + override def nowMicros(): Long = nowMicrosImpl() }, () => ()) + private[this] val nowMicrosImpl: () => Long = + Try { + val performance = js.Dynamic.global.performance.asInstanceOf[Performance] + // take it for a spin + require(!(performance.timeOrigin + performance.now()).isNaN) + + () => ((performance.timeOrigin + performance.now()) * 1000).toLong + } getOrElse { () => System.currentTimeMillis() * 1000 } +} + +@js.native +private trait Performance extends js.Any { + def timeOrigin: Double = js.native + def now(): Double = js.native } diff --git a/project/CI.scala b/project/CI.scala index eb4958d189..e36127ae16 100644 --- a/project/CI.scala +++ b/project/CI.scala @@ -69,7 +69,11 @@ object CI { command = "ciFirefox", rootProject = "rootJS", jsEnv = Some(JSEnv.Firefox), - testCommands = List("testOnly *tracing*", "testOnly *.ConsoleJSSpec"), + testCommands = List( + "testOnly *tracing*", + "testOnly *.ConsoleJSSpec", + "testOnly *.SchedulerSpec" + ), mimaReport = false, suffixCommands = List()) @@ -78,7 +82,11 @@ object CI { command = "ciChrome", rootProject = "rootJS", jsEnv = Some(JSEnv.Chrome), - testCommands = List("testOnly *tracing*", "testOnly *.ConsoleJSSpec"), + testCommands = List( + "testOnly *tracing*", + "testOnly *.ConsoleJSSpec", + "testOnly *.SchedulerSpec" + ), mimaReport = false, suffixCommands = List()) diff --git a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala index 878c26c85b..7c3ad3ba2f 100644 --- a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala +++ b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala @@ -29,6 +29,19 @@ class SchedulerSpec extends BaseSpec { "is using the correct max timeout" in real { IO.sleep(Int.MaxValue.millis).race(IO.sleep(100.millis)) mustEqual Right(()) } + "uses high-precision time" in real { + (IO.realTime <* IO.cede).product(IO.realTime).map { + case (x, y) => + val delta = y - x + 0.nanos < delta && delta < 1.milli + } + } + "correctly calculates real time" in real { + IO.realTime.product(IO(System.currentTimeMillis())).map { + case (realTime, currentTime) => + (realTime.toMillis - currentTime).abs <= 1 + } + } } } From 124de9f556a40020e60bb90ac02ff368d0c98d34 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 20 Mar 2022 21:06:29 +0000 Subject: [PATCH 02/14] Enable high-precision time for Firefox, don't cede --- build.sbt | 7 ++++++- .../src/test/scala/cats/effect/unsafe/SchedulerSpec.scala | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index a7004a50d6..04a0fb1cb3 100644 --- a/build.sbt +++ b/build.sbt @@ -19,7 +19,7 @@ import java.util.concurrent.TimeUnit import com.typesafe.tools.mima.core._ import org.openqa.selenium.chrome.ChromeOptions -import org.openqa.selenium.firefox.FirefoxOptions +import org.openqa.selenium.firefox.{FirefoxOptions, FirefoxProfile} import org.scalajs.jsenv.nodejs.NodeJSEnv import org.scalajs.jsenv.selenium.SeleniumJSEnv import sbtcrossproject.CrossProject @@ -209,7 +209,10 @@ ThisBuild / jsEnv := { useJSEnv.value match { case NodeJS => new NodeJSEnv(NodeJSEnv.Config().withSourceMap(true)) case Firefox => + val profile = new FirefoxProfile() + profile.setPreference("privacy.reduceTimerPrecision", false) val options = new FirefoxOptions() + options.setProfile(profile) options.setHeadless(true) new SeleniumJSEnv(options) case Chrome => @@ -666,3 +669,5 @@ lazy val benchmarks = project .enablePlugins(NoPublishPlugin, JmhPlugin) lazy val docs = project.in(file("site-docs")).dependsOn(core.jvm).enablePlugins(MdocPlugin) + +useJSEnv := JSEnv.Firefox diff --git a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala index 7c3ad3ba2f..307df6b4ef 100644 --- a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala +++ b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala @@ -30,7 +30,7 @@ class SchedulerSpec extends BaseSpec { IO.sleep(Int.MaxValue.millis).race(IO.sleep(100.millis)) mustEqual Right(()) } "uses high-precision time" in real { - (IO.realTime <* IO.cede).product(IO.realTime).map { + IO.realTime.product(IO.realTime).map { case (x, y) => val delta = y - x 0.nanos < delta && delta < 1.milli From fce914828e2690b4068bd566f21e8095241f2052 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 20 Mar 2022 21:16:35 +0000 Subject: [PATCH 03/14] So that's what session save does ... --- build.sbt | 2 -- 1 file changed, 2 deletions(-) diff --git a/build.sbt b/build.sbt index 04a0fb1cb3..71db5a5512 100644 --- a/build.sbt +++ b/build.sbt @@ -669,5 +669,3 @@ lazy val benchmarks = project .enablePlugins(NoPublishPlugin, JmhPlugin) lazy val docs = project.in(file("site-docs")).dependsOn(core.jvm).enablePlugins(MdocPlugin) - -useJSEnv := JSEnv.Firefox From 5327a5c7b4365bbcf0b263b76b71886ec5fb6fa9 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 20 Mar 2022 21:39:59 +0000 Subject: [PATCH 04/14] Matchers ftw --- .../js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala index 307df6b4ef..4971acb1db 100644 --- a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala +++ b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala @@ -33,13 +33,13 @@ class SchedulerSpec extends BaseSpec { IO.realTime.product(IO.realTime).map { case (x, y) => val delta = y - x - 0.nanos < delta && delta < 1.milli + (delta should be_>(0.nanos)) and (delta should be_<(1.nanos)) } } "correctly calculates real time" in real { IO.realTime.product(IO(System.currentTimeMillis())).map { case (realTime, currentTime) => - (realTime.toMillis - currentTime).abs <= 1 + (realTime.toMillis - currentTime) should be_<=(1L) } } } From 6962bd41790cee2646850f720c9f120db0a9cbd5 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 20 Mar 2022 21:49:47 +0000 Subject: [PATCH 05/14] Fix test --- tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala index 4971acb1db..4efdcf8d7a 100644 --- a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala +++ b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala @@ -33,7 +33,7 @@ class SchedulerSpec extends BaseSpec { IO.realTime.product(IO.realTime).map { case (x, y) => val delta = y - x - (delta should be_>(0.nanos)) and (delta should be_<(1.nanos)) + (delta should be_>(0.nanos)) and (delta should be_<(1.millis)) } } "correctly calculates real time" in real { From 4c303da99d012d12dce0c90a74569e425f166913 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 20 Mar 2022 21:59:17 +0000 Subject: [PATCH 06/14] Grammar --- .../js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala index 4efdcf8d7a..b841912c00 100644 --- a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala +++ b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala @@ -29,14 +29,14 @@ class SchedulerSpec extends BaseSpec { "is using the correct max timeout" in real { IO.sleep(Int.MaxValue.millis).race(IO.sleep(100.millis)) mustEqual Right(()) } - "uses high-precision time" in real { + "use high-precision time" in real { IO.realTime.product(IO.realTime).map { case (x, y) => val delta = y - x (delta should be_>(0.nanos)) and (delta should be_<(1.millis)) } } - "correctly calculates real time" in real { + "correctly calculate real time" in real { IO.realTime.product(IO(System.currentTimeMillis())).map { case (realTime, currentTime) => (realTime.toMillis - currentTime) should be_<=(1L) From e953c86e88c82574e963ebb39944857342d748db Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 20 Mar 2022 21:59:52 +0000 Subject: [PATCH 07/14] More grammar ... --- tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala index b841912c00..5a80de2407 100644 --- a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala +++ b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala @@ -26,7 +26,7 @@ class SchedulerSpec extends BaseSpec { // When the provided timeout in milliseconds overflows a signed 32-bit int, the implementation defaults to 1 millisecond IO.sleep(Long.MaxValue.nanos).race(IO.sleep(100.millis)) mustEqual Right(()) } - "is using the correct max timeout" in real { + "use the correct max timeout" in real { IO.sleep(Int.MaxValue.millis).race(IO.sleep(100.millis)) mustEqual Right(()) } "use high-precision time" in real { From 6ae23eed0422f3b9edeedc787103dc99930708fd Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 20 Mar 2022 22:23:26 +0000 Subject: [PATCH 08/14] Cede on Node.js --- .../scala/cats/effect/unsafe/SchedulerSpec.scala | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala index 5a80de2407..aa20eb371f 100644 --- a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala +++ b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala @@ -18,9 +18,15 @@ package cats.effect package unsafe import scala.concurrent.duration._ +import scala.scalajs.js +import scala.util.Try class SchedulerSpec extends BaseSpec { + private def isNodeJS = Try(js.Dynamic.global.process.release.name.asInstanceOf[String]) + .toOption + .exists(_ == "node") + "Default scheduler" should { "correctly handle very long sleeps" in real { // When the provided timeout in milliseconds overflows a signed 32-bit int, the implementation defaults to 1 millisecond @@ -30,7 +36,12 @@ class SchedulerSpec extends BaseSpec { IO.sleep(Int.MaxValue.millis).race(IO.sleep(100.millis)) mustEqual Right(()) } "use high-precision time" in real { - IO.realTime.product(IO.realTime).map { + val program = + if (isNodeJS) + IO.realTime.product(IO.cede *> IO.realTime) + else + IO.realTime.product(IO.realTime) + program.map { case (x, y) => val delta = y - x (delta should be_>(0.nanos)) and (delta should be_<(1.millis)) From 121a90a6163b15c3dfe5657f21cce3a4c6ecfa14 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 20 Mar 2022 23:08:26 +0000 Subject: [PATCH 09/14] Revert "Cede on Node.js" This reverts commit 6ae23eed0422f3b9edeedc787103dc99930708fd. --- .../scala/cats/effect/unsafe/SchedulerSpec.scala | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala index aa20eb371f..5a80de2407 100644 --- a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala +++ b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala @@ -18,15 +18,9 @@ package cats.effect package unsafe import scala.concurrent.duration._ -import scala.scalajs.js -import scala.util.Try class SchedulerSpec extends BaseSpec { - private def isNodeJS = Try(js.Dynamic.global.process.release.name.asInstanceOf[String]) - .toOption - .exists(_ == "node") - "Default scheduler" should { "correctly handle very long sleeps" in real { // When the provided timeout in milliseconds overflows a signed 32-bit int, the implementation defaults to 1 millisecond @@ -36,12 +30,7 @@ class SchedulerSpec extends BaseSpec { IO.sleep(Int.MaxValue.millis).race(IO.sleep(100.millis)) mustEqual Right(()) } "use high-precision time" in real { - val program = - if (isNodeJS) - IO.realTime.product(IO.cede *> IO.realTime) - else - IO.realTime.product(IO.realTime) - program.map { + IO.realTime.product(IO.realTime).map { case (x, y) => val delta = y - x (delta should be_>(0.nanos)) and (delta should be_<(1.millis)) From ca3692196df0e2d0aed151eddf735ae6b504dcc6 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 20 Mar 2022 23:19:56 +0000 Subject: [PATCH 10/14] `performance` not in global scope on Node 14 --- .../unsafe/SchedulerCompanionPlatform.scala | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala b/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala index 1e7144e34a..1a440bf863 100644 --- a/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala +++ b/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala @@ -44,14 +44,22 @@ private[unsafe] abstract class SchedulerCompanionPlatform { this: Scheduler.type }, () => ()) - private[this] val nowMicrosImpl: () => Long = - Try { - val performance = js.Dynamic.global.performance.asInstanceOf[Performance] + private[this] val nowMicrosImpl: () => Long = { + def test(performance: Performance): Try[Performance] = Try { // take it for a spin require(!(performance.timeOrigin + performance.now()).isNaN) + performance + } - () => ((performance.timeOrigin + performance.now()) * 1000).toLong + val browsers = Try(js.Dynamic.global.performance.asInstanceOf[Performance]).flatMap(test) + val nodeJS = + Try(js.Dynamic.global.require("perf_hooks").performance.asInstanceOf[Performance]) + .flatMap(test) + + browsers.orElse(nodeJS).map { performance => () => + ((performance.timeOrigin + performance.now()) * 1000).toLong } getOrElse { () => System.currentTimeMillis() * 1000 } + } } @js.native From cd744855153bc4b508b5e8cd6e29dafe4b2cd3bb Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 21 Mar 2022 00:28:09 +0000 Subject: [PATCH 11/14] Exceptions are expensive --- .../effect/unsafe/SchedulerCompanionPlatform.scala | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala b/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala index 1a440bf863..02e7f42a4c 100644 --- a/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala +++ b/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala @@ -45,16 +45,17 @@ private[unsafe] abstract class SchedulerCompanionPlatform { this: Scheduler.type () => ()) private[this] val nowMicrosImpl: () => Long = { - def test(performance: Performance): Try[Performance] = Try { + def test(performance: Performance) = { // take it for a spin - require(!(performance.timeOrigin + performance.now()).isNaN) - performance + !(performance.timeOrigin + performance.now()).isNaN } - val browsers = Try(js.Dynamic.global.performance.asInstanceOf[Performance]).flatMap(test) - val nodeJS = + def browsers = + Try(js.Dynamic.global.performance.asInstanceOf[Performance]).toOption.filter(test) + def nodeJS = Try(js.Dynamic.global.require("perf_hooks").performance.asInstanceOf[Performance]) - .flatMap(test) + .toOption + .filter(test) browsers.orElse(nodeJS).map { performance => () => ((performance.timeOrigin + performance.now()) * 1000).toLong From 2eecb63f89c7e7f7d1ea78b73badc810d2a67e38 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 21 Mar 2022 00:47:05 +0000 Subject: [PATCH 12/14] Bring back the cede --- tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala index 5a80de2407..81c2359f14 100644 --- a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala +++ b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala @@ -30,7 +30,7 @@ class SchedulerSpec extends BaseSpec { IO.sleep(Int.MaxValue.millis).race(IO.sleep(100.millis)) mustEqual Right(()) } "use high-precision time" in real { - IO.realTime.product(IO.realTime).map { + IO.realTime.product(IO.cede *> IO.realTime).map { case (x, y) => val delta = y - x (delta should be_>(0.nanos)) and (delta should be_<(1.millis)) From d07b9561a058bc47fe7b6e375150c22eda285cb6 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 21 Mar 2022 00:53:33 +0000 Subject: [PATCH 13/14] Check for undefined --- .../effect/unsafe/SchedulerCompanionPlatform.scala | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala b/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala index 02e7f42a4c..9ddf2342e2 100644 --- a/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala +++ b/core/js/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala @@ -51,11 +51,17 @@ private[unsafe] abstract class SchedulerCompanionPlatform { this: Scheduler.type } def browsers = - Try(js.Dynamic.global.performance.asInstanceOf[Performance]).toOption.filter(test) - def nodeJS = - Try(js.Dynamic.global.require("perf_hooks").performance.asInstanceOf[Performance]) + Try(js.Dynamic.global.performance.asInstanceOf[js.UndefOr[Performance]]) .toOption + .flatMap(_.toOption) .filter(test) + def nodeJS = + Try( + js.Dynamic + .global + .require("perf_hooks") + .performance + .asInstanceOf[js.UndefOr[Performance]]).toOption.flatMap(_.toOption).filter(test) browsers.orElse(nodeJS).map { performance => () => ((performance.timeOrigin + performance.now()) * 1000).toLong From 68c3547bc31bc238de208e07f83a919572abad67 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 21 Mar 2022 01:22:43 +0000 Subject: [PATCH 14/14] Write a smarter assertion --- .../js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala index 81c2359f14..25a8b79f14 100644 --- a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala +++ b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala @@ -31,9 +31,7 @@ class SchedulerSpec extends BaseSpec { } "use high-precision time" in real { IO.realTime.product(IO.cede *> IO.realTime).map { - case (x, y) => - val delta = y - x - (delta should be_>(0.nanos)) and (delta should be_<(1.millis)) + case (x, y) => (y - x).toMicros % 1000 should not(be_==(0L)) } } "correctly calculate real time" in real {