Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -263,11 +263,11 @@ jobs:
shell: bash
run: sbt +update

- name: Setup NodeJS v18 LTS
- name: Setup NodeJS v20 LTS
if: matrix.ci == 'ciJS'
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20

- name: Install jsdom and source-map-support
if: matrix.ci == 'ciJS'
Expand Down
29 changes: 29 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ 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._

Expand Down Expand Up @@ -295,6 +296,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))
Expand All @@ -309,6 +318,8 @@ ThisBuild / jsEnv := {
val options = new ChromeOptions()
options.setHeadless(true)
new SeleniumJSEnv(options)
case WASM =>
nodeJSWasmEnv
}
}

Expand Down Expand Up @@ -985,6 +996,24 @@ lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatf
githubWorkflowArtifactUpload := false
)
.jsSettings(
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)
.withOptimizer(false) // disable Optimizer for WASM
},
Compile / scalaJSUseMainModuleInitializer := true,
Compile / mainClass := Some("catseffect.examples.JSRunner"),
// The default configured mapSourceURI is used for trace filtering
Expand Down
1 change: 1 addition & 0 deletions core/js/src/main/scala/cats/effect/Platform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ private object Platform {
final val isJs = true
final val isJvm = false
final val isNative = false
final val isWasm = true

class static extends scala.annotation.Annotation
}
52 changes: 45 additions & 7 deletions core/js/src/main/scala/cats/effect/tracing/TracingConstants.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,56 @@ 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")

final val isFullStackTracing: Boolean = stackTracingMode.equalsIgnoreCase("full")

final val isStackTracing: Boolean = isFullStackTracing || isCachedStackTracing

final val isWasm: Boolean =
System.getenv("WASM_MODE") == "true" ||
js.typeOf(js.Dynamic.global.WebAssembly) != "undefined"

// Singleton event for identical functions
final val WASM_IDENTICAL_EVENT: TracingEvent =
TracingEvent.WasmTrace(Array.empty, isIdentical = true)

// Cache for unique traces
private[this] val uniqueTraceCache =
if (isWasm) js.Dictionary.empty[TracingEvent]
else null

def createUniqueWasmTrace(): TracingEvent = {
if (isWasm) {
val event = TracingEvent.WasmTrace(Array.empty)
uniqueTraceCache(event.hashCode().toString) = event
event
} else {
TracingEvent.WasmTrace(Array.empty)
}
}

def markAsIdentical(f: AnyRef): Unit = {
if (isWasm) {
f.asInstanceOf[js.Dynamic].__cats_effect_identical = true
}
}

def isIdenticalFunction(f: AnyRef): Boolean = {
isWasm &&
js.typeOf(f.asInstanceOf[js.Dynamic].__cats_effect_identical) == "boolean" &&
f.asInstanceOf[js.Dynamic].__cats_effect_identical.asInstanceOf[Boolean]
}
}
135 changes: 93 additions & 42 deletions core/js/src/main/scala/cats/effect/tracing/TracingPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,48 +23,94 @@ 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 => ""
}
}
}

def calculateTracingEvent[A](f: Function0[A]): TracingEvent = {
calculateTracingEvent(
f.asInstanceOf[js.Dynamic].selectDynamic(function0Property).toString())
if (isWasm) {
if (isIdenticalFunction(f.asInstanceOf[AnyRef])) WASM_IDENTICAL_EVENT
else createUniqueWasmTrace()
} 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 (isIdenticalFunction(f.asInstanceOf[AnyRef])) WASM_IDENTICAL_EVENT
else createUniqueWasmTrace()
} 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) createUniqueWasmTrace()
else {
try {
calculateTracingEvent(cont.getClass())
} catch {
case _: Throwable => null
}
}
}

private[this] final val calculateTracingEvent: Any => TracingEvent = {
if (LinkingInfo.developmentMode) {
if (isWasm) { key =>
if (isIdenticalFunction(key.asInstanceOf[AnyRef])) WASM_IDENTICAL_EVENT
else TracingEvent.WasmTrace(Array.empty)
} 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/",
Expand All @@ -73,54 +119,59 @@ 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_",
"_Lorg_scalajs_"
)

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(
callSiteClassName: String,
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 == "<jscode>" &&
(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))
}
}

}
1 change: 1 addition & 0 deletions core/jvm/src/main/scala/cats/effect/Platform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ private object Platform {
final val isJs = false
final val isJvm = true
final val isNative = false
final val isWasm = false

type static = org.typelevel.scalaccompat.annotation.static3
class safePublish extends scala.annotation.Annotation
Expand Down
1 change: 1 addition & 0 deletions core/native/src/main/scala/cats/effect/Platform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ private object Platform {
final val isJs = false
final val isJvm = false
final val isNative = true
final val isWasm = false

class static extends scala.annotation.Annotation
type safePublish = scala.scalanative.annotation.safePublish
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
* limitations under the License.
*/

package cats.effect
package tracing
package cats.effect.tracing

private[effect] object TracingConstants {

Expand All @@ -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()
}
Loading
Loading