diff --git a/build.sbt b/build.sbt index 080003e10d..74c4a7abbb 100644 --- a/build.sbt +++ b/build.sbt @@ -20,7 +20,7 @@ import java.util.concurrent.TimeUnit import com.typesafe.tools.mima.core._ import com.github.sbt.git.SbtGit.GitKeys._ 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 @@ -224,7 +224,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 => 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..9ddf2342e2 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,37 @@ 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 = { + def test(performance: Performance) = { + // take it for a spin + !(performance.timeOrigin + performance.now()).isNaN + } + + def browsers = + 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 + } 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 31c692da45..a8b578468b 100644 --- a/project/CI.scala +++ b/project/CI.scala @@ -69,8 +69,12 @@ object CI { command = "ciFirefox", rootProject = "rootJS", jsEnv = Some(JSEnv.Firefox), - testCommands = - List("testOnly *tracing*", "testOnly *.RandomSpec", "testOnly *.ConsoleJSSpec"), + testCommands = List( + "testOnly *tracing*", + "testOnly *.ConsoleJSSpec", + "testOnly *.RandomSpec", + "testOnly *.SchedulerSpec" + ), mimaReport = false, suffixCommands = List()) @@ -79,8 +83,12 @@ object CI { command = "ciChrome", rootProject = "rootJS", jsEnv = Some(JSEnv.Chrome), - testCommands = - List("testOnly *tracing*", "testOnly *.RandomSpec", "testOnly *.ConsoleJSSpec"), + testCommands = List( + "testOnly *tracing*", + "testOnly *.ConsoleJSSpec", + "testOnly *.RandomSpec", + "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..25a8b79f14 100644 --- a/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala +++ b/tests/js/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala @@ -26,9 +26,20 @@ 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 { + IO.realTime.product(IO.cede *> IO.realTime).map { + case (x, y) => (y - x).toMicros % 1000 should not(be_==(0L)) + } + } + "correctly calculate real time" in real { + IO.realTime.product(IO(System.currentTimeMillis())).map { + case (realTime, currentTime) => + (realTime.toMillis - currentTime) should be_<=(1L) + } + } } }