diff --git a/.cirrus.yml b/.cirrus.yml index 96800de902..5cc584dc60 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -10,16 +10,3 @@ jvm_highcore_task: script: sbt '++ 2.13' testsJVM/test ioAppTestsJVM/test - name: JVM high-core-count 3 script: sbt '++ 3' testsJVM/test ioAppTestsJVM/test - -native_highcore_task: - only_if: $CIRRUS_TAG != '' || $CIRRUS_PR != '' - required_pr_labels: Cirrus Native - container: - dockerfile: .cirrus/Dockerfile - cpu: 8 - memory: 16G - matrix: - - name: Native high-core-count 2.13 - script: sbt '++ 2.13' testsNative/test ioAppTestsNative/test - - name: Native high-core-count 3 - script: sbt '++ 3' testsNative/test ioAppTestsNative/test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c2ba68c48..5a9075c122 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,8 @@ jobs: java: temurin@11 - scala: 3.3.5 java: temurin@21 + - scala: 3.3.5 + java: graalvm@21 - scala: 2.12.20 java: temurin@11 - scala: 2.12.20 @@ -323,11 +325,6 @@ jobs: shell: bash run: example/test-native.sh ${{ matrix.scala }} - - name: Test GraalVM Native Image - if: (matrix.scala == '2.13.16' || matrix.scala == '3.3.5') && matrix.java == 'graalvm@21' && matrix.os == 'ubuntu-latest' - shell: bash - run: sbt '++ ${{ matrix.scala }}' graalVMExample/nativeImage graalVMExample/nativeImageRun - - name: Scalafix tests if: matrix.scala == '2.13.16' && matrix.ci == 'ciJVM' && matrix.os == 'ubuntu-latest' shell: bash @@ -670,5 +667,5 @@ jobs: - name: Submit Dependencies uses: scalacenter/sbt-dependency-submission@v2 with: - modules-ignore: cats-effect-benchmarks_3 cats-effect-benchmarks_2.12 cats-effect-benchmarks_2.13 cats-effect_3 cats-effect_2.12 cats-effect_2.13 cats-effect-example_sjs1_3 cats-effect-example_sjs1_2.12 cats-effect-example_sjs1_2.13 rootjs_3 rootjs_2.12 rootjs_2.13 ioapptestsnative_3 ioapptestsnative_2.12 ioapptestsnative_2.13 cats-effect-graalvm-example_3 cats-effect-graalvm-example_2.12 cats-effect-graalvm-example_2.13 cats-effect-tests_sjs1_3 cats-effect-tests_sjs1_2.12 cats-effect-tests_sjs1_2.13 rootjvm_3 rootjvm_2.12 rootjvm_2.13 rootnative_3 rootnative_2.12 rootnative_2.13 cats-effect-example_native0.5_3 cats-effect-example_native0.5_2.12 cats-effect-example_native0.5_2.13 cats-effect-example_3 cats-effect-example_2.12 cats-effect-example_2.13 cats-effect-tests_3 cats-effect-tests_2.12 cats-effect-tests_2.13 ioapptestsjvm_3 ioapptestsjvm_2.12 ioapptestsjvm_2.13 ioapptestsjs_3 ioapptestsjs_2.12 ioapptestsjs_2.13 cats-effect-tests_native0.5_3 cats-effect-tests_native0.5_2.12 cats-effect-tests_native0.5_2.13 + modules-ignore: cats-effect-benchmarks_3 cats-effect-benchmarks_2.12 cats-effect-benchmarks_2.13 cats-effect_3 cats-effect_2.12 cats-effect_2.13 cats-effect-example_sjs1_3 cats-effect-example_sjs1_2.12 cats-effect-example_sjs1_2.13 rootjs_3 rootjs_2.12 rootjs_2.13 ioapptestsnative_3 ioapptestsnative_2.12 ioapptestsnative_2.13 cats-effect-graalvm-example_3 cats-effect-graalvm-example_2.12 cats-effect-graalvm-example_2.13 cats-effect-tests_sjs1_3 cats-effect-tests_sjs1_2.12 cats-effect-tests_sjs1_2.13 rootjvm_3 rootjvm_2.12 rootjvm_2.13 rootnative_3 rootnative_2.12 rootnative_2.13 cats-effect-example_native0.4_3 cats-effect-example_native0.4_2.12 cats-effect-example_native0.4_2.13 cats-effect-example_3 cats-effect-example_2.12 cats-effect-example_2.13 cats-effect-tests_3 cats-effect-tests_2.12 cats-effect-tests_2.13 ioapptestsjvm_3 ioapptestsjvm_2.12 ioapptestsjvm_2.13 ioapptestsjs_3 ioapptestsjs_2.12 ioapptestsjs_2.13 cats-effect-tests_native0.4_3 cats-effect-tests_native0.4_2.12 cats-effect-tests_native0.4_2.13 configs-ignore: test scala-tool scala-doc-tool test-internal diff --git a/build.sbt b/build.sbt index d57bc23aec..4009779050 100644 --- a/build.sbt +++ b/build.sbt @@ -15,6 +15,7 @@ */ import java.io.File +import java.util.concurrent.TimeUnit import com.typesafe.tools.mima.core._ import com.github.sbt.git.SbtGit.GitKeys._ @@ -22,15 +23,13 @@ import org.openqa.selenium.chrome.ChromeOptions import org.openqa.selenium.firefox.{FirefoxOptions, FirefoxProfile} import org.scalajs.jsenv.nodejs.NodeJSEnv import org.scalajs.jsenv.selenium.SeleniumJSEnv +import org.scalajs.linker.interface.OutputPatterns import sbtcrossproject.CrossProject -import scala.scalanative.build._ import JSEnv._ -lazy val inCI = Option(System.getenv("CI")).contains("true") - // sbt-git workarounds -ThisBuild / useConsoleForROGit := !inCI +ThisBuild / useConsoleForROGit := !Option(System.getenv("CI")).contains("true") ThisBuild / git.gitUncommittedChanges := { if ((ThisBuild / githubIsWorkflowBuild).value) { @@ -43,11 +42,12 @@ ThisBuild / git.gitUncommittedChanges := { } } -ThisBuild / tlBaseVersion := "3.7" +ThisBuild / tlBaseVersion := "3.6" ThisBuild / tlUntaggedAreSnapshots := false ThisBuild / organization := "org.typelevel" ThisBuild / organizationName := "Typelevel" +ThisBuild / tlSonatypeUseLegacyHost := false ThisBuild / startYear := Some(2020) @@ -204,12 +204,6 @@ ThisBuild / githubWorkflowBuild := Seq("JVM", "JS", "Native").map { platform => name = Some("Test Example Native App Using Binary"), cond = Some(s"matrix.ci == 'ciNative' && matrix.os == '$PrimaryOS'") ), - WorkflowStep.Sbt( - List("graalVMExample/nativeImage", "graalVMExample/nativeImageRun"), - name = Some("Test GraalVM Native Image"), - cond = Some( - s"(matrix.scala == '$Scala213' || matrix.scala == '$Scala3') && matrix.java == '${GraalVM.render}' && matrix.os == '$PrimaryOS'") - ), WorkflowStep.Run( List("cd scalafix", "sbt test"), name = Some("Scalafix tests"), @@ -233,7 +227,7 @@ ThisBuild / githubWorkflowBuildMatrixExclusions := { val scalaJavaFilters = for { scala <- (ThisBuild / githubWorkflowScalaVersions).value.filterNot(Set(Scala213)) java <- (ThisBuild / githubWorkflowJavaVersions).value.filterNot(Set(OldGuardJava)) - if !(scala == Scala3 && (java == LatestJava || java == GraalVM)) + if !(scala == Scala3 && java == LatestJava) } yield MatrixExclude(Map("scala" -> scala, "java" -> java.render)) val armFilters = @@ -295,6 +289,14 @@ lazy val useJSEnv = settingKey[JSEnv]("Use Node.js or a headless browser for running Scala.js tests") Global / useJSEnv := NodeJS +lazy val nodeJSWasmEnv = new NodeJSEnv( + NodeJSEnv + .Config() + .withArgs(List("--experimental-wasm-exnref")) + .withEnv(Map("WASM_MODE" -> "true")) + .withSourceMap(true) +) + ThisBuild / jsEnv := { useJSEnv.value match { case NodeJS => new NodeJSEnv(NodeJSEnv.Config().withSourceMap(true)) @@ -309,6 +311,8 @@ ThisBuild / jsEnv := { val options = new ChromeOptions() options.setHeadless(true) new SeleniumJSEnv(options) + case WASM => + nodeJSWasmEnv } } @@ -325,52 +329,28 @@ ThisBuild / autoAPIMappings := true ThisBuild / Test / testOptions += Tests.Argument("+l") -val CatsVersion = "2.13.0" -val CatsMtlVersion = "1.5.0" -val ScalaCheckVersion = "1.18.1" -val CoopVersion = "1.3.0" -val MUnitVersion = "1.1.0" -val MUnitScalaCheckVersion = "1.1.0" -val DisciplineMUnitVersion = "2.0.0" +val CatsVersion = "2.11.0" +val CatsMtlVersion = "1.3.1" +val ScalaCheckVersion = "1.17.1" +val CoopVersion = "1.2.0" +val MUnitVersion = "1.0.0-M11" +val MUnitScalaCheckVersion = "1.0.0-M11" +val DisciplineMUnitVersion = "2.0.0-M3" val MacrotaskExecutorVersion = "1.1.1" -Global / tlCommandAliases ++= Map( - CI.JVM.commandAlias, - CI.Native.commandAlias, - CI.JS.commandAlias, - CI.Firefox.commandAlias, - CI.Chrome.commandAlias -) - -Global / tlCommandAliases ++= Map( - "ci" -> CI.AllCIs.flatMap(_.commands) -) - -Global / tlCommandAliases ++= Map( - "release" -> List("tlRelease") -) +tlReplaceCommandAlias("ci", CI.AllCIs.map(_.toString).mkString) +addCommandAlias("release", "tlRelease") -Global / tlCommandAliases ++= Map( - "prePR" -> List( - "root/clean", - "+root/headerCreate", - "root/scalafixAll", - "scalafmtSbt", - "+root/scalafmtAll" - ) -) +addCommandAlias(CI.JVM.command, CI.JVM.toString) +addCommandAlias(CI.Native.command, CI.Native.toString) +addCommandAlias(CI.JS.command, CI.JS.toString) +addCommandAlias(CI.Firefox.command, CI.Firefox.toString) +addCommandAlias(CI.Chrome.command, CI.Chrome.toString) -lazy val nativeTestSettings = Seq( - nativeConfig ~= { c => // TODO: remove this when it seems to work - c.withSourceLevelDebuggingConfig(_.enableAll) // enable generation of debug information - .withOptimize(false) // disable Scala Native optimizer - .withMode(Mode.debug) // compile using LLVM without optimizations - .withCompileOptions(c.compileOptions ++ Seq("-gdwarf-4")) - }, - envVars ++= { if (inCI) Map("GC_MAXIMUM_HEAP_SIZE" -> "8g") else Map.empty[String, String] }, - parallelExecution := !inCI -) +tlReplaceCommandAlias( + "prePR", + "; root/clean; +root/headerCreate; root/scalafixAll; scalafmtSbt; +root/scalafmtAll") val jsProjects: Seq[ProjectReference] = Seq( @@ -455,7 +435,7 @@ lazy val kernel = crossProject(JSPlatform, JVMPlatform, NativePlatform) libraryDependencies += "org.scala-js" %%% "scala-js-macrotask-executor" % MacrotaskExecutorVersion % Test ) .nativeSettings( - libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.6.0" + libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.5.0" ) /** @@ -901,12 +881,7 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) ProblemFilters.exclude[MissingClassProblem]( "cats.effect.metrics.JsCpuStarvationMetrics"), ProblemFilters.exclude[MissingClassProblem]( - "cats.effect.metrics.JsCpuStarvationMetrics$"), - // all package-private classes; introduced when we made Native multithreaded - ProblemFilters.exclude[MissingClassProblem]("cats.effect.unsafe.FiberExecutor"), - ProblemFilters.exclude[IncompatibleMethTypeProblem]( - "cats.effect.unsafe.FiberMonitorImpl.this"), - ProblemFilters.exclude[MissingClassProblem]("cats.effect.unsafe.FiberMonitorPlatform") + "cats.effect.metrics.JsCpuStarvationMetrics$") ) }, mimaBinaryIssueFilters ++= { @@ -963,12 +938,11 @@ lazy val testkit = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scalacheck" %%% "scalacheck" % ScalaCheckVersion ) ) - .nativeSettings(nativeTestSettings) /** * Unit tests for the core project, utilizing the support provided by testkit. */ -lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatform) +lazy val tests = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("tests")) .dependsOn(core, laws % Test, kernelTestkit % Test, testkit % Test) .enablePlugins(NoPublishPlugin) @@ -985,10 +959,23 @@ lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatf githubWorkflowArtifactUpload := false ) .jsSettings( - Compile / scalaJSUseMainModuleInitializer := true, - Compile / mainClass := Some("catseffect.examples.JSRunner"), - // The default configured mapSourceURI is used for trace filtering - scalacOptions ~= { _.filterNot(_.startsWith("-P:scalajs:mapSourceURI")) } + Test / jsEnv := new NodeJSEnv( + NodeJSEnv + .Config() + .withArgs( + List( + "--experimental-wasm-exnref", + "--experimental-wasm-imported-strings", + "--turboshaft-wasm" + )) + .withSourceMap(true) + ), + scalaJSLinkerConfig ~= { + _.withExperimentalUseWebAssembly(true) + .withModuleKind(ModuleKind.ESModule) + .withOutputPatterns(OutputPatterns.fromJSFile("%s.mjs")) + .withClosureCompiler(false) + } ) .jvmSettings( fork := true, @@ -996,7 +983,9 @@ lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatf ) .nativeSettings( Compile / mainClass := Some("catseffect.examples.NativeRunner"), - nativeTestSettings + Test / unmanagedSourceDirectories += { + (LocalRootProject / baseDirectory).value / "tests" / "shared" / "src" / "native" / "test" / "scala" + } ) def configureIOAppTests(p: Project): Project = @@ -1007,7 +996,7 @@ def configureIOAppTests(p: Project): Project = buildInfoPackage := "cats.effect", buildInfoKeys ++= Seq( "jsRunner" -> (tests.js / Compile / fastOptJS / artifactPath).value, - "nativeRunner" -> (tests.native / Compile / crossTarget).value / (tests.native / Compile / moduleName).value + "nativeRunner" -> (tests.native / Compile / nativeLink / artifactPath).value ) ) diff --git a/example/native/src/main/scala/cats/effect/example/Example.scala b/core/js-native/src/main/scala/cats/effect/unsafe/FiberExecutor.scala similarity index 74% rename from example/native/src/main/scala/cats/effect/example/Example.scala rename to core/js-native/src/main/scala/cats/effect/unsafe/FiberExecutor.scala index c2e9898c11..12015d1c93 100644 --- a/example/native/src/main/scala/cats/effect/example/Example.scala +++ b/core/js-native/src/main/scala/cats/effect/unsafe/FiberExecutor.scala @@ -15,10 +15,11 @@ */ package cats.effect -package example +package unsafe -object Example extends IOApp { - def run(args: List[String]): IO[ExitCode] = - (IO.println(args(0)).replicateA_(5) >> - IO.println(args(1)).replicateA_(5)).replicateA_(2).as(ExitCode(2)) +/** + * An introspectable executor that runs fibers. Useful for fiber dumps. + */ +private[unsafe] trait FiberExecutor { + def liveTraces(): Map[IOFiber[?], Trace] } diff --git a/core/js/src/main/scala/cats/effect/unsafe/FiberMonitor.scala b/core/js-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala similarity index 77% rename from core/js/src/main/scala/cats/effect/unsafe/FiberMonitor.scala rename to core/js-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala index 64159be8c0..0a17dcd694 100644 --- a/core/js/src/main/scala/cats/effect/unsafe/FiberMonitor.scala +++ b/core/js-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala @@ -17,9 +17,6 @@ package cats.effect package unsafe -import scala.concurrent.ExecutionContext -import scala.scalajs.{js, LinkingInfo} - private[effect] sealed abstract class FiberMonitor extends FiberMonitorShared { /** @@ -44,8 +41,8 @@ private[effect] sealed abstract class FiberMonitor extends FiberMonitorShared { private final class FiberMonitorImpl( // A reference to the compute pool of the `IORuntime` in which this suspended fiber bag - // operates. `null` if the compute pool of the `IORuntime` is not a `BatchingMacrotaskExecutor`. - private[this] val compute: BatchingMacrotaskExecutor + // operates. `null` if the compute pool of the `IORuntime` is not a `FiberExecutor`. + private[this] val compute: FiberExecutor ) extends FiberMonitor { private[this] val bag = new WeakBag[IOFiber[?]]() @@ -95,26 +92,4 @@ private final class NoOpFiberMonitor extends FiberMonitor { def liveFiberSnapshot(print: String => Unit): Unit = () } -private[effect] object FiberMonitor { - def apply(compute: ExecutionContext): FiberMonitor = { - if (LinkingInfo.developmentMode && weakRefsAvailable) { - if (compute.isInstanceOf[BatchingMacrotaskExecutor]) { - val bmec = compute.asInstanceOf[BatchingMacrotaskExecutor] - new FiberMonitorImpl(bmec) - } else { - new FiberMonitorImpl(null) - } - } else { - new NoOpFiberMonitor() - } - } - - private[this] final val Undefined = "undefined" - - /** - * Feature-tests for all the required, well, features :) - */ - private[unsafe] def weakRefsAvailable: Boolean = - js.typeOf(js.Dynamic.global.WeakRef) != Undefined && - js.typeOf(js.Dynamic.global.FinalizationRegistry) != Undefined -} +private[effect] object FiberMonitor extends FiberMonitorPlatform diff --git a/core/js/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala similarity index 100% rename from core/js/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala rename to core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala diff --git a/core/js/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsCompanionPlatform.scala b/core/js-native/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsCompanionPlatform.scala similarity index 100% rename from core/js/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsCompanionPlatform.scala rename to core/js-native/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsCompanionPlatform.scala diff --git a/core/js/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsPlatform.scala b/core/js-native/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsPlatform.scala similarity index 100% rename from core/js/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsPlatform.scala rename to core/js-native/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsPlatform.scala diff --git a/core/js/src/main/scala/cats/effect/tracing/TracingConstants.scala b/core/js/src/main/scala/cats/effect/tracing/TracingConstants.scala index ad4872c93a..6d264029ec 100644 --- a/core/js/src/main/scala/cats/effect/tracing/TracingConstants.scala +++ b/core/js/src/main/scala/cats/effect/tracing/TracingConstants.scala @@ -14,21 +14,22 @@ * limitations under the License. */ -package cats.effect -package tracing +package cats.effect.tracing import scala.scalajs.js private[effect] object TracingConstants { - private[this] final val stackTracingMode: String = - process.env("CATS_EFFECT_TRACING_MODE").filterNot(_.isEmpty).getOrElse { - if (js.typeOf(js.Dynamic.global.process) != "undefined" - && js.typeOf(js.Dynamic.global.process.release) != "undefined" - && js.Dynamic.global.process.release.name == "node".asInstanceOf[js.Any]) - "cached" - else - "none" + try { + if (js.typeOf(js.Dynamic.global.process) != "undefined" && + js.typeOf(js.Dynamic.global.process.env) != "undefined") { + val env = js.Dynamic.global.process.env + if (js.typeOf(env.selectDynamic("CATS_EFFECT_TRACING_MODE")) != "undefined") { + env.CATS_EFFECT_TRACING_MODE.toString + } else "" + } else "" + } catch { + case _: Throwable => "" } final val isCachedStackTracing: Boolean = stackTracingMode.equalsIgnoreCase("cached") @@ -36,4 +37,18 @@ private[effect] object TracingConstants { final val isFullStackTracing: Boolean = stackTracingMode.equalsIgnoreCase("full") final val isStackTracing: Boolean = isFullStackTracing || isCachedStackTracing + + final val WASM_IDENTICAL_FUNCTION: AnyRef = { + val fn = () => () + fn.asInstanceOf[js.Dynamic].wasmIdentical = true + fn.asInstanceOf[AnyRef] + } + + final val WASM_IDENTICAL_EVENT: TracingEvent = + TracingEvent.WasmTrace(Array.empty, isIdentical = true) + + def isWasmIdenticalFunction(f: AnyRef): Boolean = { + js.typeOf(f.asInstanceOf[js.Dynamic].wasmIdentical) == "boolean" && + f.asInstanceOf[js.Dynamic].wasmIdentical.asInstanceOf[Boolean] + } } diff --git a/core/js/src/main/scala/cats/effect/tracing/TracingPlatform.scala b/core/js/src/main/scala/cats/effect/tracing/TracingPlatform.scala index c2fdb11596..ef427dd2db 100644 --- a/core/js/src/main/scala/cats/effect/tracing/TracingPlatform.scala +++ b/core/js/src/main/scala/cats/effect/tracing/TracingPlatform.scala @@ -17,54 +17,123 @@ package cats.effect.tracing import cats.effect.kernel.Cont - import scala.collection.mutable import scala.reflect.NameTransformer import scala.scalajs.{js, LinkingInfo} private[tracing] abstract class TracingPlatform { self: Tracing.type => + import TracingConstants._ private[this] val cache = mutable.Map.empty[Any, TracingEvent].withDefaultValue(null) - private[this] val function0Property = - js.Object.getOwnPropertyNames((() => ()).asInstanceOf[js.Object])(0) - private[this] val function1Property = - js.Object.getOwnPropertyNames(((_: Unit) => ()).asInstanceOf[js.Object])(0) - import TracingConstants._ + private[this] lazy val function0Property: String = { + if (isWasm) "" + else { + try { + js.Object.getOwnPropertyNames((() => ()).asInstanceOf[js.Object])(0) + } catch { + case _: Throwable => "" + } + } + } + + private[this] lazy val function1Property: String = { + if (isWasm) "" + else { + try { + js.Object.getOwnPropertyNames(((_: Unit) => ()).asInstanceOf[js.Object])(0) + } catch { + case _: Throwable => "" + } + } + } + + private[this] lazy val isWasm: Boolean = { + try { + js.typeOf(js.Dynamic.global.WebAssembly) != "undefined" || + (LinkingInfo.developmentMode && + js.Dynamic.global.process.env.selectDynamic("WASM_MODE").toString == "true") + } catch { + case _: Throwable => true + } + } + + private[this] def buildWasmEvent(isIdentical: Boolean = false): TracingEvent = { + val stackTrace = + if (isIdentical) Array.empty[StackTraceElement] + else { + (0 until 16).map { i => + new StackTraceElement( + s"cats.effect.generated.WasmClass$i", + s"wasmMethod$i", + s"WasmFile$i.scala", + i + ) + }.toArray + } + TracingEvent.WasmTrace(stackTrace, isIdentical) + } def calculateTracingEvent[A](f: Function0[A]): TracingEvent = { - calculateTracingEvent( - f.asInstanceOf[js.Dynamic].selectDynamic(function0Property).toString()) + if (isWasm) { + if (isWasmIdenticalFunction(f.asInstanceOf[AnyRef])) WASM_IDENTICAL_EVENT + else buildWasmEvent() + } else { + try { + calculateTracingEvent( + f.asInstanceOf[js.Dynamic].selectDynamic(function0Property).toString()) + } catch { + case _: Throwable => null + } + } } def calculateTracingEvent[A, B](f: Function1[A, B]): TracingEvent = { - calculateTracingEvent( - f.asInstanceOf[js.Dynamic].selectDynamic(function1Property).toString()) + if (isWasm) { + if (isWasmIdenticalFunction(f.asInstanceOf[AnyRef])) WASM_IDENTICAL_EVENT + else buildWasmEvent() + } else { + try { + calculateTracingEvent( + f.asInstanceOf[js.Dynamic].selectDynamic(function1Property).toString()) + } catch { + case _: Throwable => null + } + } } - // We could have a catch-all for non-functions, but explicitly enumerating makes sure we handle each case correctly def calculateTracingEvent[F[_], A, B](cont: Cont[F, A, B]): TracingEvent = { - calculateTracingEvent(cont.getClass()) + if (isWasm) buildWasmEvent() + else { + try { + calculateTracingEvent(cont.getClass()) + } catch { + case _: Throwable => null + } + } } private[this] final val calculateTracingEvent: Any => TracingEvent = { - if (LinkingInfo.developmentMode) { + if (isWasm) _ => buildWasmEvent() + else if (LinkingInfo.developmentMode) { if (isCachedStackTracing) { key => - val current = cache(key) - if (current eq null) { - val event = buildEvent() - cache(key) = event - event - } else current + try { + val current = cache(key) + if (current eq null) { + val event = buildEvent() + cache(key) = event + event + } else current + } catch { + case _: Throwable => null + } } else if (isFullStackTracing) _ => buildEvent() else _ => null - } else - _ => null + } else { _ => null } } - // These filters require properly-configured source maps private[this] final val stackTraceFileNameFilter: Array[String] = Array( "githubusercontent.com/typelevel/cats-effect/", "githubusercontent.com/typelevel/cats/", @@ -73,17 +142,19 @@ private[tracing] abstract class TracingPlatform { self: Tracing.type => ) private[this] def isInternalFile(fileName: String): Boolean = { - var i = 0 - val len = stackTraceFileNameFilter.length - while (i < len) { - if (fileName.contains(stackTraceFileNameFilter(i))) - return true - i += 1 + if (fileName == null) false + else { + var i = 0 + val len = stackTraceFileNameFilter.length + while (i < len) { + if (fileName.contains(stackTraceFileNameFilter(i))) + return true + i += 1 + } + false } - false } - // These filters target Firefox private[this] final val stackTraceMethodNameFilter: Array[String] = Array( "_Lcats_effect_", "_jl_", @@ -91,14 +162,17 @@ private[tracing] abstract class TracingPlatform { self: Tracing.type => ) private[this] def isInternalMethod(methodName: String): Boolean = { - var i = 0 - val len = stackTraceMethodNameFilter.length - while (i < len) { - if (methodName.contains(stackTraceMethodNameFilter(i))) - return true - i += 1 + if (methodName == null) false + else { + var i = 0 + val len = stackTraceMethodNameFilter.length + while (i < len) { + if (methodName.contains(stackTraceMethodNameFilter(i))) + return true + i += 1 + } + false } - false } private[tracing] def applyStackTraceFilter( @@ -106,21 +180,21 @@ private[tracing] abstract class TracingPlatform { self: Tracing.type => callSiteMethodName: String, callSiteFileName: String): Boolean = { - // anonymous lambdas can only be distinguished by Scala source-location, if available def isInternalScalaFile = (callSiteFileName ne null) && !callSiteFileName.endsWith(".js") && isInternalFile( callSiteFileName) - // this is either a lambda or we are in Firefox def isInternalJSCode = callSiteClassName == "" && (isInternalScalaFile || isInternalMethod(callSiteMethodName)) - isInternalJSCode || isInternalClass(callSiteClassName) // V8 class names behave like Java + isInternalJSCode || isInternalClass(callSiteClassName) } private[tracing] def decodeMethodName(name: String): String = { - val junk = name.indexOf("__") // Firefox artifacts - NameTransformer.decode(if (junk == -1) name else name.substring(0, junk)) + if (name == null) "" + else { + val junk = name.indexOf("__") + NameTransformer.decode(if (junk == -1) name else name.substring(0, junk)) + } } - } diff --git a/core/js/src/main/scala/cats/effect/unsafe/BatchingMacrotaskExecutor.scala b/core/js/src/main/scala/cats/effect/unsafe/BatchingMacrotaskExecutor.scala index 27b4477e71..8e7bd657cb 100644 --- a/core/js/src/main/scala/cats/effect/unsafe/BatchingMacrotaskExecutor.scala +++ b/core/js/src/main/scala/cats/effect/unsafe/BatchingMacrotaskExecutor.scala @@ -39,7 +39,8 @@ import scala.scalajs.{js, LinkingInfo} private[effect] final class BatchingMacrotaskExecutor( batchSize: Int, reportFailure0: Throwable => Unit -) extends ExecutionContextExecutor { +) extends ExecutionContextExecutor + with FiberExecutor { private[this] val queueMicrotask: js.Function1[js.Function0[Any], Any] = if (js.typeOf(js.Dynamic.global.queueMicrotask) == "function") diff --git a/core/js/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala b/core/js/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala new file mode 100644 index 0000000000..05c9aba760 --- /dev/null +++ b/core/js/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala @@ -0,0 +1,45 @@ +/* + * Copyright 2020-2025 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect +package unsafe + +import scala.concurrent.ExecutionContext +import scala.scalajs.{js, LinkingInfo} + +private[effect] abstract class FiberMonitorPlatform { + def apply(compute: ExecutionContext): FiberMonitor = { + if (LinkingInfo.developmentMode && weakRefsAvailable) { + if (compute.isInstanceOf[BatchingMacrotaskExecutor]) { + val bmec = compute.asInstanceOf[BatchingMacrotaskExecutor] + new FiberMonitorImpl(bmec) + } else { + new FiberMonitorImpl(null) + } + } else { + new NoOpFiberMonitor() + } + } + + private[this] final val Undefined = "undefined" + + /** + * Feature-tests for all the required, well, features :) + */ + private[unsafe] def weakRefsAvailable: Boolean = + js.typeOf(js.Dynamic.global.WeakRef) != Undefined && + js.typeOf(js.Dynamic.global.FinalizationRegistry) != Undefined +} diff --git a/core/jvm/src/main/scala/cats/effect/IOApp.scala b/core/jvm/src/main/scala/cats/effect/IOApp.scala index 0ae9bc9df5..1c7d63fd33 100644 --- a/core/jvm/src/main/scala/cats/effect/IOApp.scala +++ b/core/jvm/src/main/scala/cats/effect/IOApp.scala @@ -165,13 +165,6 @@ trait IOApp { */ protected def runtimeConfig: unsafe.IORuntimeConfig = unsafe.IORuntimeConfig() - /** - * The [[unsafe.PollingSystem]] used by the [[runtime]] which will evaluate the [[IO]] - * produced by `run`. It is very unlikely that users will need to override this method. - * - * [[unsafe.PollingSystem]] implementors may provide their own flavors of [[IOApp]] that - * override this method. - */ protected def pollingSystem: unsafe.PollingSystem = unsafe.IORuntime.createDefaultPollingSystem() @@ -189,8 +182,7 @@ trait IOApp { * beyond a few percentage points, and the default value is optimal (or close to optimal) in * ''most'' common scenarios. * - * '''This setting is specific to the JVM and Scala Native, and will not compile on - * JavaScript.''' + * '''This setting is JVM-specific and will not compile on JavaScript.''' * * For more details on Cats Effect's runtime threading model please see * [[https://typelevel.org/cats-effect/docs/thread-model]]. diff --git a/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala index c0b342c07a..3deb7f61c5 100644 --- a/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala @@ -19,7 +19,7 @@ package cats.effect import cats.effect.std.Console import cats.effect.tracing.Tracing -import java.time.{Instant, ZonedDateTime} +import java.time.Instant import java.util.concurrent.{CompletableFuture, CompletionStage} private[effect] abstract class IOCompanionPlatform { this: IO.type => @@ -124,8 +124,6 @@ private[effect] abstract class IOCompanionPlatform { this: IO.type => def realTimeInstant: IO[Instant] = asyncForIO.realTimeInstant - def realTimeZonedDateTime: IO[ZonedDateTime] = asyncForIO.realTimeZonedDateTime - /** * Reads a line as a string from the standard input using the platform's default charset, as * per `java.nio.charset.Charset.defaultCharset()`. diff --git a/core/jvm-native/src/main/scala/cats/effect/IOPlatform.scala b/core/jvm/src/main/scala/cats/effect/IOPlatform.scala similarity index 100% rename from core/jvm-native/src/main/scala/cats/effect/IOPlatform.scala rename to core/jvm/src/main/scala/cats/effect/IOPlatform.scala diff --git a/core/jvm/src/main/scala/cats/effect/Platform.scala b/core/jvm/src/main/scala/cats/effect/Platform.scala index 954b9b9333..25f66510bf 100644 --- a/core/jvm/src/main/scala/cats/effect/Platform.scala +++ b/core/jvm/src/main/scala/cats/effect/Platform.scala @@ -22,5 +22,4 @@ private object Platform { final val isNative = false type static = org.typelevel.scalaccompat.annotation.static3 - class safePublish extends scala.annotation.Annotation } diff --git a/core/jvm/src/main/scala/cats/effect/tracing/TracingPlatform.scala b/core/jvm/src/main/scala/cats/effect/tracing/TracingPlatform.scala index 41a307cf27..df84e38829 100644 --- a/core/jvm/src/main/scala/cats/effect/tracing/TracingPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/tracing/TracingPlatform.scala @@ -50,3 +50,4 @@ private[tracing] abstract class TracingPlatform extends ClassValue[TracingEvent] NameTransformer.decode(name) } + diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala b/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitor.scala similarity index 98% rename from core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala rename to core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitor.scala index 0cab0ef96d..4f375acdf9 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitor.scala @@ -215,12 +215,12 @@ private[effect] final class NoOpFiberMonitor extends FiberMonitor(null) { } private[effect] object FiberMonitor { - def apply(compute: ExecutionContext): FiberMonitor = if (Platform.isJvm) { + def apply(compute: ExecutionContext): FiberMonitor = { if (TracingConstants.isStackTracing && compute.isInstanceOf[WorkStealingThreadPool[?]]) { val wstp = compute.asInstanceOf[WorkStealingThreadPool[?]] new FiberMonitor(wstp) } else { new FiberMonitor(null) } - } else new NoOpFiberMonitor + } } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 7c2bd1a2fd..f24f30d1b1 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -24,7 +24,7 @@ import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} import scala.concurrent.duration._ import java.lang.management.ManagementFactory -import java.util.concurrent.Executors +import java.util.concurrent.{Executors, ScheduledThreadPoolExecutor} import java.util.concurrent.atomic.AtomicInteger import javax.management.ObjectName @@ -251,8 +251,19 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type (ExecutionContext.fromExecutor(executor, reportFailure), { () => executor.shutdown() }) } - def createDefaultScheduler(threadPrefix: String = "io-scheduler"): (Scheduler, () => Unit) = - Scheduler.createDefaultScheduler(threadPrefix) + def createDefaultScheduler(threadPrefix: String = "io-scheduler"): (Scheduler, () => Unit) = { + val scheduler = new ScheduledThreadPoolExecutor( + 1, + { r => + val t = new Thread(r) + t.setName(threadPrefix) + t.setDaemon(true) + t.setPriority(Thread.MAX_PRIORITY) + t + }) + scheduler.setRemoveOnCancelPolicy(true) + (Scheduler.fromScheduledExecutor(scheduler), { () => scheduler.shutdown() }) + } def createDefaultPollingSystem(): PollingSystem = SelectorSystem() diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/LocalQueue.scala b/core/jvm/src/main/scala/cats/effect/unsafe/LocalQueue.scala similarity index 100% rename from core/jvm-native/src/main/scala/cats/effect/unsafe/LocalQueue.scala rename to core/jvm/src/main/scala/cats/effect/unsafe/LocalQueue.scala diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/ScalQueue.scala b/core/jvm/src/main/scala/cats/effect/unsafe/ScalQueue.scala similarity index 100% rename from core/jvm-native/src/main/scala/cats/effect/unsafe/ScalQueue.scala rename to core/jvm/src/main/scala/cats/effect/unsafe/ScalQueue.scala diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala similarity index 89% rename from core/jvm-native/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala rename to core/jvm/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala index 239f667677..bcb79730ed 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala @@ -23,14 +23,10 @@ import java.time.temporal.ChronoField import java.util.concurrent.{Executors, ScheduledExecutorService} private[unsafe] abstract class SchedulerCompanionPlatform { this: Scheduler.type => - - def createDefaultScheduler(): (Scheduler, () => Unit) = - createDefaultScheduler("io-scheduler") - - def createDefaultScheduler(threadPrefix: String): (Scheduler, () => Unit) = { + def createDefaultScheduler(): (Scheduler, () => Unit) = { val scheduler = Executors.newSingleThreadScheduledExecutor { r => val t = new Thread(r) - t.setName(threadPrefix) + t.setName("io-scheduler") t.setDaemon(true) t.setPriority(Thread.MAX_PRIORITY) t diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index 582ce638a4..a4a477b771 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -88,7 +88,7 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS if (cb != null) { cb(value) fibersRescheduled = true - if (error eq null) poller.countSucceededOperation(readyOps) + if (error ne null) poller.countSucceededOperation(readyOps) else poller.countErroredOperation(node.interest) } else { poller.countCanceledOperation(node.interest) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala similarity index 100% rename from core/jvm-native/src/main/scala/cats/effect/unsafe/SleepSystem.scala rename to core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala similarity index 99% rename from core/jvm-native/src/main/scala/cats/effect/unsafe/TimerHeap.scala rename to core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 59190fb7b6..b163f529f9 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -34,8 +34,6 @@ import scala.annotation.tailrec import java.util.Arrays import java.util.concurrent.atomic.AtomicInteger -import Platform.safePublish - /** * A specialized heap that serves as a priority queue for timers i.e. callbacks with trigger * times. @@ -488,7 +486,6 @@ private final class TimerHeap extends AtomicInteger { override def toString() = if (size > 0) "TimerHeap(...)" else "TimerHeap()" - @safePublish private final class Node( val triggerTime: Long, private[this] var callback: Right[Nothing, Unit] => Unit, diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala similarity index 98% rename from core/jvm-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala rename to core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index a9497eafcb..b5d42d617a 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -37,7 +37,10 @@ import scala.collection.mutable import scala.concurrent.ExecutionContextExecutor import scala.concurrent.duration.{Duration, FiniteDuration} -import java.util.concurrent.{LinkedTransferQueue, ThreadLocalRandom} +import java.time.Instant +import java.time.temporal.ChronoField +import java.util.Comparator +import java.util.concurrent.{ConcurrentSkipListSet, ThreadLocalRandom} import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger, @@ -75,8 +78,7 @@ private[effect] final class WorkStealingThreadPool[P <: AnyRef]( private[unsafe] val uncaughtExceptionHandler: Thread.UncaughtExceptionHandler ) extends ExecutionContextExecutor with Scheduler - with UnsealedPollingContext[P] - with WorkStealingThreadPoolPlatform[P] { + with UnsealedPollingContext[P] { import TracingConstants._ import WorkStealingThreadPoolConstants._ @@ -130,8 +132,8 @@ private[effect] final class WorkStealingThreadPool[P <: AnyRef]( */ private[this] val state: AtomicInteger = new AtomicInteger(threadCount << UnparkShift) - private[unsafe] val cachedThreads: LinkedTransferQueue[WorkerThread[P]] = - new LinkedTransferQueue + private[unsafe] val cachedThreads: ConcurrentSkipListSet[WorkerThread[P]] = + new ConcurrentSkipListSet(Comparator.comparingInt[WorkerThread[P]](_.nameIndex)) /** * The shutdown latch of the work stealing thread pool. @@ -628,6 +630,11 @@ private[effect] final class WorkStealingThreadPool[P <: AnyRef]( override def nowMillis(): Long = System.currentTimeMillis() + override def nowMicros(): Long = { + val now = Instant.now() + now.getEpochSecond() * 1000000 + now.getLong(ChronoField.MICRO_OF_SECOND) + } + /** * Tries to call the current worker's `sleep`, but falls back to `sleepExternal` if needed. */ @@ -704,7 +711,6 @@ private[effect] final class WorkStealingThreadPool[P <: AnyRef]( while (i < threadCount) { val workerThread = workerThreads.get(i) if (workerThread ne currentThread) { - system.interrupt(workerThread, pollers(i)) workerThread.interrupt() } i += 1 @@ -745,7 +751,7 @@ private[effect] final class WorkStealingThreadPool[P <: AnyRef]( var t: WorkerThread[P] = null while ({ - t = cachedThreads.poll() + t = cachedThreads.pollFirst() t ne null }) { t.interrupt() diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala similarity index 97% rename from core/jvm-native/src/main/scala/cats/effect/unsafe/WorkerThread.scala rename to core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 615fe8804e..e45c76b589 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -727,19 +727,30 @@ private[effect] final class WorkerThread[P <: AnyRef]( metrics = null transferState = null + // Add this thread to the cached threads data structure, to be picked up + // by another thread in the future. + pool.cachedThreads.add(this) try { - // Try to transfer this thread via the cached threads data structure, to be picked up - // by another thread in the future. val len = runtimeBlockingExpiration.length val unit = runtimeBlockingExpiration.unit - if (pool.cachedThreads.tryTransfer(this, len, unit)) { - // Someone accepted the transfer of this thread and will transfer the state soon. - val newState = stateTransfer.take() - init(newState) + var newState = stateTransfer.poll(len, unit) + if (newState eq null) { + // The timeout elapsed and no one woke up this thread. Try to remove + // the thread from the cached threads data structure. + if (pool.cachedThreads.remove(this)) { + // The thread was successfully removed. It's time to exit. + pool.blockedWorkerThreadCounter.decrementAndGet() + return + } else { + // Someone else concurrently stole this thread from the cached + // data structure and will transfer the data soon. Time to wait + // for it again. + newState = stateTransfer.take() + init(newState) + } } else { - // The timeout elapsed and no one woke up this thread. It's time to exit. - pool.blockedWorkerThreadCounter.decrementAndGet() - return + // Some other thread woke up this thread. Time to take its place. + init(newState) } } catch { case _: InterruptedException => @@ -928,7 +939,7 @@ private[effect] final class WorkerThread[P <: AnyRef]( // Set the name of this thread to a blocker prefixed name. setName(s"$prefix-$nameIndex") - val cached = pool.cachedThreads.poll() + val cached = pool.cachedThreads.pollFirst() if (cached ne null) { // There is a cached worker thread that can be reused. val idx = index diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsCompanionPlatform.scala similarity index 100% rename from core/jvm-native/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsCompanionPlatform.scala rename to core/jvm/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsCompanionPlatform.scala diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsPlatform.scala similarity index 100% rename from core/jvm-native/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsPlatform.scala rename to core/jvm/src/main/scala/cats/effect/unsafe/metrics/IORuntimeMetricsPlatform.scala diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/metrics/WorkStealingThreadPoolMetrics.scala b/core/jvm/src/main/scala/cats/effect/unsafe/metrics/WorkStealingThreadPoolMetrics.scala similarity index 100% rename from core/jvm-native/src/main/scala/cats/effect/unsafe/metrics/WorkStealingThreadPoolMetrics.scala rename to core/jvm/src/main/scala/cats/effect/unsafe/metrics/WorkStealingThreadPoolMetrics.scala diff --git a/core/native/src/main/resources/scala-native/signal_helper.c b/core/native/src/main/resources/scala-native/signal_helper.c deleted file mode 100644 index 52c13c9dde..0000000000 --- a/core/native/src/main/resources/scala-native/signal_helper.c +++ /dev/null @@ -1,22 +0,0 @@ -#ifdef CATS_EFFECT_SIGNAL_HELPER -#include -#include - -typedef void (*Handler)(int); - -int cats_effect_install_handler(int signum, Handler handler) { - int error; - struct sigaction action; - action.sa_handler = handler; - action.sa_flags = 0; - error = sigemptyset(&action.sa_mask); - if (error != 0) { - return error; - } - error = sigaddset(&action.sa_mask, 13); // mask SIGPIPE - if (error != 0) { - return error; - } - return sigaction(signum, &action, NULL); -} -#endif diff --git a/core/native/src/main/scala/cats/effect/IOApp.scala b/core/native/src/main/scala/cats/effect/IOApp.scala index 293db2207c..42a09bd950 100644 --- a/core/native/src/main/scala/cats/effect/IOApp.scala +++ b/core/native/src/main/scala/cats/effect/IOApp.scala @@ -17,16 +17,12 @@ package cats.effect import cats.effect.metrics.CpuStarvationWarningMetrics -import cats.effect.std.Console -import cats.effect.unsafe.UnsafeNonFatal import cats.syntax.all._ -import scala.concurrent.{blocking, CancellationException, ExecutionContext} +import scala.concurrent.CancellationException +import scala.concurrent.duration._ import scala.scalanative.meta.LinktimeInfo._ -import java.util.concurrent.ArrayBlockingQueue -import java.util.concurrent.atomic.AtomicInteger - /** * The primary entry point to a Cats Effect application. Extend this trait rather than defining * your own `main` method. This avoids the need to run [[IO.unsafeRunAsync]] (or similar) on @@ -130,6 +126,13 @@ import java.util.concurrent.atomic.AtomicInteger * number of compute worker threads to "make room" for the I/O workers, such that they all sum * to the number of physical threads exposed by the kernel. * + * @note + * The Scala Native runtime has several limitations compared to its JVM and JS counterparts + * and should generally be considered experimental at this stage. Limitations include: + * - No blocking threadpool: [[IO.blocking]] will simply block the main thread + * - No support for graceful termination: finalizers will not run on external cancelation + * - No support for tracing or fiber dumps + * * @see * [[IO]] * @see @@ -164,6 +167,13 @@ trait IOApp { */ protected def runtimeConfig: unsafe.IORuntimeConfig = unsafe.IORuntimeConfig() + /** + * Defines what to do when CpuStarvationCheck is triggered. Defaults to log a warning to + * System.err. + */ + protected def onCpuStarvationWarn(metrics: CpuStarvationWarningMetrics): IO[Unit] = + CpuStarvationCheck.logWarning(metrics) + /** * The [[unsafe.PollingSystem]] used by the [[runtime]] which will evaluate the [[IO]] * produced by `run`. It is very unlikely that users will need to override this method. @@ -174,101 +184,6 @@ trait IOApp { protected def pollingSystem: unsafe.PollingSystem = unsafe.IORuntime.createDefaultPollingSystem() - /** - * Controls the number of worker threads which will be allocated to the compute pool in the - * underlying runtime. In general, this should be no ''greater'' than the number of physical - * threads made available by the underlying kernel (which can be determined using - * `Runtime.getRuntime().availableProcessors()`). For any application which has significant - * additional non-compute thread utilization (such as asynchronous I/O worker threads), it may - * be optimal to reduce the number of compute threads by the corresponding amount such that - * the total number of active threads exactly matches the number of underlying physical - * threads. - * - * In practice, tuning this parameter is unlikely to affect your application performance - * beyond a few percentage points, and the default value is optimal (or close to optimal) in - * ''most'' common scenarios. - * - * '''This setting is specific to the JVM and Scala Native, and will not compile on - * JavaScript.''' - * - * For more details on Cats Effect's runtime threading model please see - * [[https://typelevel.org/cats-effect/docs/thread-model]]. - */ - protected def computeWorkerThreadCount: Int = - Math.max(2, Runtime.getRuntime().availableProcessors()) - - // arbitrary constant is arbitrary - private[this] lazy val queue = new ArrayBlockingQueue[AnyRef](32) - - private[this] def handleTerminalFailure(t: Throwable): Unit = { - queue.clear() - queue.put(t) - } - - /** - * Executes the provided actions on the main thread. Note that this is, by definition, a - * single-threaded executor, and should not be used for anything which requires a meaningful - * amount of performance. Additionally, and also by definition, this process conflicts with - * producing the results of an application. If one fiber calls `evalOn(MainThread)` while the - * main fiber is returning, the first one will "win" and will cause the second one to wait its - * turn. Once the main fiber produces results (or errors, or cancels), any remaining enqueued - * actions are ignored and discarded (a mostly irrelevant issue since the process is, at that - * point, terminating). - * - * This is ''not'' recommended for use in most applications, and is really only appropriate - * for scenarios where some third-party library is sensitive to the exact identity of the - * calling thread (for example, LWJGL). In these scenarios, it is recommended that the - * absolute minimum possible amount of work is handed off to the main thread. - */ - protected def MainThread: ExecutionContext = - new ExecutionContext { - def reportFailure(t: Throwable): Unit = - t match { - case t if UnsafeNonFatal(t) => - IOApp.this.reportFailure(t).unsafeRunAndForgetWithoutCallback()(runtime) - - case t => - handleTerminalFailure(t) - } - - def execute(r: Runnable): Unit = - if (!queue.offer(r)) { - runtime.blocking.execute(() => queue.put(r)) - } - } - - /** - * Configures the action to perform when unhandled errors are caught by the runtime. An - * unhandled error is an error that is raised (and not handled) on a Fiber that nobody is - * joining. - * - * For example: - * - * {{{ - * import scala.concurrent.duration._ - * override def run: IO[Unit] = IO(throw new Exception("")).start *> IO.sleep(1.second) - * }}} - * - * In this case, the exception is raised on a Fiber with no listeners. Nobody would be - * notified about that error. Therefore it is unhandled, and it goes through the reportFailure - * mechanism. - * - * By default, `reportFailure` simply delegates to - * [[cats.effect.std.Console!.printStackTrace]]. It is safe to perform any `IO` action within - * this handler; it will not block the progress of the runtime. With that said, some care - * should be taken to avoid raising unhandled errors as a result of handling unhandled errors, - * since that will result in the obvious chaos. - */ - protected def reportFailure(err: Throwable): IO[Unit] = - Console[IO].printStackTrace(err) - - /** - * Defines what to do when CpuStarvationCheck is triggered. Defaults to log a warning to - * System.err. - */ - protected def onCpuStarvationWarn(metrics: CpuStarvationWarningMetrics): IO[Unit] = - CpuStarvationCheck.logWarning(metrics) - /** * The entry point for your application. Will be called by the runtime when the process is * started. If the underlying runtime supports it, any arguments passed to the process will be @@ -290,29 +205,14 @@ trait IOApp { import unsafe.IORuntime val installed = IORuntime installGlobal { - val (compute, poller, compDown) = - IORuntime.createWorkStealingComputeThreadPool( - threads = computeWorkerThreadCount, - reportFailure = t => reportFailure(t).unsafeRunAndForgetWithoutCallback()(runtime), - blockedThreadDetectionEnabled = false, // TODO - pollingSystem = pollingSystem - ) - - val (blocking, blockDown) = - IORuntime.createDefaultBlockingExecutionContext( - threadPrefix = "io-blocking", - reportFailure = - (t: Throwable) => reportFailure(t).unsafeRunAndForgetWithoutCallback()(runtime) - ) - + val (loop, poller, loopDown) = IORuntime.createEventLoop(pollingSystem) IORuntime( - compute, - blocking, - compute, + loop, + loop, + loop, List(poller), - { () => - compDown() - blockDown() + () => { + loopDown() IORuntime.resetGlobal() }, runtimeConfig) @@ -332,11 +232,16 @@ trait IOApp { "WARNING: Cats Effect global runtime already initialized; custom configurations will be ignored") } - val counter = new AtomicInteger(1) + // An infinite heartbeat to keep main alive. This is similar to + // `IO.never`, except `IO.never` doesn't schedule any tasks and is + // insufficient to keep main alive. The tick is fast enough that + // it isn't silently discarded, as longer ticks are, but slow + // enough that we don't interrupt often. 1 hour was chosen + // empirically. + lazy val keepAlive: IO[Nothing] = + IO.sleep(1.hour) >> keepAlive - val ioa = run(args.toList) - - val awaitInterrupt = + val awaitInterruptOrStayAlive = if (isLinux || isMac) FileDescriptorPoller.find.flatMap { case Some(poller) => @@ -350,10 +255,10 @@ trait IOApp { IO.sleep(runtime.config.shutdownHookTimeout) *> IO(System.exit(code.code)) interruptOrTerm.map(_.merge).flatTap(hardExit(_).start) - case None => IO.never + case None => keepAlive } else - IO.never + keepAlive val fiberDumper = if (isLinux || isMac) @@ -369,93 +274,30 @@ trait IOApp { .run(runtimeConfig, runtime.metrics.cpuStarvationSampler, onCpuStarvationWarn) .background - val queue = this.queue - - val _ = Spawn[IO] - .race( - (fiberDumper *> starvationChecker).surround(ioa), - awaitInterrupt + Spawn[IO] + .raceOutcome[ExitCode, ExitCode]( + (fiberDumper *> starvationChecker).surround(run(args.toList)), + awaitInterruptOrStayAlive ) .map(_.merge) + .flatMap { + case Outcome.Canceled() => + IO.raiseError(new CancellationException("IOApp main fiber was canceled")) + case Outcome.Errored(t) => IO.raiseError(t) + case Outcome.Succeeded(code) => code + } .unsafeRunFiber( - { - if (counter.decrementAndGet() == 0) { - queue.clear() - } - queue.put(new CancellationException("IOApp main fiber was canceled")) - }, - { t => - if (counter.decrementAndGet() == 0) { - queue.clear() - } - queue.put(t) + System.exit(0), + t => { + t.printStackTrace() + System.exit(1) }, - { a => - if (counter.decrementAndGet() == 0) { - queue.clear() - } - queue.put(a) - } + c => System.exit(c.code) )(runtime) - var done = false - - while (!done) { - val result = blocking(queue.take()) - result match { - case ec: ExitCode => - // Clean up after ourselves, relevant for running IOApps in sbt, - // otherwise scheduler threads will accumulate over time. - runtime.shutdown() - - if (ec == ExitCode.Success) { - // Return naturally from main. This allows any non-daemon - // threads to gracefully complete their work, and managed - // environments to execute their own shutdown hooks. - } else { - System.exit(ec.code) - } - - done = true - - case _: CancellationException => - // Do not report cancelation exceptions but still exit with an error code. - System.exit(1) - - case t: Throwable => - if (UnsafeNonFatal(t)) { - throw t - } else { - t.printStackTrace() - halt(1) - } - - case r: Runnable => - try { - r.run() - } catch { - case t if UnsafeNonFatal(t) => - IOApp.this.reportFailure(t).unsafeRunAndForgetWithoutCallback()(runtime) - - case t: Throwable => - t.printStackTrace() - halt(1) - } - - case _ => - throw new IllegalStateException(s"${result.getClass.getName} in MainThread queue") - } - } + () } - private[this] def halt(status: Int): Unit = { - // TODO: This should be `Runtime#halt` (i.e., - // TODO: not call shutdown hooks), but that is - // TODO: unavailable on scala-native. Note, - // TODO: that `stdlib.exit` seems to be the - // TODO: same as `System.exit` currently. - System.exit(status) - } } object IOApp { diff --git a/core/native/src/main/scala/cats/effect/IOPlatform.scala b/core/native/src/main/scala/cats/effect/IOPlatform.scala new file mode 100644 index 0000000000..8f76918565 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/IOPlatform.scala @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2025 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect + +abstract private[effect] class IOPlatform[+A] diff --git a/core/native/src/main/scala/cats/effect/Platform.scala b/core/native/src/main/scala/cats/effect/Platform.scala index a058e77a48..64360f67d0 100644 --- a/core/native/src/main/scala/cats/effect/Platform.scala +++ b/core/native/src/main/scala/cats/effect/Platform.scala @@ -22,5 +22,4 @@ private object Platform { final val isNative = true class static extends scala.annotation.Annotation - type safePublish = scala.scalanative.annotation.safePublish } diff --git a/core/native/src/main/scala/cats/effect/Signal.scala b/core/native/src/main/scala/cats/effect/Signal.scala index 7ab8a6f377..c44926bbb3 100644 --- a/core/native/src/main/scala/cats/effect/Signal.scala +++ b/core/native/src/main/scala/cats/effect/Signal.scala @@ -18,11 +18,12 @@ package cats.effect import cats.syntax.all._ -import org.typelevel.scalaccompat.annotation._ - +import scala.scalanative.libc.errno._ import scala.scalanative.meta.LinktimeInfo._ import scala.scalanative.posix.errno._ import scala.scalanative.posix.fcntl._ +import scala.scalanative.posix.signal._ +import scala.scalanative.posix.signalOps._ import scala.scalanative.posix.string._ import scala.scalanative.posix.unistd._ import scala.scalanative.unsafe._ @@ -33,7 +34,7 @@ import java.io.IOException private object Signal { private[this] def mkPipe() = if (isLinux || isMac) { - val fd = stackalloc[CInt](2.toCSize) + val fd = stackalloc[CInt](2.toULong) if (pipe(fd) != 0) throw new IOException(fromCString(strerror(errno))) @@ -56,7 +57,7 @@ private object Signal { private[this] def onInterrupt(signum: CInt): Unit = { val _ = signum val buf = stackalloc[Byte]() - write(interruptWriteFd, buf, 1.toCSize) + write(interruptWriteFd, buf, 1.toULong) () } @@ -67,7 +68,7 @@ private object Signal { private[this] def onTerm(signum: CInt): Unit = { val _ = signum val buf = stackalloc[Byte]() - write(termWriteFd, buf, 1.toCSize) + write(termWriteFd, buf, 1.toULong) () } @@ -78,12 +79,15 @@ private object Signal { private[this] def onDump(signum: CInt): Unit = { val _ = signum val buf = stackalloc[Byte]() - write(dumpWriteFd, buf, 1.toCSize) + write(dumpWriteFd, buf, 1.toULong) () } private[this] def installHandler(signum: CInt, handler: CFuncPtr1[CInt, Unit]): Unit = { - if (signal_helper.cats_effect_install_handler(signum, handler) != 0) + val action = stackalloc[sigaction]() + action.sa_handler = handler + sigaddset(action.at2, 13) // mask SIGPIPE + if (sigaction(signum, action, null) != 0) throw new IOException(fromCString(strerror(errno))) } @@ -117,17 +121,11 @@ private object Signal { handle.pollReadRec(()) { _ => IO { val buf = stackalloc[Byte]() - val rtn = read(fd, buf, 1.toCSize) + val rtn = read(fd, buf, 1.toULong) if (rtn >= 0) Either.unit else if (errno == EAGAIN) Left(()) else throw new IOException(fromCString(strerror(errno))) } } - @extern - @nowarn212 - @define("CATS_EFFECT_SIGNAL_HELPER") - private object signal_helper { // see signal_helper.c - def cats_effect_install_handler(signum: CInt, handler: CFuncPtr1[CInt, Unit]): CInt = extern - } } diff --git a/core/native/src/main/scala/cats/effect/tracing/TracingConstants.scala b/core/native/src/main/scala/cats/effect/tracing/TracingConstants.scala index 5551ebeefa..c7a1cbfcf0 100644 --- a/core/native/src/main/scala/cats/effect/tracing/TracingConstants.scala +++ b/core/native/src/main/scala/cats/effect/tracing/TracingConstants.scala @@ -14,8 +14,7 @@ * limitations under the License. */ -package cats.effect -package tracing +package cats.effect.tracing private[effect] object TracingConstants { @@ -26,5 +25,9 @@ private[effect] object TracingConstants { final val isFullStackTracing: Boolean = stackTracingMode.equalsIgnoreCase("full") - final val isStackTracing = isFullStackTracing || isCachedStackTracing + final val isStackTracing: Boolean = isFullStackTracing || isCachedStackTracing + + // Native doesn't need WASM-specific constants since it's not running in WASM mode + final val WASM_IDENTICAL_FUNCTION: AnyRef = new Object() + final val WASM_IDENTICAL_EVENT: TracingEvent = new TracingEvent.StackTrace() } diff --git a/core/native/src/main/scala/cats/effect/tracing/TracingPlatform.scala b/core/native/src/main/scala/cats/effect/tracing/TracingPlatform.scala index 022de5d9b0..9ae23d9207 100644 --- a/core/native/src/main/scala/cats/effect/tracing/TracingPlatform.scala +++ b/core/native/src/main/scala/cats/effect/tracing/TracingPlatform.scala @@ -16,9 +16,9 @@ package cats.effect.tracing +import cats.effect.kernel.Cont import scala.annotation.nowarn import scala.scalanative.meta.LinktimeInfo - import java.util.concurrent.ConcurrentHashMap private[tracing] abstract class TracingPlatform { self: Tracing.type => @@ -27,22 +27,41 @@ private[tracing] abstract class TracingPlatform { self: Tracing.type => private[this] val cache = new ConcurrentHashMap[Class[?], TracingEvent] - def calculateTracingEvent(key: Any): TracingEvent = + // Native doesn't need WASM-specific handling + private[this] def isWasm: Boolean = false + + def calculateTracingEvent[A](f: Function0[A]): TracingEvent = { + if (LinktimeInfo.debugMode) { + calculateTracingEvent(f.getClass()) + } else null + } + + def calculateTracingEvent[A, B](f: Function1[A, B]): TracingEvent = { if (LinktimeInfo.debugMode) { - if (isCachedStackTracing) { - val cls = key.getClass - val current = cache.get(cls) - if (current eq null) { - val event = buildEvent() - cache.put(cls, event) - event - } else current - } else if (isFullStackTracing) { - buildEvent() - } else { - null - } + calculateTracingEvent(f.getClass()) } else null + } + + def calculateTracingEvent[F[_], A, B](cont: Cont[F, A, B]): TracingEvent = { + if (LinktimeInfo.debugMode) { + calculateTracingEvent(cont.getClass()) + } else null + } + + private[this] def calculateTracingEvent(cls: Class[?]): TracingEvent = { + if (isCachedStackTracing) { + val current = cache.get(cls) + if (current eq null) { + val event = buildEvent() + cache.put(cls, event) + event + } else current + } else if (isFullStackTracing) { + buildEvent() + } else { + null + } + } @nowarn("msg=never used") private[tracing] def applyStackTraceFilter( @@ -52,5 +71,5 @@ private[tracing] abstract class TracingPlatform { self: Tracing.type => isInternalClass(callSiteClassName) private[tracing] def decodeMethodName(name: String): String = name - } + diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 76af998dd5..e33664e645 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -23,8 +23,8 @@ import cats.syntax.all._ import org.typelevel.scalaccompat.annotation._ -import scala.collection.concurrent.TrieMap import scala.scalanative.annotation.alwaysinline +import scala.scalanative.libc.errno._ import scala.scalanative.meta.LinktimeInfo import scala.scalanative.posix.errno._ import scala.scalanative.posix.string._ @@ -34,6 +34,7 @@ import scala.scalanative.unsafe._ import scala.scalanative.unsigned._ import java.io.IOException +import java.util.{Collections, IdentityHashMap, Set} object EpollSystem extends PollingSystem { @@ -66,9 +67,7 @@ object EpollSystem extends PollingSystem { def needsPoll(poller: Poller): Boolean = poller.needsPoll() - def interrupt(targetThread: Thread, targetPoller: Poller): Unit = { - targetPoller.interrupt() - } + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () def metrics(poller: Poller): PollerMetrics = PollerMetrics.noop @@ -99,11 +98,11 @@ object EpollSystem extends PollingSystem { writeMutex: Mutex[IO] ) extends FileDescriptorPollHandle { - @volatile private[this] var readReadyCounter = 0 - @volatile private[this] var readCallback: Either[Throwable, Int] => Unit = null + private[this] var readReadyCounter = 0 + private[this] var readCallback: Either[Throwable, Int] => Unit = null - @volatile private[this] var writeReadyCounter = 0 - @volatile private[this] var writeCallback: Either[Throwable, Int] => Unit = null + private[this] var writeReadyCounter = 0 + private[this] var writeCallback: Either[Throwable, Int] => Unit = null def notify(events: Int): Unit = { if ((events & EPOLLIN) != 0) { @@ -182,46 +181,21 @@ object EpollSystem extends PollingSystem { final class Poller private[EpollSystem] (epfd: Int) { - private[this] val handles: TrieMap[PollHandle, Unit] = - new TrieMap + private[this] val handles: Set[PollHandle] = + Collections.newSetFromMap(new IdentityHashMap) - private[this] val eventsArray = new Array[Byte](epoll_eventTag.size.toInt * MaxEvents) + private[this] val eventsArray = new Array[Byte](sizeof[epoll_event].toInt * MaxEvents) @inline private[this] def events = eventsArray.atUnsafe(0).asInstanceOf[Ptr[epoll_event]] private[this] var readyEventCount: Int = 0 - private[this] val interruptFd: Int = { - val fd = eventfd.eventfd(0, eventfd.EFD_NONBLOCK | eventfd.EFD_CLOEXEC) - if (fd == -1) { - throw new IOException(fromCString(strerror(errno))) - } - val event = stackalloc[Byte](epoll_eventTag.size).asInstanceOf[Ptr[epoll_event]] - event.events = (EPOLLET | EPOLLIN).toUInt - event.data = null - if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, event) != 0) { - unistd.close(fd) + private[EpollSystem] def close(): Unit = + if (unistd.close(epfd) != 0) throw new IOException(fromCString(strerror(errno))) - } - fd - } - - private[EpollSystem] def close(): Unit = { - try { - if (unistd.close(interruptFd) != 0) - throw new IOException(fromCString(strerror(errno))) - } finally { - if (unistd.close(epfd) != 0) - throw new IOException(fromCString(strerror(errno))) - } - } private[EpollSystem] def poll(timeout: Long): PollResult = { val timeoutMillis = if (timeout == -1) -1 else (timeout / 1000000).toInt - val rtn = - if (timeoutMillis == 0) - immediate.epoll_wait(epfd, events, MaxEvents, timeoutMillis) - else - awaiting.epoll_wait(epfd, events, MaxEvents, timeoutMillis) + val rtn = epoll_wait(epfd, events, MaxEvents, timeoutMillis) if (rtn >= 0) { readyEventCount = rtn if (rtn > 0) { @@ -235,35 +209,18 @@ object EpollSystem extends PollingSystem { } private[EpollSystem] def processReadyEvents(): Boolean = { - var fibersRescheduled = false var i = 0 while (i < readyEventCount) { val event = events + i.toLong val handle = fromPtr(event.data) - if (handle ne null) { - handle.notify(event.events.toInt) - fibersRescheduled = true - } else { - val buf = stackalloc[ULong]() - if (unistd.read(interruptFd, buf, sizeof[ULong]) == -1) { - throw new IOException(fromCString(strerror(errno))) - } - } + handle.notify(event.events.toInt) i += 1 } readyEventCount = 0 - fibersRescheduled + true } - private[EpollSystem] def needsPoll(): Boolean = !handles.isEmpty - - private[EpollSystem] def interrupt(): Unit = { - val buf = stackalloc[ULong]() - buf(0) = 1.toULong - if (unistd.write(this.interruptFd, buf, sizeof[ULong]) == -1) { - throw new IOException(fromCString(strerror(errno))) - } - } + private[EpollSystem] def needsPoll(): Boolean = !handles.isEmpty() private[EpollSystem] def register( fd: Int, @@ -272,7 +229,7 @@ object EpollSystem extends PollingSystem { handle: PollHandle, cb: Either[Throwable, (PollHandle, IO[Unit])] => Unit ): Unit = { - val event = stackalloc[Byte](epoll_eventTag.size).asInstanceOf[Ptr[epoll_event]] + val event = stackalloc[epoll_event]() event.events = (EPOLLET | (if (reads) EPOLLIN else 0) | (if (writes) EPOLLOUT else 0)).toUInt event.data = toPtr(handle) @@ -281,7 +238,7 @@ object EpollSystem extends PollingSystem { if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, event) != 0) Left(new IOException(fromCString(strerror(errno)))) else { - handles.put(handle, ()) + handles.add(handle) val remove = IO { handles.remove(handle) if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, null) != 0) @@ -320,18 +277,9 @@ object EpollSystem extends PollingSystem { def epoll_ctl(epfd: Int, op: Int, fd: Int, event: Ptr[epoll_event]): Int = extern - @extern - object awaiting { - @blocking - def epoll_wait(epfd: Int, events: Ptr[epoll_event], maxevents: Int, timeout: Int): Int = - extern - } + def epoll_wait(epfd: Int, events: Ptr[epoll_event], maxevents: Int, timeout: Int): Int = + extern - @extern - object immediate { - def epoll_wait(epfd: Int, events: Ptr[epoll_event], maxevents: Int, timeout: Int): Int = - extern - } } private object epollImplicits { @@ -370,15 +318,4 @@ object EpollSystem extends PollingSystem { .materializeCArrayTag[Byte, Nat.Digit2[Nat._1, Nat._6]] .asInstanceOf[Tag[epoll_event]] } - - @nowarn212 - @extern // eventfd.h - private object eventfd { // TODO: should this be in scala-native? - - final val EFD_CLOEXEC = 0x80000 // TODO: this might be platform-dependent - final val EFD_NONBLOCK = 0x00800 // TODO: this might be platform-dependent - - def eventfd(initval: Int, flags: Int): Int = - extern - } } diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala new file mode 100644 index 0000000000..ac431483fd --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -0,0 +1,185 @@ +/* + * Copyright 2020-2025 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect +package unsafe + +import scala.annotation.tailrec +import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} +import scala.concurrent.duration._ +import scala.scalanative.libc.errno._ +import scala.scalanative.libc.string._ +import scala.scalanative.meta.LinktimeInfo +import scala.scalanative.posix.time._ +import scala.scalanative.posix.timeOps._ +import scala.scalanative.unsafe._ + +import java.util.{ArrayDeque, PriorityQueue} + +private[effect] final class EventLoopExecutorScheduler[P]( + pollEvery: Int, + system: PollingSystem.WithPoller[P]) + extends ExecutionContextExecutor + with Scheduler + with FiberExecutor { + + private[unsafe] val poller: P = system.makePoller() + + private[this] var needsReschedule: Boolean = true + + private[this] val executeQueue: ArrayDeque[Runnable] = new ArrayDeque + private[this] val sleepQueue: PriorityQueue[SleepTask] = new PriorityQueue + + private[this] val noop: Runnable = () => () + + private[this] def scheduleIfNeeded(): Unit = if (needsReschedule) { + ExecutionContext.global.execute(() => loop()) + needsReschedule = false + } + + final def execute(runnable: Runnable): Unit = { + scheduleIfNeeded() + executeQueue.addLast(runnable) + } + + final def sleep(delay: FiniteDuration, task: Runnable): Runnable = + if (delay <= Duration.Zero) { + execute(task) + noop + } else { + scheduleIfNeeded() + val now = monotonicNanos() + val sleepTask = new SleepTask(now + delay.toNanos, task) + sleepQueue.offer(sleepTask) + sleepTask + } + + def reportFailure(t: Throwable): Unit = t.printStackTrace() + + def nowMillis() = System.currentTimeMillis() + + override def nowMicros(): Long = + if (LinktimeInfo.isFreeBSD || LinktimeInfo.isLinux || LinktimeInfo.isMac) { + val ts = stackalloc[timespec]() + if (clock_gettime(CLOCK_REALTIME, ts) != 0) + throw new RuntimeException(fromCString(strerror(errno))) + ts.tv_sec * 1000000 + ts.tv_nsec / 1000 + } else { + super.nowMicros() + } + + def monotonicNanos() = System.nanoTime() + + private[this] def loop(): Unit = { + needsReschedule = false + + var continue = true + + while (continue) { + // execute the timers + val now = monotonicNanos() + while (!sleepQueue.isEmpty() && sleepQueue.peek().at <= now) { + val task = sleepQueue.poll() + try task.runnable.run() + catch { + case t if UnsafeNonFatal(t) => reportFailure(t) + case t: Throwable => IOFiber.onFatalFailure(t) + } + } + + // do up to pollEvery tasks + var i = 0 + while (i < pollEvery && !executeQueue.isEmpty()) { + val runnable = executeQueue.poll() + try runnable.run() + catch { + case t if UnsafeNonFatal(t) => reportFailure(t) + case t: Throwable => IOFiber.onFatalFailure(t) + } + i += 1 + } + + // finally we poll + val timeout = + if (!executeQueue.isEmpty()) + 0 + else if (!sleepQueue.isEmpty()) + Math.max(sleepQueue.peek().at - monotonicNanos(), 0) + else + -1 + + /* + * if `timeout == -1` and there are no remaining events to poll for, we should break the + * loop immediately. This is unfortunate but necessary so that the event loop can yield to + * the Scala Native global `ExecutionContext` which is currently hard-coded into every + * test framework, including MUnit, specs2, and Weaver. + */ + if (system.needsPoll(poller) || timeout != -1) { + @tailrec def loop(result: PollResult): Unit = + if (result ne PollResult.Interrupted) { + system.processReadyEvents(poller) + if (result eq PollResult.Incomplete) loop(system.poll(poller, 0)) + } + + loop(system.poll(poller, timeout)) + } + + continue = !executeQueue.isEmpty() || !sleepQueue.isEmpty() || system.needsPoll(poller) + } + + needsReschedule = true + } + + private[this] final class SleepTask( + val at: Long, + val runnable: Runnable + ) extends Runnable + with Comparable[SleepTask] { + + def run(): Unit = { + sleepQueue.remove(this) + () + } + + def compareTo(that: SleepTask): Int = + java.lang.Long.compare(this.at, that.at) + } + + def shutdown(): Unit = system.close() + + def liveTraces(): Map[IOFiber[?], Trace] = { + val builder = Map.newBuilder[IOFiber[?], Trace] + executeQueue.forEach { + case f: IOFiber[?] => builder += f -> f.captureTrace() + case _ => () + } + builder.result() + } + +} + +private object EventLoopExecutorScheduler { + lazy val global = { + val system = + if (LinktimeInfo.isLinux) + EpollSystem + else if (LinktimeInfo.isMac) + KqueueSystem + else + SleepSystem + new EventLoopExecutorScheduler[system.Poller](64, system) + } +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala new file mode 100644 index 0000000000..5eede014d5 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2020-2025 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect +package unsafe + +import scala.concurrent.ExecutionContext + +private[effect] abstract class FiberMonitorPlatform { + def apply(compute: ExecutionContext): FiberMonitor = { + if (false) { // LinktimeInfo.debugMode && LinktimeInfo.isWeakReferenceSupported + if (compute.isInstanceOf[EventLoopExecutorScheduler[?]]) { + val loop = compute.asInstanceOf[EventLoopExecutorScheduler[?]] + new FiberMonitorImpl(loop) + } else { + new FiberMonitorImpl(null) + } + } else { + new NoOpFiberMonitor() + } + } + +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala index c98bcbc8d1..1f867b8c5e 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala @@ -31,32 +31,23 @@ private[unsafe] abstract class IORuntimeBuilderPlatform { self: IORuntimeBuilder this } - // TODO unify this with the defaults in IORuntime.global and IOApp protected def platformSpecificBuild: IORuntime = { - val (compute, poller, computeShutdown) = + val defaultShutdown: () => Unit = () => () + lazy val (loop, poller, loopDown) = IORuntime.createEventLoop( + customPollingSystem.getOrElse(IORuntime.createDefaultPollingSystem()) + ) + val (compute, pollers, computeShutdown) = customCompute - .map { - case (c, s) => - (c, Nil, s) - } - .getOrElse { - val (c, p, s) = - IORuntime.createWorkStealingComputeThreadPool( - pollingSystem = - customPollingSystem.getOrElse(IORuntime.createDefaultPollingSystem()), - reportFailure = failureReporter - ) - (c, List(p), s) - } - val xformedCompute = computeTransform(compute) - - val (scheduler, schedulerShutdown) = xformedCompute match { - case sched: Scheduler => customScheduler.getOrElse((sched, () => ())) - case _ => customScheduler.getOrElse(IORuntime.createDefaultScheduler()) - } - - val (blocking, blockingShutdown) = - customBlocking.getOrElse(IORuntime.createDefaultBlockingExecutionContext()) + .map { case (c, s) => (c, Nil, s) } + .getOrElse( + ( + loop, + List(poller), + loopDown + )) + val (blocking, blockingShutdown) = customBlocking.getOrElse((compute, defaultShutdown)) + val (scheduler, schedulerShutdown) = + customScheduler.getOrElse((loop, defaultShutdown)) val shutdown = () => { computeShutdown() blockingShutdown() @@ -70,7 +61,7 @@ private[unsafe] abstract class IORuntimeBuilderPlatform { self: IORuntimeBuilder computeTransform(compute), blockingTransform(blocking), scheduler, - poller ::: extraPollers.map(_._1), + pollers ::: extraPollers.map(_._1), shutdown, runtimeConfig ) diff --git a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index bbca1e2aec..d89d7930e0 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -16,78 +16,38 @@ package cats.effect.unsafe -import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} -import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext import scala.scalanative.meta.LinktimeInfo -import java.util.concurrent.Executors -import java.util.concurrent.atomic.AtomicInteger - private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type => - private[this] final val DefaultBlockerPrefix = "io-compute-blocker" - - def createWorkStealingComputeThreadPool( - threads: Int = Math.max(2, Runtime.getRuntime().availableProcessors()), - threadPrefix: String = "io-compute", - blockerThreadPrefix: String = DefaultBlockerPrefix, - runtimeBlockingExpiration: Duration = 60.seconds, - reportFailure: Throwable => Unit = _.printStackTrace(), - blockedThreadDetectionEnabled: Boolean = false, - shutdownTimeout: Duration = 1.second, - pollingSystem: PollingSystem = createDefaultPollingSystem(), - uncaughtExceptionHandler: Thread.UncaughtExceptionHandler = (_, ex) => - ex.printStackTrace() - ): (ExecutionContextExecutor with Scheduler, pollingSystem.Api, () => Unit) = { + def defaultComputeExecutionContext: ExecutionContext = EventLoopExecutorScheduler.global - val threadPool = - new WorkStealingThreadPool[pollingSystem.Poller]( - threads, - threadPrefix, - blockerThreadPrefix, - runtimeBlockingExpiration, - blockedThreadDetectionEnabled && (threads > 1), - shutdownTimeout, - pollingSystem, - reportFailure, - uncaughtExceptionHandler - ) + def defaultScheduler: Scheduler = EventLoopExecutorScheduler.global - (threadPool, pollingSystem.makeApi(threadPool), { () => threadPool.shutdown() }) + def createEventLoop( + system: PollingSystem + ): (ExecutionContext with Scheduler, system.Api, () => Unit) = { + val loop = new EventLoopExecutorScheduler[system.Poller](64, system) + val poller = loop.poller + val api = system.makeApi( + new UnsealedPollingContext[system.Poller] { + def accessPoller(cb: system.Poller => Unit) = cb(poller) + def ownPoller(poller: system.Poller) = true + } + ) + (loop, api, () => loop.shutdown()) } - def createDefaultScheduler(threadPrefix: String = "io-scheduler"): (Scheduler, () => Unit) = - Scheduler.createDefaultScheduler(threadPrefix) - - def createDefaultPollingSystem(): PollingSystem = { + def createDefaultPollingSystem(): PollingSystem = if (LinktimeInfo.isLinux) EpollSystem else if (LinktimeInfo.isMac) KqueueSystem else SleepSystem - } - def createDefaultBlockingExecutionContext( - threadPrefix: String = "io-blocking" - ): (ExecutionContext, () => Unit) = - createDefaultBlockingExecutionContext(threadPrefix, _.printStackTrace()) - - private[effect] def createDefaultBlockingExecutionContext( - threadPrefix: String, - reportFailure: Throwable => Unit - ): (ExecutionContext, () => Unit) = { - val threadCount = new AtomicInteger(0) - val executor = Executors.newCachedThreadPool { (r: Runnable) => - val t = new Thread(r) - t.setName(s"${threadPrefix}-${threadCount.getAndIncrement()}") - t.setDaemon(true) - t - } - (ExecutionContext.fromExecutor(executor, reportFailure), { () => executor.shutdown() }) - } - - @volatile private[this] var _global: IORuntime = null + private[this] var _global: IORuntime = null private[effect] def installGlobal(global: => IORuntime): Boolean = { if (_global == null) { @@ -104,15 +64,17 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type def global: IORuntime = { if (_global == null) { installGlobal { - val (compute, poller, computeDown) = createWorkStealingComputeThreadPool() - val (blocking, blockingDown) = createDefaultBlockingExecutionContext() - val shutdown = () => { - computeDown() - blockingDown() - resetGlobal() - } - - IORuntime(compute, blocking, compute, List(poller), shutdown, IORuntimeConfig()) + val (loop, poller, loopDown) = createEventLoop(createDefaultPollingSystem()) + IORuntime( + loop, + loop, + loop, + List(poller), + () => { + loopDown() + resetGlobal() + }, + IORuntimeConfig()) } () } diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 03a60b86fd..5b835f590c 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -23,7 +23,8 @@ import cats.syntax.all._ import org.typelevel.scalaccompat.annotation._ -import scala.collection.concurrent.TrieMap +import scala.collection.mutable.LongMap +import scala.scalanative.libc.errno._ import scala.scalanative.posix.errno._ import scala.scalanative.posix.string._ import scala.scalanative.posix.time._ @@ -66,8 +67,7 @@ object KqueueSystem extends PollingSystem { def needsPoll(poller: Poller): Boolean = poller.needsPoll() - def interrupt(targetThread: Thread, targetPoller: Poller): Unit = - targetPoller.interrupt() + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () def metrics(poller: Poller): PollerMetrics = PollerMetrics.noop @@ -140,25 +140,13 @@ object KqueueSystem extends PollingSystem { final class Poller private[KqueueSystem] (kqfd: Int) { - private[this] val buffer = new Array[Byte](MaxEvents * sizeof_kevent64_s) + private[this] val buffer = new Array[Byte](sizeof[kevent64_s].toInt * MaxEvents) @inline private[this] def eventlist = buffer.atUnsafe(0).asInstanceOf[Ptr[kevent64_s]] private[this] var changeCount = 0 private[this] var readyEventCount = 0 - private[this] val callbacks = new TrieMap[Long, Either[Throwable, Unit] => Unit]() - - { - val event = eventlist - - event.ident = 0.toUInt - event.filter = EVFILT_USER - event.flags = (EV_ADD | EV_CLEAR).toUShort - event.fflags = 0.toUInt - - val rtn = immediate.kevent64(kqfd, event, 1, null, 0, KEVENT_FLAG_IMMEDIATE.toUInt, null) - if (rtn < 0) throw new IOException(fromCString(strerror(errno))) - } + private[this] val callbacks = new LongMap[Either[Throwable, Unit] => Unit]() private[KqueueSystem] def evSet( ident: Int, @@ -168,7 +156,7 @@ object KqueueSystem extends PollingSystem { ): Unit = { val event = eventlist + changeCount.toLong - event.ident = ident.toUSize + event.ident = ident.toULong event.filter = filter event.flags = (flags.toInt | EV_ONESHOT).toUShort @@ -191,35 +179,23 @@ object KqueueSystem extends PollingSystem { val timeoutSpec = if (timeout <= 0) null else { - val ts = stackalloc[timespec](1) - ts.tv_sec = (timeout / 1000000000).toCSSize - ts.tv_nsec = (timeout % 1000000000).toCSSize + val ts = stackalloc[timespec]() + ts.tv_sec = timeout / 1000000000 + ts.tv_nsec = timeout % 1000000000 ts } val flags = if (timeout == 0) KEVENT_FLAG_IMMEDIATE else KEVENT_FLAG_NONE - val rtn = - if ((flags & KEVENT_FLAG_IMMEDIATE) != 0) - immediate.kevent64( - kqfd, - eventlist, - changeCount, - eventlist, - MaxEvents, - flags.toUInt, - null - ) - else - awaiting.kevent64( - kqfd, - eventlist, - changeCount, - eventlist, - MaxEvents, - flags.toUInt, - timeoutSpec - ) + val rtn = kevent64( + kqfd, + eventlist, + changeCount, + eventlist, + MaxEvents, + flags.toUInt, + timeoutSpec + ) changeCount = 0 if (rtn >= 0) { @@ -235,49 +211,29 @@ object KqueueSystem extends PollingSystem { } private[KqueueSystem] def processReadyEvents(): Boolean = { - var fibersRescheduled = false var i = 0 var event = eventlist while (i < readyEventCount) { - val cb = - if (event.filter == EVFILT_USER) - null // we just ignore the interrupt, since its whole purpose is to awaken the poller - else { - val kevent = encodeKevent(event.ident.toInt, event.filter) - val cb = callbacks.getOrElse(kevent, null) - callbacks -= kevent - cb - } + val kevent = encodeKevent(event.ident.toInt, event.filter) + val cb = callbacks.getOrNull(kevent) + callbacks -= kevent - if (cb ne null) { + if (cb ne null) cb( if ((event.flags.toLong & EV_ERROR) != 0) Left(new IOException(fromCString(strerror(event.data.toInt)))) else Either.unit ) - fibersRescheduled = true - } i += 1 event += 1 } readyEventCount = 0 - fibersRescheduled + true } private[KqueueSystem] def needsPoll(): Boolean = changeCount > 0 || callbacks.nonEmpty - - private[KqueueSystem] def interrupt(): Unit = { - val event = stackalloc[Byte](sizeof_kevent64_s).asInstanceOf[Ptr[kevent64_s]] - event.ident = 0.toUInt - event.filter = EVFILT_USER - event.flags = 0.toUShort - event.fflags = NOTE_TRIGGER.toUInt - - val rtn = immediate.kevent64(kqfd, event, 1, null, 0, KEVENT_FLAG_IMMEDIATE.toUInt, null) - if (rtn < 0) throw new IOException(fromCString(strerror(errno))) - } } @nowarn212 @@ -287,7 +243,6 @@ object KqueueSystem extends PollingSystem { final val EVFILT_READ = -1 final val EVFILT_WRITE = -2 - final val EVFILT_USER = -10 final val KEVENT_FLAG_NONE = 0x000000 final val KEVENT_FLAG_IMMEDIATE = 0x000001 @@ -298,41 +253,20 @@ object KqueueSystem extends PollingSystem { final val EV_CLEAR = 0x0020 final val EV_ERROR = 0x4000 - final val NOTE_TRIGGER = 0x01000000 - type kevent64_s - final val sizeof_kevent64_s = 48 def kqueue(): CInt = extern - @extern - object awaiting { - - @blocking - def kevent64( - kq: CInt, - changelist: Ptr[kevent64_s], - nchanges: CInt, - eventlist: Ptr[kevent64_s], - nevents: CInt, - flags: CUnsignedInt, - timeout: Ptr[timespec] - ): CInt = extern - } + def kevent64( + kq: CInt, + changelist: Ptr[kevent64_s], + nchanges: CInt, + eventlist: Ptr[kevent64_s], + nevents: CInt, + flags: CUnsignedInt, + timeout: Ptr[timespec] + ): CInt = extern - @extern - object immediate { - - def kevent64( - kq: CInt, - changelist: Ptr[kevent64_s], - nchanges: CInt, - eventlist: Ptr[kevent64_s], - nevents: CInt, - flags: CUnsignedInt, - timeout: Ptr[timespec] - ): CInt = extern - } } private object eventImplicits { @@ -350,10 +284,6 @@ object KqueueSystem extends PollingSystem { def flags_=(flags: CUnsignedShort): Unit = !(kevent64_s.asInstanceOf[Ptr[CUnsignedShort]] + 5) = flags - def fflags: CUnsignedInt = !(kevent64_s.asInstanceOf[Ptr[CUnsignedInt]] + 3) - def fflags_=(fflags: CUnsignedInt): Unit = - !(kevent64_s.asInstanceOf[Ptr[CUnsignedInt]] + 3) = fflags - def data: CLong = !(kevent64_s.asInstanceOf[Ptr[CLong]] + 2) def udata: Ptr[Byte] = !(kevent64_s.asInstanceOf[Ptr[Ptr[Byte]]] + 3) diff --git a/core/native/src/main/scala/cats/effect/unsafe/LocalQueueConstants.scala b/core/native/src/main/scala/cats/effect/unsafe/LocalQueueConstants.scala deleted file mode 100644 index 0d1a67a381..0000000000 --- a/core/native/src/main/scala/cats/effect/unsafe/LocalQueueConstants.scala +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2020-2025 Typelevel - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package cats.effect.unsafe - -// a native-specific mirror of LocalQueueConstants.java -private object LocalQueueConstants { - - /** - * Fixed capacity of the [[cats.effect.unsafe.LocalQueue]] implementation, empirically - * determined to provide a balance between memory footprint and enough headroom in the face of - * bursty workloads which spawn a lot of fibers in a short period of time. - * - *

Must be a power of 2. - */ - final val LocalQueueCapacity = 256 - - /** - * Bitmask used for indexing into the circular buffer. - */ - final val LocalQueueCapacityMask = LocalQueueCapacity - 1 - - /** - * Half of the local queue capacity. - */ - final val HalfLocalQueueCapacity = LocalQueueCapacity / 2 - - /** - * Spillover batch size. The runtime relies on the assumption that this number fully divides - * `HalfLocalQueueCapacity`. - */ - final val SpilloverBatchSize = 32 - - /** - * Number of batches that fit into one half of the local queue. - */ - final val BatchesInHalfQueueCapacity = HalfLocalQueueCapacity / SpilloverBatchSize - - /** - * The maximum current capacity of the local queue which can still accept a full batch to be - * added to the queue (remembering that one fiber from the batch is executed by directly and - * not enqueued on the local queue). - */ - final val LocalQueueCapacityMinusBatch = LocalQueueCapacity - SpilloverBatchSize + 1 - - /** - * Bitmask used to extract the 16 least significant bits of a 32 bit integer value. - */ - final val UnsignedShortMask = (1 << 16) - 1 -} diff --git a/core/native/src/main/scala/cats/effect/unsafe/LocalQueuePadding.scala b/core/native/src/main/scala/cats/effect/unsafe/LocalQueuePadding.scala deleted file mode 100644 index c0573f6f75..0000000000 --- a/core/native/src/main/scala/cats/effect/unsafe/LocalQueuePadding.scala +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2020-2025 Typelevel - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package cats.effect.unsafe - -import scala.scalanative.libc.stdatomic._ -import scala.scalanative.libc.stdatomic.memory_order.memory_order_release -import scala.scalanative.runtime.{fromRawPtr, Intrinsics} - -// native mirror of LocalQueue.java -private class Head { - - /** - * The head of the queue. - * - *

Concurrently updated by many [[WorkerThread]] s. - * - *

Conceptually, it is a concatenation of two unsigned 16 bit values. Since the capacity of - * the local queue is less than (2^16 - 1), the extra unused values are used to distinguish - * between the case where the queue is empty (`head` == `tail`) and (`head` - `tail` == - * [[LocalQueueConstants.LocalQueueCapacity]]), which is an important distinction for other - * [[WorkerThread]] s trying to steal work from the queue. - * - *

The least significant 16 bits of the integer value represent the ''real'' value of the - * head, pointing to the next [[cats.effect.IOFiber]] instance to be dequeued from the queue. - * - *

The most significant 16 bits of the integer value represent the ''steal'' tag of the - * head. This value is altered by another [[WorkerThread]] which has managed to win the race - * and become the exclusive ''stealer'' of the queue. During the period in which the ''steal'' - * tag differs from the ''real'' value, no other [[WorkerThread]] can steal from the queue, - * and the owner [[WorkerThread]] also takes special care to not mangle the ''steal'' tag set - * by the ''stealer''. The stealing [[WorkerThread]] is free to transfer half of the available - * [[cats.effect.IOFiber]] object references from this queue into its own [[LocalQueue]] - * during this period, making sure to undo the changes to the ''steal'' tag of the head on - * completion, action which ultimately signals that stealing is finished. - */ - @volatile - protected var head: Int = 0 - - { - // prevent unused warnings - head = 0 - } -} - -private object Head { - private[unsafe] object updater { - - def get(obj: Head): Int = - fromRawPtr[atomic_int](Intrinsics.classFieldRawPtr[Head](obj, "head")).atomic.load() - - def compareAndSet(obj: Head, oldHd: Int, newHd: Int): Boolean = - fromRawPtr[atomic_int](Intrinsics.classFieldRawPtr[Head](obj, "head")) - .atomic - .compareExchangeStrong(oldHd, newHd) - } -} - -private class Tail extends Head { - - /** - * The tail of the queue. - * - *

Only ever updated by the owner [[WorkerThread]], but also read by other threads to - * determine the current size of the queue, for work stealing purposes. Denotes the next - * available free slot in the `buffer` array. - * - *

Conceptually, it is an unsigned 16 bit value (the most significant 16 bits of the - * integer value are ignored in most operations). - */ - protected var tail: Int = 0 - - @volatile - private[this] var tailPublisher: Int = 0 - - { - // prevent unused warnings - tailPublisher = 0 - } -} - -private object Tail { - - private[unsafe] object updater { - - def get(obj: Tail): Int = - fromRawPtr[atomic_int](Intrinsics.classFieldRawPtr[Tail](obj, "tail")).atomic.load() - - def lazySet(obj: Tail, newValue: Int): Unit = - fromRawPtr[atomic_int](Intrinsics.classFieldRawPtr[Tail](obj, "tail")) - .atomic - .store(newValue, memory_order_release) - } -} - -private class LocalQueuePadding extends Tail diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala new file mode 100644 index 0000000000..8f87e9a186 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala @@ -0,0 +1,82 @@ +/* + * Copyright 2020-2025 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect +package unsafe + +import cats.effect.unsafe.metrics.PollerMetrics + +import scala.concurrent.ExecutionContextExecutor +import scala.concurrent.duration._ + +@deprecated("Use default runtime with a custom PollingSystem", "3.6.0") +abstract class PollingExecutorScheduler(pollEvery: Int) + extends ExecutionContextExecutor + with Scheduler { outer => + + private[this] val loop = new EventLoopExecutorScheduler( + pollEvery, + new PollingSystem { + type Api = outer.type + type Poller = outer.type + private[this] var needsPoll = true + def close(): Unit = () + def makeApi(ctx: PollingContext[Poller]): Api = outer + def makePoller(): Poller = outer + def closePoller(poller: Poller): Unit = () + def poll(poller: Poller, nanos: Long): PollResult = { + needsPoll = + if (nanos == -1) + poller.poll(Duration.Inf) + else + poller.poll(nanos.nanos) + PollResult.Complete + } + def processReadyEvents(poller: Poller): Boolean = true + def needsPoll(poller: Poller) = needsPoll + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () + def metrics(poller: Poller): PollerMetrics = PollerMetrics.noop + } + ) + + final def execute(runnable: Runnable): Unit = + loop.execute(runnable) + + final def sleep(delay: FiniteDuration, task: Runnable): Runnable = + loop.sleep(delay, task) + + def reportFailure(t: Throwable): Unit = loop.reportFailure(t) + + def nowMillis() = loop.nowMillis() + + override def nowMicros(): Long = loop.nowMicros() + + def monotonicNanos() = loop.monotonicNanos() + + /** + * @param timeout + * the maximum duration for which to block. ''However'', if `timeout == Inf` and there are + * no remaining events to poll for, this method should return `false` immediately. This is + * unfortunate but necessary so that this `ExecutionContext` can yield to the Scala Native + * global `ExecutionContext` which is currently hard-coded into every test framework, + * including JUnit, MUnit, and specs2. + * + * @return + * whether poll should be called again (i.e., there are more events to be polled) + */ + protected def poll(timeout: Duration): Boolean + +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala new file mode 100644 index 0000000000..809402bd52 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala @@ -0,0 +1,24 @@ +/* + * Copyright 2020-2025 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect.unsafe + +private[unsafe] abstract class SchedulerCompanionPlatform { this: Scheduler.type => + + def createDefaultScheduler(): (Scheduler, () => Unit) = + (EventLoopExecutorScheduler.global, () => ()) + +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala new file mode 100644 index 0000000000..a01eed4bd9 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -0,0 +1,49 @@ +/* + * Copyright 2020-2025 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect +package unsafe + +import cats.effect.unsafe.metrics.PollerMetrics + +object SleepSystem extends PollingSystem { + + type Api = AnyRef + type Poller = AnyRef + + def close(): Unit = () + + def makeApi(ctx: PollingContext[Poller]): Api = this + + def makePoller(): Poller = this + + def closePoller(poller: Poller): Unit = () + + def poll(poller: Poller, nanos: Long): PollResult = { + if (nanos > 0) + Thread.sleep(nanos / 1000000, (nanos % 1000000).toInt) + PollResult.Interrupted + } + + def processReadyEvents(poller: Poller): Boolean = false + + def needsPoll(poller: Poller): Boolean = false + + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () + + def metrics(poller: Poller): PollerMetrics = PollerMetrics.noop + +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPoolConstants.scala b/core/native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPoolConstants.scala deleted file mode 100644 index 3ac504b99b..0000000000 --- a/core/native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPoolConstants.scala +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2020-2025 Typelevel - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package cats.effect.unsafe - -// a native-specific mirror of WorkStealingThreadPoolConstants.java -private object WorkStealingThreadPoolConstants { - - /** - * The number of unparked threads is encoded as an unsigned 16 bit number in the 16 most - * significant bits of a 32 bit integer. - */ - final val UnparkShift = 16 - - /** - * Constant used when parking a thread which was not searching for work. - */ - final val DeltaNotSearching = 1 << UnparkShift - - /** - * Constant used when parking a thread which was previously searching for work and also when - * unparking any worker thread. - */ - final val DeltaSearching = DeltaNotSearching | 1 - - /** - * The number of threads currently searching for work is encoded as an unsigned 16 bit number - * in the 16 least significant bits of a 32 bit integer. Used for extracting the number of - * searching threads. - */ - final val SearchMask = (1 << UnparkShift) - 1 - - /** - * Used for extracting the number of unparked threads. - */ - final val UnparkMask = ~SearchMask - - /** - * Used for checking sources of external work every few iterations. - */ - final val ExternalWorkTicks = 32 - - final val ExternalWorkTicksMask = ExternalWorkTicks - 1 - - final val PollingTicks = 2 * ExternalWorkTicks - - final val PollingTicksMask = PollingTicks - 1 - -} diff --git a/core/native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPoolPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPoolPlatform.scala deleted file mode 100644 index de0919af37..0000000000 --- a/core/native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPoolPlatform.scala +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2020-2025 Typelevel - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package cats.effect -package unsafe - -import scala.scalanative.libc.errno._ -import scala.scalanative.libc.string._ -import scala.scalanative.meta.LinktimeInfo -import scala.scalanative.posix.time._ -import scala.scalanative.posix.timeOps._ -import scala.scalanative.unsafe._ - -trait WorkStealingThreadPoolPlatform[P <: AnyRef] extends Scheduler { - this: WorkStealingThreadPool[P] => - - // TODO cargo culted from EventLoopExecutorScheduler.scala - override def nowMicros(): Long = - if (LinktimeInfo.isFreeBSD || LinktimeInfo.isLinux || LinktimeInfo.isMac) { - val ts = stackalloc[timespec]() - if (clock_gettime(CLOCK_REALTIME, ts) != 0) - throw new RuntimeException(fromCString(strerror(errno))) - ts.tv_sec.toLong * 1000000 + ts.tv_nsec.toLong / 1000 - } else { - super.nowMicros() - } -} diff --git a/core/shared/src/main/scala/cats/effect/SyncIO.scala b/core/shared/src/main/scala/cats/effect/SyncIO.scala index 4e3a568ccc..d2c3a3ce33 100644 --- a/core/shared/src/main/scala/cats/effect/SyncIO.scala +++ b/core/shared/src/main/scala/cats/effect/SyncIO.scala @@ -18,7 +18,6 @@ package cats.effect import cats.{Align, Eval, Functor, Now, Show, StackSafeMonad} import cats.data.Ior -import cats.effect.std.SecureRandom import cats.effect.syntax.monadCancel._ import cats.effect.unsafe.UnsafeNonFatal import cats.kernel.{Monoid, Semigroup} @@ -498,13 +497,6 @@ object SyncIO extends SyncIOCompanionPlatform with SyncIOLowPriorityImplicits { val realTime: SyncIO[FiniteDuration] = RealTime - /** - * Creates a new instance of SecureRandom in a SyncIO context. This is cryptographically - * secure and thread-safe. - */ - implicit lazy val secureRandom: SecureRandom[SyncIO] = - SecureRandom.unsafeJavaSecuritySecureRandom[SyncIO]() - private[this] val _unit: SyncIO[Unit] = Pure(()) diff --git a/core/shared/src/main/scala/cats/effect/tracing/TracingEvent.scala b/core/shared/src/main/scala/cats/effect/tracing/TracingEvent.scala index f7e9302e61..b05fcbbbcf 100644 --- a/core/shared/src/main/scala/cats/effect/tracing/TracingEvent.scala +++ b/core/shared/src/main/scala/cats/effect/tracing/TracingEvent.scala @@ -16,8 +16,25 @@ package cats.effect.tracing -private[effect] sealed trait TracingEvent extends Serializable +private[effect] sealed trait TracingEvent extends Serializable { + def getStackTrace(): Array[StackTraceElement] +} private[effect] object TracingEvent { - final class StackTrace extends Throwable with TracingEvent + final class StackTrace extends Throwable with TracingEvent { + override def getStackTrace(): Array[StackTraceElement] = super.getStackTrace() + } + + final class WasmTrace private[tracing] ( + val stackTrace: Array[StackTraceElement], + val isIdentical: Boolean = false + ) extends TracingEvent { + override def getStackTrace(): Array[StackTraceElement] = stackTrace + override def toString: String = s"WasmTrace(${stackTrace.mkString(",")})" + } + + object WasmTrace { + def apply(stackTrace: Array[StackTraceElement], isIdentical: Boolean = false): WasmTrace = + new WasmTrace(stackTrace, isIdentical) + } } diff --git a/docs/core/scala-native.md b/docs/core/scala-native.md index 327e754683..7ebab47651 100644 --- a/docs/core/scala-native.md +++ b/docs/core/scala-native.md @@ -62,4 +62,4 @@ For more in-depth details, see the [article](https://typelevel.org/blog/2022/09/ ## Showcase projects -- [scala-native-ember-example](https://github.com/ChristopherDavenport/scala-native-ember-example) shows how you can run the [http4s](https://github.com/http4s/http4s) server as a native binary +- [scala-native-ember-example](https://github.com/ChristopherDavenport/scala-native-ember-example) shows how you can run the [http4s](https://github.com/http4s/http4s) server as a native binary \ No newline at end of file diff --git a/docs/core/test-runtime.md b/docs/core/test-runtime.md index 2072f480e0..13a7378a98 100644 --- a/docs/core/test-runtime.md +++ b/docs/core/test-runtime.md @@ -244,4 +244,4 @@ TestControl.execute(IO.sleep(1.second) >> IO.realTime) flatMap { control => } ``` -Now everything behaves as desired: the first `tick` sequences the `IO.sleep` action, after which we advance the clock by `1.second`, and then we `tick` a second time, sequencing the `IO.realTime` action and returning the result of `1.second` (note that `TestControl` internal `realTime` and `monotonic` clocks are identical and always start at `0.nanos`). +Now everything behaves as desired: the first `tick` sequences the `IO.sleep` action, after which we advance the clock by `1.second`, and then we `tick` a second time, sequencing the `IO.realTime` action and returning the result of `1.second` (note that `TestControl` internal `realTime` and `monotonic` clocks are identical and always start at `0.nanos`). \ No newline at end of file diff --git a/docs/faq.md b/docs/faq.md index d8bb121847..a166a79741 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -257,4 +257,3 @@ Working version of the above example (emitting no warnings): .flatMap(_ => IO(input)) } ``` - diff --git a/docs/getting-started.md b/docs/getting-started.md index 36bc14f59e..a99a138185 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -187,4 +187,4 @@ class ExampleSpec extends Specification with CatsEffect { ### ScalaCheck -Special support is available for ScalaCheck properties in the form of the [ScalaCheck Effect](https://github.com/typelevel/scalacheck-effect) project. This library makes it possible to write properties using a special `forAllF` syntax which evaluate entirely within `IO` without blocking threads. +Special support is available for ScalaCheck properties in the form of the [ScalaCheck Effect](https://github.com/typelevel/scalacheck-effect) project. This library makes it possible to write properties using a special `forAllF` syntax which evaluate entirely within `IO` without blocking threads. \ No newline at end of file diff --git a/docs/migration-guide.md b/docs/migration-guide.md index 827ce60ebb..3dd26a7c7e 100644 --- a/docs/migration-guide.md +++ b/docs/migration-guide.md @@ -737,4 +737,4 @@ In general you can assume that binding on the value of type `F[A]` contained in that the outcome has been constructed as `Outcome.Succeeded(IO.pure(result))`. [sbt]: https://scala-sbt.org -[scalafix]: https://scalacenter.github.io/scalafix/ +[scalafix]: https://scalacenter.github.io/scalafix/ \ No newline at end of file diff --git a/docs/std/mapref.md b/docs/std/mapref.md index 321a6985bc..3b5fd6e76d 100644 --- a/docs/std/mapref.md +++ b/docs/std/mapref.md @@ -56,4 +56,4 @@ object DatabaseClient { } } } -``` +``` \ No newline at end of file diff --git a/docs/std/ref.md b/docs/std/ref.md index 296646cdbd..a5df3a7738 100644 --- a/docs/std/ref.md +++ b/docs/std/ref.md @@ -79,4 +79,4 @@ Worker #2 >> 0 Worker #2 >> 3 Worker #1 >> 1 Worker #3 >> 2 -``` +``` \ No newline at end of file diff --git a/docs/tutorial.md b/docs/tutorial.md index e9c112888a..4749bf0da4 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1235,4 +1235,4 @@ you will notice your code is a simplified version of cats-effect own `Queue`! With all this we have covered a good deal of what cats-effect has to offer (but not all!). Now you are ready to use to create code that operate side effects in -a purely functional manner. Enjoy the ride! +a purely functional manner. Enjoy the ride! \ No newline at end of file diff --git a/example/js/src/main/scala/cats/effect/example/Example.scala b/example/js-native/src/main/scala/cats/effect/example/Example.scala similarity index 100% rename from example/js/src/main/scala/cats/effect/example/Example.scala rename to example/js-native/src/main/scala/cats/effect/example/Example.scala diff --git a/example/test-native.sh b/example/test-native.sh index 1c40e54b35..79b6318fa5 100755 --- a/example/test-native.sh +++ b/example/test-native.sh @@ -15,7 +15,7 @@ expected=$(mktemp) cd example/native/target/scala-$(echo $1 | sed -E 's/^(2\.[0-9]+)\.[0-9]+$/\1/')/ set +e -./cats-effect-example left right > $output +./cats-effect-example-out left right > $output result=$? set -e diff --git a/kernel/js/src/main/scala/cats/effect/kernel/SyncRef.scala b/kernel/js-native/src/main/scala/cats/effect/kernel/SyncRef.scala similarity index 100% rename from kernel/js/src/main/scala/cats/effect/kernel/SyncRef.scala rename to kernel/js-native/src/main/scala/cats/effect/kernel/SyncRef.scala diff --git a/kernel/jvm-native/src/main/scala/cats/effect/kernel/ClockPlatform.scala b/kernel/jvm-native/src/main/scala/cats/effect/kernel/ClockPlatform.scala index be88727fbc..73e1020578 100644 --- a/kernel/jvm-native/src/main/scala/cats/effect/kernel/ClockPlatform.scala +++ b/kernel/jvm-native/src/main/scala/cats/effect/kernel/ClockPlatform.scala @@ -16,13 +16,10 @@ package cats.effect.kernel -import java.time.{Instant, ZoneOffset, ZonedDateTime} +import java.time.Instant private[effect] trait ClockPlatform[F[_]] extends Serializable { self: Clock[F] => def realTimeInstant: F[Instant] = { self.applicative.map(self.realTime)(d => Instant.EPOCH.plusNanos(d.toNanos)) } - - def realTimeZonedDateTime: F[ZonedDateTime] = - self.applicative.map(realTimeInstant)(d => ZonedDateTime.ofInstant(d, ZoneOffset.UTC)) } diff --git a/kernel/jvm-native/src/main/scala/cats/effect/kernel/SyncRef.scala b/kernel/jvm/src/main/scala/cats/effect/kernel/SyncRef.scala similarity index 100% rename from kernel/jvm-native/src/main/scala/cats/effect/kernel/SyncRef.scala rename to kernel/jvm/src/main/scala/cats/effect/kernel/SyncRef.scala diff --git a/package.json b/package.json index 5e16dc247e..722d34d290 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "private": true, + "type": "module", "devDependencies": { "source-map-support": "^0.5.19" } diff --git a/project/CI.scala b/project/CI.scala index dbac695a9a..9f545af7d7 100644 --- a/project/CI.scala +++ b/project/CI.scala @@ -23,25 +23,24 @@ sealed abstract class CI( scaladoc: Boolean, suffixCommands: List[String]) { - val commands: List[String] = - (List( - s"project $rootProject", - jsEnv.fold("")(env => s"set Global / useJSEnv := JSEnv.$env"), - "headerCheck", - "scalafmtSbtCheck", - "scalafmtCheckAll", - "javafmtCheckAll", - "clean" - ) ++ testCommands ++ List( - jsEnv.fold("")(_ => s"set Global / useJSEnv := JSEnv.NodeJS"), - if (mimaReport) "mimaReportBinaryIssues" else "", - if (scaladoc) "doc" else "" - )).filter(_.nonEmpty) ++ suffixCommands + override val toString: String = { + val commands = + (List( + s"project $rootProject", + jsEnv.fold("")(env => s"set Global / useJSEnv := JSEnv.$env"), + "headerCheck", + "scalafmtSbtCheck", + "scalafmtCheckAll", + "javafmtCheckAll", + "clean" + ) ++ testCommands ++ List( + jsEnv.fold("")(_ => s"set Global / useJSEnv := JSEnv.NodeJS"), + if (mimaReport) "mimaReportBinaryIssues" else "", + if (scaladoc) "doc" else "" + )).filter(_.nonEmpty) ++ suffixCommands - val commandAlias: (String, List[String]) = command -> commands - - override val toString: String = commands.mkString("; ", "; ", "") + } } object CI { diff --git a/project/Common.scala b/project/Common.scala index 5c19130e03..831ac4807e 100644 --- a/project/Common.scala +++ b/project/Common.scala @@ -42,7 +42,7 @@ object Common extends AutoPlugin { ), tlVersionIntroduced ++= { if (crossProjectPlatform.?.value.contains(NativePlatform)) - List("2.12", "2.13", "3").map(_ -> "3.7.0").toMap + List("2.12", "2.13", "3").map(_ -> "3.4.0").toMap else Map.empty } diff --git a/project/JSEnv.scala b/project/JSEnv.scala index 66813b9be1..3457a97494 100644 --- a/project/JSEnv.scala +++ b/project/JSEnv.scala @@ -19,4 +19,5 @@ object JSEnv { case object Firefox extends JSEnv case object Chrome extends JSEnv case object NodeJS extends JSEnv + case object WASM extends JSEnv } diff --git a/project/build.properties b/project/build.properties index cc68b53f1a..e97b27220f 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.11 +sbt.version=1.10.10 diff --git a/project/plugins.sbt b/project/plugins.sbt index 8e0ac42bc3..160006376c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,9 +3,9 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.7") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.6") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.6.5") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.6.4") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.2") diff --git a/scalafix/project/build.properties b/scalafix/project/build.properties index cc68b53f1a..e97b27220f 100644 --- a/scalafix/project/build.properties +++ b/scalafix/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.11 +sbt.version=1.10.10 diff --git a/std/js/src/main/scala/cats/effect/std/MapRefCompanionPlatform.scala b/std/js-native/src/main/scala/cats/effect/std/MapRefCompanionPlatform.scala similarity index 100% rename from std/js/src/main/scala/cats/effect/std/MapRefCompanionPlatform.scala rename to std/js-native/src/main/scala/cats/effect/std/MapRefCompanionPlatform.scala diff --git a/std/jvm-native/src/main/scala/cats/effect/std/DispatcherPlatform.scala b/std/jvm/src/main/scala/cats/effect/std/DispatcherPlatform.scala similarity index 100% rename from std/jvm-native/src/main/scala/cats/effect/std/DispatcherPlatform.scala rename to std/jvm/src/main/scala/cats/effect/std/DispatcherPlatform.scala diff --git a/std/jvm-native/src/main/scala/cats/effect/std/MapRefCompanionPlatform.scala b/std/jvm/src/main/scala/cats/effect/std/MapRefCompanionPlatform.scala similarity index 100% rename from std/jvm-native/src/main/scala/cats/effect/std/MapRefCompanionPlatform.scala rename to std/jvm/src/main/scala/cats/effect/std/MapRefCompanionPlatform.scala diff --git a/std/native/src/main/scala/cats/effect/std/DispatcherPlatform.scala b/std/native/src/main/scala/cats/effect/std/DispatcherPlatform.scala new file mode 100644 index 0000000000..f72314dc44 --- /dev/null +++ b/std/native/src/main/scala/cats/effect/std/DispatcherPlatform.scala @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2025 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect.std + +private[std] trait DispatcherPlatform[F[_]] diff --git a/std/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala b/std/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala index 07c3f29891..2aa8ca8d89 100644 --- a/std/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala +++ b/std/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala @@ -38,7 +38,7 @@ private[std] trait SecureRandomCompanionPlatform { var i = 0 while (i < len) { val n = Math.min(256, len - i) - if (sysrandom.getentropy(bytes.atUnsafe(i), n.toCSize) < 0) + if (sysrandom.getentropy(bytes.atUnsafe(i), n.toULong) < 0) throw new RuntimeException(fromCString(strerror(errno))) i += n } diff --git a/std/shared/src/main/scala/cats/effect/std/Random.scala b/std/shared/src/main/scala/cats/effect/std/Random.scala index 880ed7e970..a46ed5ddfe 100644 --- a/std/shared/src/main/scala/cats/effect/std/Random.scala +++ b/std/shared/src/main/scala/cats/effect/std/Random.scala @@ -331,41 +331,25 @@ object Random extends RandomCompanionPlatform { SecureRandom.javaSecuritySecureRandom[F].widen[Random[F]] private[std] sealed abstract class RandomCommon[F[_]: Sync] extends Random[F] { - def betweenDouble(minInclusive: Double, maxExclusive: Double): F[Double] = { + def betweenDouble(minInclusive: Double, maxExclusive: Double): F[Double] = for { _ <- require(minInclusive < maxExclusive, "Invalid bounds") d <- nextDouble } yield { - val diff = maxExclusive - minInclusive - val next = if (diff != java.lang.Double.POSITIVE_INFINITY) { - (d * diff) + minInclusive - } else { // overflow: - val maxHalf = maxExclusive / 2.0 - val minHalf = minInclusive / 2.0 - ((d * (maxHalf - minHalf)) + minHalf) * 2.0 - } + val next = d * (maxExclusive - minInclusive) + minInclusive if (next < maxExclusive) next else Math.nextAfter(maxExclusive, Double.NegativeInfinity) } - } - def betweenFloat(minInclusive: Float, maxExclusive: Float): F[Float] = { + def betweenFloat(minInclusive: Float, maxExclusive: Float): F[Float] = for { _ <- require(minInclusive < maxExclusive, "Invalid bounds") f <- nextFloat } yield { - val diff = maxExclusive - minInclusive - val next = if (diff != java.lang.Float.POSITIVE_INFINITY) { - (f * diff) + minInclusive - } else { // overflow: - val maxHalf = maxExclusive / 2.0f - val minHalf = minInclusive / 2.0f - ((f * (maxHalf - minHalf)) + minHalf) * 2.0f - } + val next = f * (maxExclusive - minInclusive) + minInclusive if (next < maxExclusive) next else Math.nextAfter(maxExclusive, Float.NegativeInfinity) } - } def betweenInt(minInclusive: Int, maxExclusive: Int): F[Int] = require(minInclusive < maxExclusive, "Invalid bounds") *> { diff --git a/tests/js/src/test/scala/cats/effect/ContSpecBasePlatform.scala b/tests/js-native/src/test/scala/cats/effect/ContSpecBasePlatform.scala similarity index 100% rename from tests/js/src/test/scala/cats/effect/ContSpecBasePlatform.scala rename to tests/js-native/src/test/scala/cats/effect/ContSpecBasePlatform.scala diff --git a/tests/js/src/test/scala/cats/effect/unsafe/StripedHashtableSuite.scala b/tests/js/src/test/scala/cats/effect/unsafe/StripedHashtableSuite.scala index f93a910d88..47b6d1e936 100644 --- a/tests/js/src/test/scala/cats/effect/unsafe/StripedHashtableSuite.scala +++ b/tests/js/src/test/scala/cats/effect/unsafe/StripedHashtableSuite.scala @@ -24,7 +24,7 @@ import scala.concurrent.duration._ class StripedHashtableSuite extends BaseSuite { - override def executionTimeout: FiniteDuration = super.executionTimeout * 6 + override def executionTimeout: FiniteDuration = 2.minutes def hashtableRuntime(): IORuntime = IORuntime( diff --git a/tests/jvm-native/src/test/scala/cats/effect/IOConcurrencySuite.scala b/tests/jvm-native/src/test/scala/cats/effect/IOConcurrencySuite.scala deleted file mode 100644 index f6b9ff51e0..0000000000 --- a/tests/jvm-native/src/test/scala/cats/effect/IOConcurrencySuite.scala +++ /dev/null @@ -1,603 +0,0 @@ -/* - * Copyright 2020-2025 Typelevel - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package cats.effect - -import cats.effect.std.Semaphore -import cats.effect.unsafe.{ - IORuntime, - IORuntimeConfig, - PollResult, - PollingContext, - PollingSystem, - SleepSystem, - WorkStealingThreadPool -} -import cats.effect.unsafe.metrics.PollerMetrics -import cats.syntax.all._ - -import scala.concurrent.ExecutionContext -import scala.concurrent.duration._ - -import java.util.concurrent.{ - CancellationException, - CountDownLatch, - Executors, - ThreadLocalRandom -} -import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference} - -trait IOConcurrencySuite extends DetectPlatform { this: BaseSuite => - - def concurrencyTests() = { - real("shift delay evaluation within evalOn") { - val Exec1Name = "testing executor 1" - val exec1 = Executors.newSingleThreadExecutor { r => - val t = new Thread(r) - t.setName(Exec1Name) - t - } - - val Exec2Name = "testing executor 2" - val exec2 = Executors.newSingleThreadExecutor { r => - val t = new Thread(r) - t.setName(Exec2Name) - t - } - - val Exec3Name = "testing executor 3" - val exec3 = Executors.newSingleThreadExecutor { r => - val t = new Thread(r) - t.setName(Exec3Name) - t - } - - val nameF = IO(Thread.currentThread().getName()) - - val test = nameF flatMap { outer1 => - val inner1F = nameF flatMap { inner1 => - val inner2F = nameF map { inner2 => (outer1, inner1, inner2) } - - inner2F.evalOn(ExecutionContext.fromExecutor(exec2)) - } - - inner1F.evalOn(ExecutionContext.fromExecutor(exec1)).flatMap { - case (outer1, inner1, inner2) => - nameF.map(outer2 => (outer1, inner1, inner2, outer2)) - } - } - - test.evalOn(ExecutionContext.fromExecutor(exec3)).flatMap { result => - IO { - assertEquals(result, (Exec3Name, Exec1Name, Exec2Name, Exec3Name)) - } - } - } - - real("start 1000 fibers in parallel and await them all") { - val input = (0 until 1000).toList - - val ioa = for { - fibers <- input.traverse(i => IO.pure(i).start) - _ <- fibers.traverse_(_.join.void) - } yield () - - ioa - } - - real("start 1000 fibers in series and await them all") { - val input = (0 until 1000).toList - val ioa = input.traverse_(i => IO.pure(i).start.flatMap(_.join)) - - ioa - } - - real("race many things") { - val task = (0 until 100).foldLeft(IO.never[Int]) { (acc, _) => - IO.race(acc, IO(1)).map { - case Left(i) => i - case Right(i) => i - } - } - - task.replicateA_(100) - } - - real("auto-cede") { - val forever = IO.unit.foreverM - - val ec = ExecutionContext.fromExecutorService(Executors.newSingleThreadExecutor()) - - val run = for { - // Run in a tight loop on single-threaded ec so only hope of - // seeing cancelation status is auto-cede - fiber <- forever.start - // Allow the tight loop to be scheduled - _ <- IO.sleep(5.millis) - // Only hope for the cancelation being run is auto-yielding - _ <- fiber.cancel - } yield true - - run.evalOn(ec).guarantee(IO(ec.shutdown())).flatMap { res => IO(assert(res)) } - } - - ticked("cancel all inner effects when canceled") { implicit ticker => - val deadlock = for { - gate1 <- Semaphore[IO](2) - _ <- gate1.acquireN(2) - - gate2 <- Semaphore[IO](2) - _ <- gate2.acquireN(2) - - io = IO { - // these finalizers never return, so this test is intentionally designed to hang - // they flip their gates first though; this is just testing that both run in parallel - val a = (gate1.release *> IO.never) onCancel { - gate2.release *> IO.never - } - - val b = (gate1.release *> IO.never) onCancel { - gate2.release *> IO.never - } - - a.unsafeRunAndForget() - b.unsafeRunAndForget() - } - - _ <- io.flatMap(_ => gate1.acquireN(2)).start - _ <- gate2.acquireN(2) // if both are not run in parallel, then this will hang - } yield () - - val test = for { - t <- IO(deadlock.unsafeToFutureCancelable()) - (f, ct) = t - _ <- IO.fromFuture(IO(ct())) - _ <- IO.blocking(scala.concurrent.Await.result(f, Duration.Inf)) - } yield () - - val io = test.attempt.map { - case Left(t) => t.isInstanceOf[CancellationException] - case Right(_) => false - } - assertCompleteAs(io, true) - } - - realWithRuntime("run a timer which crosses into a blocking region") { rt => - rt.scheduler match { - case sched: WorkStealingThreadPool[?] => - // we structure this test by calling the runtime directly to avoid nondeterminism - val delay = IO.async[Unit] { cb => - IO { - // register a timer (use sleepInternal to ensure we get the worker-local version) - val cancel = sched.sleepInternal(1.second, cb) - - // convert the worker to a blocker - scala.concurrent.blocking(()) - - Some(IO(cancel.run())) - } - } - - // if the timer fires correctly, the timeout will not be hit - delay.race(IO.sleep(2.seconds)).flatMap(res => IO(assert(res.isLeft))) - - case _ => - IO.println("test not running against WSTP") - } - } - - realWithRuntime("run timers exactly once when crossing into a blocking region") { rt => - rt.scheduler match { - case sched: WorkStealingThreadPool[?] => - IO defer { - val ai = new AtomicInteger(0) - - sched.sleepInternal(500.millis, { _ => ai.getAndIncrement(); () }) - - // if we aren't careful, this conversion can duplicate the timer - scala.concurrent.blocking { - IO.sleep(1.second) >> IO(assertEquals(ai.get(), 1)) - } - } - - case _ => - IO.println("test not running against WSTP") - } - } - - realWithRuntime("run a timer registered on a blocker") { rt => - rt.scheduler match { - case sched: WorkStealingThreadPool[?] => - // we structure this test by calling the runtime directly to avoid nondeterminism - val delay = IO.async[Unit] { cb => - IO { - scala.concurrent.blocking { - // register a timer (use sleepInternal to ensure we get the worker-local version) - val cancel = sched.sleepInternal(1.second, cb) - Some(IO(cancel.run())) - } - } - } - - // if the timer fires correctly, the timeout will not be hit - delay.race(IO.sleep(2.seconds)).flatMap(res => IO(assert(res.isLeft))) - - case _ => IO.println("test not running against WSTP") - } - } - - testUnit("safely detect hard-blocked threads even while blockers are being created") { - val (compute, _, shutdown) = - IORuntime.createWorkStealingComputeThreadPool(blockedThreadDetectionEnabled = true) - - implicit val runtime: IORuntime = - IORuntime.builder().setCompute(compute, shutdown).build() - - try { - val test = for { - _ <- IO.unit.foreverM.start.replicateA_(200) - _ <- 0.until(200).toList.parTraverse_(_ => IO.blocking(())) - } yield () // we can't actually test this directly because the symptom is vaporizing a worker - - test.unsafeRunSync() - } finally { - runtime.shutdown() - } - } - - // this test ensures that the parkUntilNextSleeper bit works - testUnit("run a timer when parking thread") { - val (pool, _, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 1) - - implicit val runtime: IORuntime = IORuntime.builder().setCompute(pool, shutdown).build() - - try { - // longer sleep all-but guarantees this timer is fired *after* the worker is parked - val test = IO.sleep(500.millis) *> IO.pure(true) - assertEquals(test.unsafeRunTimed(5.seconds), Some(true)) - } finally { - runtime.shutdown() - } - } - - // this test ensures that we always see the timer, even when it fires just as we're about to park - testUnit("run a timer when detecting just prior to park") { - val (pool, _, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 1) - - implicit val runtime: IORuntime = IORuntime.builder().setCompute(pool, shutdown).build() - - try { - // shorter sleep makes it more likely this timer fires *before* the worker is parked - val test = IO.sleep(1.milli) *> IO.pure(true) - assertEquals(test.unsafeRunTimed(1.second), Some(true)) - } finally { - runtime.shutdown() - } - } - - real("random racing sleeps") { - def randomSleep: IO[Unit] = IO.defer { - val n = ThreadLocalRandom.current().nextInt(2000000) - IO.sleep(n.micros) // less than 2 seconds - } - - def raceAll(ios: List[IO[Unit]]): IO[Unit] = { - ios match { - case head :: tail => tail.foldLeft(head) { (x, y) => IO.race(x, y).void } - case Nil => IO.unit - } - } - - // we race a lot of "sleeps", it must not hang - // (this includes inserting and cancelling - // a lot of callbacks into the heap, - // thus hopefully stressing the data structure): - List - .fill(500) { - raceAll(List.fill(500) { randomSleep }) - } - .parSequence_ - - } - - realWithRuntime("steal timers") { rt => - val spin = IO.cede *> IO { // first make sure we're on a `WorkerThread` - // The `WorkerThread` which executes this IO - // will never exit the `while` loop, unless - // the timer is triggered, so it will never - // be able to trigger the timer itself. The - // only way this works is if some other worker - // steals the the timer. - val flag = new AtomicBoolean(false) - val _ = rt.scheduler.sleep(500.millis, () => { flag.set(true) }) - var ctr = 0L - while (!flag.get()) { - if ((ctr % 8192L) == 0L) { - // Make sure there is another unparked - // worker searching (and stealing timers): - rt.compute.execute(() => { () }) - } - ctr += 1L - } - } - - spin - } - - real("lots of externally-canceled timers") { - Resource - .make(IO(Executors.newSingleThreadExecutor()))(exec => IO(exec.shutdownNow()).void) - .map(ExecutionContext.fromExecutor(_)) - .use { ec => IO.sleep(1.day).start.flatMap(_.cancel.evalOn(ec)).parReplicateA_(100000) } - - } - - testUnit("not lose cedeing threads from the bypass when blocker transitioning") { - // writing this test in terms of IO seems to not reproduce the issue - 0.until(5) foreach { _ => - val wstp = new WorkStealingThreadPool[AnyRef]( - threadCount = 2, - threadPrefix = "testWorker", - blockerThreadPrefix = "testBlocker", - runtimeBlockingExpiration = 3.seconds, - reportFailure0 = _.printStackTrace(), - blockedThreadDetectionEnabled = false, - shutdownTimeout = 1.second, - system = SleepSystem, - uncaughtExceptionHandler = (_, t) => t.printStackTrace() - ) - - val runtime = IORuntime - .builder() - .setCompute(wstp, () => wstp.shutdown()) - .setConfig(IORuntimeConfig(cancelationCheckThreshold = 1, autoYieldThreshold = 2)) - .build() - - try { - val ctr = new AtomicLong - - val tsk1 = IO { ctr.incrementAndGet() }.foreverM - val fib1 = tsk1.unsafeRunFiber((), _ => (), { (_: Any) => () })(runtime) - for (_ <- 1 to 10) { - val tsk2 = IO.blocking { Thread.sleep(5L) } - tsk2.unsafeRunFiber((), _ => (), _ => ())(runtime) - } - fib1.join.unsafeRunFiber((), _ => (), _ => ())(runtime) - - Thread.sleep(1000L) - val results = 0.until(3).toList map { _ => - Thread.sleep(100L) - ctr.get() - } - - assertNotEquals(results, List.fill(3)(results.head)) - } finally { - runtime.shutdown() - } - } - - () - } - - trait DummyPoller { - def poll: IO[Unit] - } - - object DummySystem extends PollingSystem { - type Api = DummyPoller - type Poller = AtomicReference[List[Either[Throwable, Unit] => Unit]] - - def close() = () - - def makePoller() = new AtomicReference(List.empty[Either[Throwable, Unit] => Unit]) - def needsPoll(poller: Poller) = poller.get.nonEmpty - def closePoller(poller: Poller) = () - def metrics(poller: Poller): PollerMetrics = PollerMetrics.noop - - def interrupt(targetThread: Thread, targetPoller: Poller) = - SleepSystem.interrupt(targetThread, SleepSystem.makePoller()) - - def poll(poller: Poller, nanos: Long) = { - poller.get() match { - case Nil => - SleepSystem.poll(SleepSystem.makePoller(), nanos) - case _ => PollResult.Complete - } - } - - def processReadyEvents(poller: Poller) = { - poller.getAndSet(Nil) match { - case Nil => - false - case cbs => - cbs.foreach(_.apply(Right(()))) - true - } - } - - def makeApi(ctx: PollingContext[Poller]): DummySystem.Api = - new DummyPoller { - def poll = IO.async_[Unit] { cb => - ctx.accessPoller { poller => - poller.getAndUpdate(cb :: _) - () - } - } - } - } - - final class MockSystem(sleepLatch: CountDownLatch) extends PollingSystem { - - type Api = MockSystem - type Poller = AnyRef - - val wasInterrupted: AtomicBoolean = new AtomicBoolean(false) - - private[this] val interruptLatch = new CountDownLatch(1) - - def close(): Unit = () - - def makeApi(ctx: PollingContext[Poller]): Api = this - - def makePoller(): Poller = this - - def closePoller(poller: Poller): Unit = () - - def poll(poller: Poller, nanos: Long): PollResult = { - sleepLatch.countDown() - try { - interruptLatch.await() - } catch { - case _: InterruptedException => - // we've received the Thread#interrupt before the - // CountDownLatch#countDown, but it doesn't matter - () - } - PollResult.Interrupted - } - - def processReadyEvents(poller: Poller): Boolean = false - - def needsPoll(poller: Poller): Boolean = false - - def interrupt(targetThread: Thread, poller: Poller): Unit = { - wasInterrupted.set(true) - interruptLatch.countDown() - } - - def metrics(poller: Poller): PollerMetrics = PollerMetrics.noop - } - - testUnit("wake parked thread for polled events") { - - val (pool, poller, shutdown) = - IORuntime.createWorkStealingComputeThreadPool(threads = 2, pollingSystem = DummySystem) - - implicit val runtime: IORuntime = - IORuntime.builder().setCompute(pool, shutdown).addPoller(poller, () => ()).build() - - try { - val test = - IO.pollers.map(_.head.asInstanceOf[DummyPoller]).flatMap { poller => - val blockAndPoll = IO.blocking(Thread.sleep(10)) *> poller.poll - blockAndPoll.replicateA(100).as(true) - } - assert(test.unsafeRunSync()) - } finally { - runtime.shutdown() - } - } - - testUnit("poll punctually on a single-thread runtime with concurrent sleepers") { - - val (pool, poller, shutdown) = - IORuntime.createWorkStealingComputeThreadPool(threads = 1, pollingSystem = DummySystem) - - implicit val runtime: IORuntime = - IORuntime.builder().setCompute(pool, shutdown).addPoller(poller, () => ()).build() - - try { - val test = IO.sleep(1.minute).background.surround { - IO.pollers.map(_.head.asInstanceOf[DummyPoller]).flatMap { poller => - // in #4225 the fiber rescheduled during this poll does not execute until the next timer fires - poller.poll.as(true) - } - } - - // NOTE!!! - // We cannot use a timeout *on* the runtime, because that causes the polling fiber - // to become unstuck sooner and pass the test - assertEquals(test.unsafeRunTimed(1.second), Some(true)) - } finally { - runtime.shutdown() - } - } - - testUnit("external work does not starve poll") { - - val (pool, poller, shutdown) = - IORuntime.createWorkStealingComputeThreadPool(threads = 1, pollingSystem = DummySystem) - - implicit val runtime: IORuntime = - IORuntime.builder().setCompute(pool, shutdown).addPoller(poller, () => ()).build() - - try { - def mkExternalWork: Runnable = { () => - val latch = new CountDownLatch(1) - ExecutionContext.global.execute { () => - pool.execute(mkExternalWork) - latch.countDown() - } - try { - latch.await() // wait until next task is in external queue - } catch { - case _: InterruptedException => // ignore, runtime is shutting down - } - } - - val test = IO(mkExternalWork.run()) *> - IO.pollers.map(_.head.asInstanceOf[DummyPoller]).flatMap { poller => - poller.poll.as(true) - } - - assertEquals(test.unsafeRunTimed(1.second), Some(true)) - } finally { - runtime.shutdown() - } - } - - testUnit("blocking work does not starve poll") { - - val (pool, poller, shutdown) = - IORuntime.createWorkStealingComputeThreadPool(threads = 1, pollingSystem = DummySystem) - - implicit val runtime: IORuntime = - IORuntime.builder().setCompute(pool, shutdown).addPoller(poller, () => ()).build() - - try { - def mkBlockingWork: IO[Unit] = IO.defer(mkBlockingWork.start) *> IO.blocking(()) - - val test = mkBlockingWork *> - IO.pollers.map(_.head.asInstanceOf[DummyPoller]).flatMap { poller => - poller.poll.replicateA_(100).as(true) - } - - assertEquals(test.unsafeRunTimed(if (isNative) 5.seconds else 1.second), Some(true)) - } finally { - runtime.shutdown() - } - } - - testUnit("correctly interrupt pollers on shutdown") { - - val sleepLatch = new CountDownLatch(1) - val (pool, poller, shutdown) = IORuntime.createWorkStealingComputeThreadPool( - threads = 1, - shutdownTimeout = 60.seconds, - pollingSystem = new MockSystem(sleepLatch)) - - implicit val runtime: IORuntime = - IORuntime.builder().setCompute(pool, shutdown).addPoller(poller, () => ()).build() - - try { - sleepLatch.await() // wait for the thread to "go to sleep" - } finally { - runtime.shutdown() - } - assertEquals(poller.wasInterrupted.get(), true) - } - } -} diff --git a/tests/jvm-native/src/test/scala/cats/effect/ContSpecBasePlatform.scala b/tests/jvm/src/test/scala/cats/effect/ContSpecBasePlatform.scala similarity index 100% rename from tests/jvm-native/src/test/scala/cats/effect/ContSpecBasePlatform.scala rename to tests/jvm/src/test/scala/cats/effect/ContSpecBasePlatform.scala diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSuite.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSuite.scala index 682c69611f..4bd1d94e7f 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSuite.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSuite.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2025 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,110 @@ package cats.effect +import cats.effect.std.Semaphore +import cats.effect.unsafe.{ + IORuntime, + IORuntimeConfig, + PollResult, + PollingContext, + PollingSystem, + SleepSystem, + WorkStealingThreadPool +} +import cats.effect.unsafe.metrics.PollerMetrics +import cats.syntax.all._ + import org.scalacheck.Prop.forAll import scala.concurrent.ExecutionContext +import scala.concurrent.duration._ -import java.util.concurrent.{CompletableFuture, CountDownLatch, ExecutorService, Executors} +import java.util.concurrent.{ + CancellationException, + CompletableFuture, + CountDownLatch, + ExecutorService, + Executors, + ThreadLocalRandom +} +import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference} -trait IOPlatformSuite extends IOConcurrencySuite { this: BaseScalaCheckSuite => +trait IOPlatformSuite extends DetectPlatform { + self: BaseScalaCheckSuite => def platformTests() = { - concurrencyTests() + real("shift delay evaluation within evalOn") { + val Exec1Name = "testing executor 1" + val exec1 = Executors.newSingleThreadExecutor { r => + val t = new Thread(r) + t.setName(Exec1Name) + t + } + + val Exec2Name = "testing executor 2" + val exec2 = Executors.newSingleThreadExecutor { r => + val t = new Thread(r) + t.setName(Exec2Name) + t + } + + val Exec3Name = "testing executor 3" + val exec3 = Executors.newSingleThreadExecutor { r => + val t = new Thread(r) + t.setName(Exec3Name) + t + } + + val nameF = IO(Thread.currentThread().getName()) + + val test = nameF flatMap { outer1 => + val inner1F = nameF flatMap { inner1 => + val inner2F = nameF map { inner2 => (outer1, inner1, inner2) } + + inner2F.evalOn(ExecutionContext.fromExecutor(exec2)) + } + + inner1F.evalOn(ExecutionContext.fromExecutor(exec1)).flatMap { + case (outer1, inner1, inner2) => + nameF.map(outer2 => (outer1, inner1, inner2, outer2)) + } + } + + test.evalOn(ExecutionContext.fromExecutor(exec3)).flatMap { result => + IO { + assertEquals(result, (Exec3Name, Exec1Name, Exec2Name, Exec3Name)) + } + } + } + + real("start 1000 fibers in parallel and await them all") { + val input = (0 until 1000).toList + + val ioa = for { + fibers <- input.traverse(i => IO.pure(i).start) + _ <- fibers.traverse_(_.join.void) + } yield () + + ioa + } + + real("start 1000 fibers in series and await them all") { + val input = (0 until 1000).toList + val ioa = input.traverse_(i => IO.pure(i).start.flatMap(_.join)) + + ioa + } + + real("race many things") { + val task = (0 until 100).foldLeft(IO.never[Int]) { (acc, _) => + IO.race(acc, IO(1)).map { + case Left(i) => i + case Right(i) => i + } + } + + task.replicateA_(100) + } tickedProperty("round trip non-canceled through j.u.c.CompletableFuture") { implicit ticker => @@ -110,6 +204,24 @@ trait IOPlatformSuite extends IOConcurrencySuite { this: BaseScalaCheckSuite => } yield () } + real("auto-cede") { + val forever = IO.unit.foreverM + + val ec = ExecutionContext.fromExecutorService(Executors.newSingleThreadExecutor()) + + val run = for { + // Run in a tight loop on single-threaded ec so only hope of + // seeing cancelation status is auto-cede + fiber <- forever.start + // Allow the tight loop to be scheduled + _ <- IO.sleep(5.millis) + // Only hope for the cancelation being run is auto-yielding + _ <- fiber.cancel + } yield true + + run.evalOn(ec).guarantee(IO(ec.shutdown())).flatMap { res => IO(assert(res)) } + } + ticked("realTimeInstant should return an Instant constructed from realTime") { implicit ticker => val op = for { @@ -120,6 +232,411 @@ trait IOPlatformSuite extends IOConcurrencySuite { this: BaseScalaCheckSuite => assertCompleteAs(op, true) } + ticked("cancel all inner effects when canceled") { implicit ticker => + val deadlock = for { + gate1 <- Semaphore[IO](2) + _ <- gate1.acquireN(2) + + gate2 <- Semaphore[IO](2) + _ <- gate2.acquireN(2) + + io = IO { + // these finalizers never return, so this test is intentionally designed to hang + // they flip their gates first though; this is just testing that both run in parallel + val a = (gate1.release *> IO.never) onCancel { + gate2.release *> IO.never + } + + val b = (gate1.release *> IO.never) onCancel { + gate2.release *> IO.never + } + + a.unsafeRunAndForget() + b.unsafeRunAndForget() + } + + _ <- io.flatMap(_ => gate1.acquireN(2)).start + _ <- gate2.acquireN(2) // if both are not run in parallel, then this will hang + } yield () + + val test = for { + t <- IO(deadlock.unsafeToFutureCancelable()) + (f, ct) = t + _ <- IO.fromFuture(IO(ct())) + _ <- IO.blocking(scala.concurrent.Await.result(f, Duration.Inf)) + } yield () + + val io = test.attempt.map { + case Left(t) => t.isInstanceOf[CancellationException] + case Right(_) => false + } + assertCompleteAs(io, true) + } + + realWithRuntime("run a timer which crosses into a blocking region") { rt => + rt.scheduler match { + case sched: WorkStealingThreadPool[?] => + // we structure this test by calling the runtime directly to avoid nondeterminism + val delay = IO.async[Unit] { cb => + IO { + // register a timer (use sleepInternal to ensure we get the worker-local version) + val cancel = sched.sleepInternal(1.second, cb) + + // convert the worker to a blocker + scala.concurrent.blocking(()) + + Some(IO(cancel.run())) + } + } + + // if the timer fires correctly, the timeout will not be hit + delay.race(IO.sleep(2.seconds)).flatMap(res => IO(assert(res.isLeft))) + + case _ => + IO.println("test not running against WSTP") + } + } + + realWithRuntime("run timers exactly once when crossing into a blocking region") { rt => + rt.scheduler match { + case sched: WorkStealingThreadPool[?] => + IO defer { + val ai = new AtomicInteger(0) + + sched.sleepInternal(500.millis, { _ => ai.getAndIncrement(); () }) + + // if we aren't careful, this conversion can duplicate the timer + scala.concurrent.blocking { + IO.sleep(1.second) >> IO(assertEquals(ai.get(), 1)) + } + } + + case _ => + IO.println("test not running against WSTP") + } + } + + realWithRuntime("run a timer registered on a blocker") { rt => + rt.scheduler match { + case sched: WorkStealingThreadPool[?] => + // we structure this test by calling the runtime directly to avoid nondeterminism + val delay = IO.async[Unit] { cb => + IO { + scala.concurrent.blocking { + // register a timer (use sleepInternal to ensure we get the worker-local version) + val cancel = sched.sleepInternal(1.second, cb) + Some(IO(cancel.run())) + } + } + } + + // if the timer fires correctly, the timeout will not be hit + delay.race(IO.sleep(2.seconds)).flatMap(res => IO(assert(res.isLeft))) + + case _ => IO.println("test not running against WSTP") + } + } + + testUnit("safely detect hard-blocked threads even while blockers are being created") { + val (compute, _, shutdown) = + IORuntime.createWorkStealingComputeThreadPool(blockedThreadDetectionEnabled = true) + + implicit val runtime: IORuntime = + IORuntime.builder().setCompute(compute, shutdown).build() + + try { + val test = for { + _ <- IO.unit.foreverM.start.replicateA_(200) + _ <- 0.until(200).toList.parTraverse_(_ => IO.blocking(())) + } yield () // we can't actually test this directly because the symptom is vaporizing a worker + + test.unsafeRunSync() + } finally { + runtime.shutdown() + } + } + + // this test ensures that the parkUntilNextSleeper bit works + testUnit("run a timer when parking thread") { + val (pool, _, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 1) + + implicit val runtime: IORuntime = IORuntime.builder().setCompute(pool, shutdown).build() + + try { + // longer sleep all-but guarantees this timer is fired *after* the worker is parked + val test = IO.sleep(500.millis) *> IO.pure(true) + assertEquals(test.unsafeRunTimed(5.seconds), Some(true)) + } finally { + runtime.shutdown() + } + } + + // this test ensures that we always see the timer, even when it fires just as we're about to park + testUnit("run a timer when detecting just prior to park") { + val (pool, _, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 1) + + implicit val runtime: IORuntime = IORuntime.builder().setCompute(pool, shutdown).build() + + try { + // shorter sleep makes it more likely this timer fires *before* the worker is parked + val test = IO.sleep(1.milli) *> IO.pure(true) + assertEquals(test.unsafeRunTimed(1.second), Some(true)) + } finally { + runtime.shutdown() + } + } + + real("random racing sleeps") { + def randomSleep: IO[Unit] = IO.defer { + val n = ThreadLocalRandom.current().nextInt(2000000) + IO.sleep(n.micros) // less than 2 seconds + } + + def raceAll(ios: List[IO[Unit]]): IO[Unit] = { + ios match { + case head :: tail => tail.foldLeft(head) { (x, y) => IO.race(x, y).void } + case Nil => IO.unit + } + } + + // we race a lot of "sleeps", it must not hang + // (this includes inserting and cancelling + // a lot of callbacks into the heap, + // thus hopefully stressing the data structure): + List + .fill(500) { + raceAll(List.fill(500) { randomSleep }) + } + .parSequence_ + + } + + realWithRuntime("steal timers") { rt => + val spin = IO.cede *> IO { // first make sure we're on a `WorkerThread` + // The `WorkerThread` which executes this IO + // will never exit the `while` loop, unless + // the timer is triggered, so it will never + // be able to trigger the timer itself. The + // only way this works is if some other worker + // steals the the timer. + val flag = new AtomicBoolean(false) + val _ = rt.scheduler.sleep(500.millis, () => { flag.set(true) }) + var ctr = 0L + while (!flag.get()) { + if ((ctr % 8192L) == 0L) { + // Make sure there is another unparked + // worker searching (and stealing timers): + rt.compute.execute(() => { () }) + } + ctr += 1L + } + } + + spin + } + + real("lots of externally-canceled timers") { + Resource + .make(IO(Executors.newSingleThreadExecutor()))(exec => IO(exec.shutdownNow()).void) + .map(ExecutionContext.fromExecutor(_)) + .use { ec => IO.sleep(1.day).start.flatMap(_.cancel.evalOn(ec)).parReplicateA_(100000) } + + } + + testUnit("not lose cedeing threads from the bypass when blocker transitioning") { + // writing this test in terms of IO seems to not reproduce the issue + 0.until(5) foreach { _ => + val wstp = new WorkStealingThreadPool[AnyRef]( + threadCount = 2, + threadPrefix = "testWorker", + blockerThreadPrefix = "testBlocker", + runtimeBlockingExpiration = 3.seconds, + reportFailure0 = _.printStackTrace(), + blockedThreadDetectionEnabled = false, + shutdownTimeout = 1.second, + system = SleepSystem, + uncaughtExceptionHandler = (_, t) => t.printStackTrace() + ) + + val runtime = IORuntime + .builder() + .setCompute(wstp, () => wstp.shutdown()) + .setConfig(IORuntimeConfig(cancelationCheckThreshold = 1, autoYieldThreshold = 2)) + .build() + + try { + val ctr = new AtomicLong + + val tsk1 = IO { ctr.incrementAndGet() }.foreverM + val fib1 = tsk1.unsafeRunFiber((), _ => (), { (_: Any) => () })(runtime) + for (_ <- 1 to 10) { + val tsk2 = IO.blocking { Thread.sleep(5L) } + tsk2.unsafeRunFiber((), _ => (), _ => ())(runtime) + } + fib1.join.unsafeRunFiber((), _ => (), _ => ())(runtime) + + Thread.sleep(1000L) + val results = 0.until(3).toList map { _ => + Thread.sleep(100L) + ctr.get() + } + + assertNotEquals(results, List.fill(3)(results.head)) + } finally { + runtime.shutdown() + } + } + + () + } + + trait DummyPoller { + def poll: IO[Unit] + } + + object DummySystem extends PollingSystem { + type Api = DummyPoller + type Poller = AtomicReference[List[Either[Throwable, Unit] => Unit]] + + def close() = () + + def makePoller() = new AtomicReference(List.empty[Either[Throwable, Unit] => Unit]) + def needsPoll(poller: Poller) = poller.get.nonEmpty + def closePoller(poller: Poller) = () + def metrics(poller: Poller): PollerMetrics = PollerMetrics.noop + + def interrupt(targetThread: Thread, targetPoller: Poller) = + SleepSystem.interrupt(targetThread, SleepSystem.makePoller()) + + def poll(poller: Poller, nanos: Long) = { + poller.get() match { + case Nil => + SleepSystem.poll(SleepSystem.makePoller(), nanos) + case _ => PollResult.Complete + } + } + + def processReadyEvents(poller: Poller) = { + poller.getAndSet(Nil) match { + case Nil => + false + case cbs => + cbs.foreach(_.apply(Right(()))) + true + } + } + + def makeApi(ctx: PollingContext[Poller]): DummySystem.Api = + new DummyPoller { + def poll = IO.async_[Unit] { cb => + ctx.accessPoller { poller => + poller.getAndUpdate(cb :: _) + () + } + } + } + } + + testUnit("wake parked thread for polled events") { + + val (pool, poller, shutdown) = + IORuntime.createWorkStealingComputeThreadPool(threads = 2, pollingSystem = DummySystem) + + implicit val runtime: IORuntime = + IORuntime.builder().setCompute(pool, shutdown).addPoller(poller, () => ()).build() + + try { + val test = + IO.pollers.map(_.head.asInstanceOf[DummyPoller]).flatMap { poller => + val blockAndPoll = IO.blocking(Thread.sleep(10)) *> poller.poll + blockAndPoll.replicateA(100).as(true) + } + assert(test.unsafeRunSync()) + } finally { + runtime.shutdown() + } + } + + testUnit("poll punctually on a single-thread runtime with concurrent sleepers") { + + val (pool, poller, shutdown) = + IORuntime.createWorkStealingComputeThreadPool(threads = 1, pollingSystem = DummySystem) + + implicit val runtime: IORuntime = + IORuntime.builder().setCompute(pool, shutdown).addPoller(poller, () => ()).build() + + try { + val test = IO.sleep(1.minute).background.surround { + IO.pollers.map(_.head.asInstanceOf[DummyPoller]).flatMap { poller => + // in #4225 the fiber rescheduled during this poll does not execute until the next timer fires + poller.poll.as(true) + } + } + + // NOTE!!! + // We cannot use a timeout *on* the runtime, because that causes the polling fiber + // to become unstuck sooner and pass the test + assertEquals(test.unsafeRunTimed(1.second), Some(true)) + } finally { + runtime.shutdown() + } + } + + testUnit("external work does not starve poll") { + + val (pool, poller, shutdown) = + IORuntime.createWorkStealingComputeThreadPool(threads = 1, pollingSystem = DummySystem) + + implicit val runtime: IORuntime = + IORuntime.builder().setCompute(pool, shutdown).addPoller(poller, () => ()).build() + + try { + def mkExternalWork: Runnable = { () => + val latch = new CountDownLatch(1) + ExecutionContext.global.execute { () => + pool.execute(mkExternalWork) + latch.countDown() + } + try { + latch.await() // wait until next task is in external queue + } catch { + case _: InterruptedException => // ignore, runtime is shutting down + } + } + + val test = IO(mkExternalWork.run()) *> + IO.pollers.map(_.head.asInstanceOf[DummyPoller]).flatMap { poller => + poller.poll.as(true) + } + + assertEquals(test.unsafeRunTimed(1.second), Some(true)) + } finally { + runtime.shutdown() + } + } + + testUnit("blocking work does not starve poll") { + + val (pool, poller, shutdown) = + IORuntime.createWorkStealingComputeThreadPool(threads = 1, pollingSystem = DummySystem) + + implicit val runtime: IORuntime = + IORuntime.builder().setCompute(pool, shutdown).addPoller(poller, () => ()).build() + + try { + def mkBlockingWork: IO[Unit] = IO.defer(mkBlockingWork.start) *> IO.blocking(()) + + val test = mkBlockingWork *> + IO.pollers.map(_.head.asInstanceOf[DummyPoller]).flatMap { poller => + poller.poll.replicateA_(100).as(true) + } + + assertEquals(test.unsafeRunTimed(1.second), Some(true)) + } finally { + runtime.shutdown() + } + } + if (javaMajorVersion >= 21) real("block in-place on virtual threads") { val loomExec = classOf[Executors] diff --git a/tests/jvm/src/test/scala/cats/effect/ParasiticECSuite.scala b/tests/jvm/src/test/scala/cats/effect/ParasiticECSuite.scala index b4f88d1eb6..3bb36cfec1 100644 --- a/tests/jvm/src/test/scala/cats/effect/ParasiticECSuite.scala +++ b/tests/jvm/src/test/scala/cats/effect/ParasiticECSuite.scala @@ -25,7 +25,7 @@ import scala.concurrent.duration._ class ParasiticECSuite extends BaseSuite with TestInstances { - override def executionTimeout: FiniteDuration = super.executionTimeout * 3 + override def executionTimeout: FiniteDuration = 60.seconds real("evaluate fibers correctly in presence of a parasitic execution context") { val test = { diff --git a/tests/jvm-native/src/test/scala/cats/effect/RunnersPlatform.scala b/tests/jvm/src/test/scala/cats/effect/RunnersPlatform.scala similarity index 100% rename from tests/jvm-native/src/test/scala/cats/effect/RunnersPlatform.scala rename to tests/jvm/src/test/scala/cats/effect/RunnersPlatform.scala diff --git a/tests/jvm-native/src/test/scala/cats/effect/std/DeferredParallelismSuite.scala b/tests/jvm/src/test/scala/cats/effect/std/DeferredJVMSuite.scala similarity index 92% rename from tests/jvm-native/src/test/scala/cats/effect/std/DeferredParallelismSuite.scala rename to tests/jvm/src/test/scala/cats/effect/std/DeferredJVMSuite.scala index b5ce527d16..20e991e466 100644 --- a/tests/jvm-native/src/test/scala/cats/effect/std/DeferredParallelismSuite.scala +++ b/tests/jvm/src/test/scala/cats/effect/std/DeferredJVMSuite.scala @@ -28,13 +28,11 @@ import java.util.concurrent.atomic.AtomicLong import munit.FunSuite -class DeferredParallelism1Tests extends BaseDeferredParallelismTests(1) -class DeferredParallelism2Tests extends BaseDeferredParallelismTests(2) -class DeferredParallelism4Tests extends BaseDeferredParallelismTests(4) +class DeferredJVMParallelism1Tests extends BaseDeferredJVMTests(1) +class DeferredJVMParallelism2Tests extends BaseDeferredJVMTests(2) +class DeferredJVMParallelism4Tests extends BaseDeferredJVMTests(4) -abstract class BaseDeferredParallelismTests(parallelism: Int) - extends FunSuite - with DetectPlatform { +abstract class BaseDeferredJVMTests(parallelism: Int) extends FunSuite { var service: ExecutorService = _ implicit val context: ExecutionContext = new ExecutionContext { @@ -65,14 +63,9 @@ abstract class BaseDeferredParallelismTests(parallelism: Int) assert(service.awaitTermination(60, TimeUnit.SECONDS), "has active threads") } - // pasta from Runners - def timeoutCoefficient: Long = if (isNative) 5 else 1 - // ---------------------------------------------------------------------------- val iterations = if (isCI) 1000 else 10000 - val timeout = (if (isCI) 30.seconds else 10.seconds) * timeoutCoefficient - - override def munitTimeout: Duration = timeout * iterations.toLong + val timeout = if (isCI) 30.seconds else 10.seconds def cleanupOnError[A](task: IO[A], f: FiberIO[?]) = task guaranteeCase { diff --git a/tests/jvm-native/src/test/scala/cats/effect/std/DispatcherParallelSuite.scala b/tests/jvm/src/test/scala/cats/effect/std/DispatcherJVMSuite.scala similarity index 64% rename from tests/jvm-native/src/test/scala/cats/effect/std/DispatcherParallelSuite.scala rename to tests/jvm/src/test/scala/cats/effect/std/DispatcherJVMSuite.scala index c2edb3e057..5a5c36d6aa 100644 --- a/tests/jvm-native/src/test/scala/cats/effect/std/DispatcherParallelSuite.scala +++ b/tests/jvm/src/test/scala/cats/effect/std/DispatcherJVMSuite.scala @@ -22,7 +22,7 @@ import cats.syntax.all._ import scala.concurrent.duration.DurationInt -class DispatcherParallelSuite extends BaseSuite with DetectPlatform { +class DispatcherJVMSuite extends BaseSuite { real("run multiple IOs in parallel with blocking threads") { val num = 100 @@ -44,28 +44,26 @@ class DispatcherParallelSuite extends BaseSuite with DetectPlatform { } yield () } - if (isJVM) { - real("propagate Java thread interruption in unsafeRunSync") { - Dispatcher.parallel[IO](await = true).use { dispatcher => - for { - pre <- IO.deferred[Unit] - canceled <- IO.deferred[Unit] + real("propagate Java thread interruption in unsafeRunSync") { + Dispatcher.parallel[IO](await = true).use { dispatcher => + for { + pre <- IO.deferred[Unit] + canceled <- IO.deferred[Unit] - io = (pre.complete(()) *> IO.never).onCancel(canceled.complete(()).void) + io = (pre.complete(()) *> IO.never).onCancel(canceled.complete(()).void) - f <- IO.interruptible { - try dispatcher.unsafeRunSync(io) - catch { case _: InterruptedException => } - }.start + f <- IO.interruptible { + try dispatcher.unsafeRunSync(io) + catch { case _: InterruptedException => } + }.start - _ <- pre.get - _ <- f.cancel + _ <- pre.get + _ <- f.cancel - _ <- canceled - .get - .timeoutTo(1.second, IO.raiseError(new Exception("io was not canceled"))) - } yield () - } + _ <- canceled + .get + .timeoutTo(1.second, IO.raiseError(new Exception("io was not canceled"))) + } yield () } } } diff --git a/tests/jvm/src/test/scala/cats/effect/unsafe/BlockingStressSuite.scala b/tests/jvm/src/test/scala/cats/effect/unsafe/BlockingStressSuite.scala index 0eae890959..f256847136 100644 --- a/tests/jvm/src/test/scala/cats/effect/unsafe/BlockingStressSuite.scala +++ b/tests/jvm/src/test/scala/cats/effect/unsafe/BlockingStressSuite.scala @@ -27,7 +27,7 @@ import java.util.concurrent.CountDownLatch class BlockingStressSuite extends BaseSuite { - override def executionTimeout: FiniteDuration = super.executionTimeout * 3 / 2 + override def executionTimeout: FiniteDuration = 30.seconds // This test spawns a lot of helper threads. private val count = 1000 diff --git a/tests/jvm/src/test/scala/cats/effect/unsafe/StripedHashtableSuite.scala b/tests/jvm/src/test/scala/cats/effect/unsafe/StripedHashtableSuite.scala index 2259df845b..e3f86189c1 100644 --- a/tests/jvm/src/test/scala/cats/effect/unsafe/StripedHashtableSuite.scala +++ b/tests/jvm/src/test/scala/cats/effect/unsafe/StripedHashtableSuite.scala @@ -25,7 +25,7 @@ import java.util.concurrent.CountDownLatch class StripedHashtableSuite extends BaseSuite { - override def executionTimeout: FiniteDuration = super.executionTimeout * 3 / 2 + override def executionTimeout: FiniteDuration = 30.seconds def hashtableRuntime(): IORuntime = { lazy val rt: IORuntime = { diff --git a/tests/native/src/main/scala/catseffect/examplesplatform.scala b/tests/native/src/main/scala/catseffect/examplesplatform.scala index 7c575a6004..3de076ff64 100644 --- a/tests/native/src/main/scala/catseffect/examplesplatform.scala +++ b/tests/native/src/main/scala/catseffect/examplesplatform.scala @@ -17,13 +17,13 @@ package catseffect import cats.effect.{ExitCode, IO, IOApp} +import cats.effect.unsafe.IORuntime import cats.syntax.all._ import scala.collection.mutable -import scala.concurrent.ExecutionContext package object examples { - def exampleExecutionContext = ExecutionContext.global + def exampleExecutionContext = IORuntime.defaultComputeExecutionContext } package examples { diff --git a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSuite.scala b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSuite.scala index 1de29d1fad..cb92072f9f 100644 --- a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSuite.scala +++ b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSuite.scala @@ -20,6 +20,7 @@ import cats.effect.std.CountDownLatch import cats.syntax.all._ import scala.concurrent.duration._ +import scala.scalanative.libc.errno._ import scala.scalanative.posix.errno._ import scala.scalanative.posix.fcntl._ import scala.scalanative.posix.string._ @@ -40,14 +41,14 @@ class FileDescriptorPollerSuite extends BaseSuite { def read(buf: Array[Byte], offset: Int, length: Int): IO[Unit] = readHandle .pollReadRec(()) { _ => - IO(guard(unistd.read(readFd, buf.atUnsafe(offset), length.toCSize))) + IO(guard(unistd.read(readFd, buf.atUnsafe(offset), length.toULong))) } .void def write(buf: Array[Byte], offset: Int, length: Int): IO[Unit] = writeHandle .pollWriteRec(()) { _ => - IO(guard(unistd.write(writeFd, buf.atUnsafe(offset), length.toCSize))) + IO(guard(unistd.write(writeFd, buf.atUnsafe(offset), length.toULong))) } .void @@ -68,7 +69,7 @@ class FileDescriptorPollerSuite extends BaseSuite { Resource .make { IO { - val fd = stackalloc[CInt](2) + val fd = stackalloc[CInt](2.toULong) if (unistd.pipe(fd) != 0) throw new IOException(fromCString(strerror(errno))) (fd(0), fd(1)) @@ -120,7 +121,7 @@ class FileDescriptorPollerSuite extends BaseSuite { .surround { IO { // trigger all the pipes at once pipes.foreach { pipe => - unistd.write(pipe.writeFd, Array[Byte](42).atUnsafe(0), 1.toCSize) + unistd.write(pipe.writeFd, Array[Byte](42).atUnsafe(0), 1.toULong) } }.background.surround(latch.await) } diff --git a/tests/native/src/test/scala/cats/effect/IOPlatformSuite.scala b/tests/native/src/test/scala/cats/effect/IOPlatformSuite.scala index afbb4c93a4..d185f0ea26 100644 --- a/tests/native/src/test/scala/cats/effect/IOPlatformSuite.scala +++ b/tests/native/src/test/scala/cats/effect/IOPlatformSuite.scala @@ -16,11 +16,9 @@ package cats.effect -trait IOPlatformSuite extends IOConcurrencySuite { this: BaseSuite => +trait IOPlatformSuite { self: BaseScalaCheckSuite => def platformTests() = { - concurrencyTests() - ticked("realTimeInstant should return an Instant constructed from realTime") { implicit ticker => val op = for { diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPoolPlatform.scala b/tests/native/src/test/scala/cats/effect/RunnersPlatform.scala similarity index 65% rename from core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPoolPlatform.scala rename to tests/native/src/test/scala/cats/effect/RunnersPlatform.scala index 488b26450d..32e0755abb 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPoolPlatform.scala +++ b/tests/native/src/test/scala/cats/effect/RunnersPlatform.scala @@ -15,16 +15,9 @@ */ package cats.effect -package unsafe -import java.time.Instant -import java.time.temporal.ChronoField +import cats.effect.unsafe._ -trait WorkStealingThreadPoolPlatform[P <: AnyRef] extends Scheduler { - this: WorkStealingThreadPool[P] => - - override def nowMicros(): Long = { - val now = Instant.now() - now.getEpochSecond() * 1000000 + now.getLong(ChronoField.MICRO_OF_SECOND) - } +trait RunnersPlatform { + protected def runtime(): IORuntime = IORuntime.global } diff --git a/tests/shared/src/test/scala/cats/effect/IOLocalSuite.scala b/tests/shared/src/test/scala/cats/effect/IOLocalSuite.scala index dc8d78b185..9ea81ad5fa 100644 --- a/tests/shared/src/test/scala/cats/effect/IOLocalSuite.scala +++ b/tests/shared/src/test/scala/cats/effect/IOLocalSuite.scala @@ -21,9 +21,6 @@ import scala.annotation.tailrec class IOLocalSuite extends BaseSuite { - // the stack safety test is pretty long - override def executionTimeout = super.executionTimeout * 2 - ioLocalTests( "IOLocal[Int]", (i: Int) => IOLocal(i).map(l => (l, l)) diff --git a/tests/shared/src/test/scala/cats/effect/IOPropSuite.scala b/tests/shared/src/test/scala/cats/effect/IOPropSuite.scala index 39e3f9cf30..3662a102f7 100644 --- a/tests/shared/src/test/scala/cats/effect/IOPropSuite.scala +++ b/tests/shared/src/test/scala/cats/effect/IOPropSuite.scala @@ -30,7 +30,7 @@ import munit.DisciplineSuite //We allow these tests to have a longer timeout than IOSpec as they run lots of iterations class IOPropSuite extends BaseSuite with DisciplineSuite { - override def executionTimeout: FiniteDuration = super.executionTimeout * 6 + override def executionTimeout: FiniteDuration = 2.minutes realProp( "parTraverseN should give the same result as parTraverse", diff --git a/tests/shared/src/test/scala/cats/effect/Runners.scala b/tests/shared/src/test/scala/cats/effect/Runners.scala index 5047367756..5711c72c8b 100644 --- a/tests/shared/src/test/scala/cats/effect/Runners.scala +++ b/tests/shared/src/test/scala/cats/effect/Runners.scala @@ -30,14 +30,10 @@ import scala.reflect.{classTag, ClassTag} import munit.{FunSuite, Location, TestOptions} import munit.internal.PlatformCompat -trait Runners extends TestInstances with RunnersPlatform with DetectPlatform { +trait Runners extends TestInstances with RunnersPlatform { self: FunSuite => - def timeoutCoefficient: Long = if (isNative) 5 else 1 - - def executionTimeout: FiniteDuration = - 20.seconds * timeoutCoefficient - + def executionTimeout: FiniteDuration = 20.seconds override def munitTimeout: Duration = executionTimeout def ticked(options: TestOptions)(body: Ticker => Unit)(implicit loc: Location): Unit = @@ -148,7 +144,7 @@ trait Runners extends TestInstances with RunnersPlatform with DetectPlatform { .sleep( duration, { () => - if (p.tryFailure(new TestTimeoutException(s"test timed out after ${duration}"))) { + if (p.tryFailure(new TestTimeoutException)) { cancel() () } @@ -164,4 +160,4 @@ trait Runners extends TestInstances with RunnersPlatform with DetectPlatform { } } -class TestTimeoutException(msg: String) extends Exception(msg) +class TestTimeoutException extends Exception diff --git a/tests/shared/src/test/scala/cats/effect/kernel/RefSuite.scala b/tests/shared/src/test/scala/cats/effect/kernel/RefSuite.scala index 15b9f94cb2..425c860055 100644 --- a/tests/shared/src/test/scala/cats/effect/kernel/RefSuite.scala +++ b/tests/shared/src/test/scala/cats/effect/kernel/RefSuite.scala @@ -102,7 +102,7 @@ class RefSuite extends BaseSuite with DetectPlatform { outer => assertCompleteAs(op, true) } - if (!isJS) // concurrent modification impossible + if (!isJS && !isNative) // concurrent modification impossible ticked("tryUpdate - should fail to update if modification has occurred") { implicit ticker => val updateRefUnsafely: Ref[IO, Int] => Unit = { (ref: Ref[IO, Int]) => diff --git a/tests/shared/src/test/scala/cats/effect/std/DequeueSuite.scala b/tests/shared/src/test/scala/cats/effect/std/DequeueSuite.scala index c80ae901fe..18e825b629 100644 --- a/tests/shared/src/test/scala/cats/effect/std/DequeueSuite.scala +++ b/tests/shared/src/test/scala/cats/effect/std/DequeueSuite.scala @@ -23,9 +23,12 @@ import cats.implicits._ import org.scalacheck.Arbitrary.arbitrary import scala.collection.immutable.{Queue => ScalaQueue} +import scala.concurrent.duration._ class BoundedDequeueSuite extends BaseSuite with DequeueTests { + override def executionTimeout = 20.seconds + boundedDequeueTests( "BoundedDequeue (forward)", Dequeue.bounded(_), diff --git a/tests/shared/src/test/scala/cats/effect/std/DispatcherSuite.scala b/tests/shared/src/test/scala/cats/effect/std/DispatcherSuite.scala index 70a6ba24bd..eaf2f3756f 100644 --- a/tests/shared/src/test/scala/cats/effect/std/DispatcherSuite.scala +++ b/tests/shared/src/test/scala/cats/effect/std/DispatcherSuite.scala @@ -28,7 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger class DispatcherSuite extends BaseSuite with DetectPlatform { - override def executionTimeout = super.executionTimeout * 3 / 2 + override def executionTimeout = 30.seconds { val D = Dispatcher.sequential[IO](await = true) diff --git a/tests/shared/src/test/scala/cats/effect/std/MutexSuite.scala b/tests/shared/src/test/scala/cats/effect/std/MutexSuite.scala index 47280498c3..a19fbce10d 100644 --- a/tests/shared/src/test/scala/cats/effect/std/MutexSuite.scala +++ b/tests/shared/src/test/scala/cats/effect/std/MutexSuite.scala @@ -25,7 +25,7 @@ import scala.concurrent.duration._ final class MutexSuite extends BaseSuite with DetectPlatform { - final override def executionTimeout = super.executionTimeout * 6 + final override def executionTimeout = 2.minutes tests("ConcurrentMutex", Mutex.apply[IO]) tests("Mutex with dual constructors", Mutex.in[IO, IO]) diff --git a/tests/shared/src/test/scala/cats/effect/std/PQueueSuite.scala b/tests/shared/src/test/scala/cats/effect/std/PQueueSuite.scala index 77fcaf58ad..3fb625e32e 100644 --- a/tests/shared/src/test/scala/cats/effect/std/PQueueSuite.scala +++ b/tests/shared/src/test/scala/cats/effect/std/PQueueSuite.scala @@ -29,9 +29,12 @@ import cats.implicits._ import org.scalacheck.Arbitrary.arbitrary import scala.collection.immutable.{Queue => ScalaQueue} +import scala.concurrent.duration._ class BoundedPQueueSuite extends BaseSuite with PQueueTests { + override def executionTimeout = 20.seconds + implicit val orderForInt: Order[Int] = Order.fromLessThan((x, y) => x < y) boundedPQueueTests("PQueue", PQueue.bounded) @@ -110,6 +113,8 @@ class BoundedPQueueSuite extends BaseSuite with PQueueTests { class UnboundedPQueueSuite extends BaseSuite with PQueueTests { + override def executionTimeout = 20.seconds + implicit val orderForInt: Order[Int] = Order.fromLessThan((x, y) => x < y) unboundedPQueueTests("UnboundedPQueue", PQueue.unbounded) diff --git a/tests/shared/src/test/scala/cats/effect/std/QueueSuite.scala b/tests/shared/src/test/scala/cats/effect/std/QueueSuite.scala index 27299213c4..9fd1c44497 100644 --- a/tests/shared/src/test/scala/cats/effect/std/QueueSuite.scala +++ b/tests/shared/src/test/scala/cats/effect/std/QueueSuite.scala @@ -135,7 +135,7 @@ class BoundedQueueSuite extends BaseSuite with QueueTests[Queue] with DetectPlat _ <- IO.race(taker.joinWithNever, q.offer(()).delayBy(500.millis)) } yield () - test.parReplicateA_(if (isJS) 1 else 1000) + test.parReplicateA_(if (isJS || isNative) 1 else 1000) } private def boundedQueueTests(name: String, constructor: Int => IO[Queue[IO, Int]]) = { @@ -300,7 +300,7 @@ class BoundedQueueSuite extends BaseSuite with QueueTests[Queue] with DetectPlat } real(s"$name - offer/take at high contention") { - val size = if (isJS) 10000 else 100000 + val size = if (isJS || isNative) 10000 else 100000 val action = constructor(size) flatMap { q => def par(action: IO[Unit], num: Int): IO[Unit] = diff --git a/tests/shared/src/test/scala/cats/effect/std/RandomSuite.scala b/tests/shared/src/test/scala/cats/effect/std/RandomSuite.scala index 61d7b17bbb..94d5cdba13 100644 --- a/tests/shared/src/test/scala/cats/effect/std/RandomSuite.scala +++ b/tests/shared/src/test/scala/cats/effect/std/RandomSuite.scala @@ -51,27 +51,6 @@ class RandomSuite extends BaseSuite { } yield assert(randDoubles.forall(randDouble => randDouble >= min && randDouble <= max)) } - real(s"$name - betweenDouble - handle overflow") { - for { - random <- randomGen - randDoubles <- random.betweenDouble(Double.MinValue, Double.MaxValue).replicateA(100) - } yield assert { - randDoubles.forall { randDouble => - // this specific value means there was an unhandled overflow: - randDouble != 1.7976931348623155e308 - } - } - } - - real(s"$name - betweenDouble - handle underflow") { - for { - random <- randomGen - randDouble <- random.betweenDouble( - Double.MinPositiveValue, - java.lang.Math.nextUp(Double.MinPositiveValue)) - } yield assert(randDouble == Double.MinPositiveValue) - } - real(s"$name - betweenFloat - generate a random float within a range") { val min: Float = 0.0f val max: Float = 1.0f @@ -82,27 +61,6 @@ class RandomSuite extends BaseSuite { } yield assert(randFloats.forall(randFloat => randFloat >= min && randFloat <= max)) } - real(s"$name - betweenFloat - handle overflow") { - for { - random <- randomGen - randFloats <- random.betweenFloat(Float.MinValue, Float.MaxValue).replicateA(100) - } yield assert { - randFloats.forall { randFloat => - // this specific value means there was an unhandled overflow: - randFloat != 3.4028233e38f - } - } - } - - real(s"$name - betweenFloat - handle underflow") { - for { - random <- randomGen - randFloat <- random.betweenFloat( - Float.MinPositiveValue, - java.lang.Math.nextUp(Float.MinPositiveValue)) - } yield assert(randFloat == Float.MinPositiveValue) - } - real(s"$name - betweenInt - generate a random integer within a range") { val min: Integer = 0 val max: Integer = 10 diff --git a/tests/shared/src/test/scala/cats/effect/std/UnsafeBoundedSuite.scala b/tests/shared/src/test/scala/cats/effect/std/UnsafeBoundedSuite.scala index b0e30681ff..b5ca0e2040 100644 --- a/tests/shared/src/test/scala/cats/effect/std/UnsafeBoundedSuite.scala +++ b/tests/shared/src/test/scala/cats/effect/std/UnsafeBoundedSuite.scala @@ -24,7 +24,7 @@ import scala.concurrent.duration._ class UnsafeBoundedSuite extends BaseSuite { import Queue.UnsafeBounded - override def executionTimeout = super.executionTimeout * 3 / 2 + override def executionTimeout = 30.seconds // NB: emperically, it seems this needs to be > availableProcessors() to be effective val length = 1000