Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scala.js: Isolate linker in a separate classloader #629

Merged
merged 24 commits into from
Feb 24, 2022

Conversation

keynmol
Copy link
Collaborator

@keynmol keynmol commented Feb 20, 2022

Closes #601 #617 #590

  1. Remove Scala.js compiler dependency from mdoc
  2. Remove direct linker/linker-interface dependency
  3. Isolate linker in a separate class loader - this should address issues with conflicting IR versions on the classpath
    • This introduces two new modules: jsWorkerApi (Java) and jsWorker (cross Scala)
  4. Auto-detect Scala.js version from the SBT plugin
  5. Mild cleanup of interactions with SJS linker inside the modifier
  6. Remove usage of deprecated linker APIs
  7. Updates Scala.js to 1.9.0

@keynmol keynmol force-pushed the scalajs-classloaders branch from 5471c47 to 8f89013 Compare February 20, 2022 15:37
@keynmol
Copy link
Collaborator Author

keynmol commented Feb 20, 2022

As a motivating example, here's the class paths:

 Compile classpath: List(
    file:////home/runner/work/mdoc/mdoc/tests/jsdocs/target/scala-3.1.1/classes, 
    file:////home/runner/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-lang/scala3-library_sjs1_3/3.1.1/scala3-library_sjs1_3-3.1.1.jar, 
    file:////home/runner/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-js/scalajs-library_2.13/1.9.0/scalajs-library_2.13-1.9.0.jar, 
    file:////home/runner/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-js/scalajs-dom_sjs1_3/2.0.0/scalajs-dom_sjs1_3-2.0.0.jar, 
    file:////home/runner/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-lang/scala-library/2.13.8/scala-library-2.13.8.jar)

 Linker classpath: List(
    file:////home/runner/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-js/scalajs-linker_2.13/1.9.0/scalajs-linker_2.13-1.9.0.jar, 
    file:////home/runner/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-lang/scala-library/2.13.8/scala-library-2.13.8.jar, 
    file:////home/runner/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-js/scalajs-linker-interface_2.13/1.9.0/scalajs-linker-interface_2.13-1.9.0.jar, 
    file:////home/runner/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-js/scalajs-ir_2.13/1.9.0/scalajs-ir_2.13-1.9.0.jar, 
    file:////home/runner/.cache/coursier/v1/https/repo1.maven.org/maven2/com/google/javascript/closure-compiler/v20220202/closure-compiler-v20220202.jar, 
    file:////home/runner/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-lang/modules/scala-parallel-collections_2.13/0.2.0/scala-parallel-collections_2.13-0.2.0.jar, 
    file:////home/runner/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-js/scalajs-logging_2.13/1.1.1/scalajs-logging_2.13-1.1.1.jar)
  1. The "compiler class path" is passed to the compiler
  2. The linker class path is used to instantiate the ClassLoader in which linker is isolated
  3. The compiler class path is analysed by PathIRContainer

@keynmol keynmol changed the title Scala.js: Do horrible things with classloaders Scala.js: Isolate linker in a separate classloader Feb 21, 2022
build.sbt Outdated
Comment on lines 9 to 15
// This will work as long as mdoc has scala-js SBT plugin
def scalajs = {
val klass =
Class.forName("org.scalajs.ir.ScalaJSVersions", true, getClass.getClassLoader())
val method = klass.getMethod("current")
method.invoke(null).asInstanceOf[String]
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any concern of sbt loading different plugins in different classloaders? I may have encountered this but never confirmed it.

@armanbilge
Copy link
Contributor

🎉 😍

@lolgab
Copy link

lolgab commented Feb 22, 2022

Since the ScalaJS toolchain is available for the same Scala version as the one you're using, I think you can write the API module in Scala if you want. We use Java in Mill only when the Worker implementation uses a different Scala binary version than the Plugin. In your case everything is on Scala 2.12

Comment on lines +69 to +73
lazy val mdocJSWorkerClasspath = taskKey[Option[Seq[File]]](
"Optional classpath to use for Mdoc.js worker - " +
"if not provided, the classpath will be formed by resolving the worker dependency"
)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this separate task (not part of autoImport) to make mdoc's own tests to work - we can't introduce a dependency on mdoc-js-worker, and this would be reliable than placing dependsOn(jsWorker / publishLocal) in all the possible situations where this module must be available.

Comment on lines +153 to +159
MdocJSConfiguration(
scalacOptions = options.options,
compileClasspath = options.classpath,
linkerClassPath = getJars(linkerDependency) ++ workerClasspath,
moduleKind = options.moduleKind,
jsLibraries = libraries
).writeTo(props)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a strictly unnecessary refactor, but it's a small step towards my goal of weaning off mdoc's build of loading MdocPlugin at build time.

Main issue is that it forces 2.12 on all the modules that are used to build the site
It also causes some weird issues when compiling the build, i.e. I currently can't resolve the deprecation warnings because the build just fails to compile when ++2.12 is used

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sbt on 2.13 would be so nice.

Comment on lines -120 to -123
props.put(
s"js-libraries",
mdocJSLibraries.value.map(_.data).mkString(File.pathSeparator)
)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I were to guess, this setting was extracted from the block above because SBT disallows invoking the task in anonymous lambda, and not because it's a valid usecase to not have mdoc.js configuration but still use js-libraries?

@@ -1,7 +1,7 @@
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0")
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.1")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.9.0")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upgrade Mdoc's own Scala.js to 1.9.0

mdocJS := Some(jsdocs)
mdocJS := Some(jsdocs),
MdocPlugin.mdocJSWorkerClasspath := {
val _ = (jsWorker / Compile / compile).value
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dependency on classDirectory is not enough to trigger compilation.
The user's build will not include this setting (mdocJSWorkerClasspath) at all, it's here only because mdoc bootstraps itself

@@ -1,2 +1,2 @@
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.0")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.9.0")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This version is intended to be updated with each new release of Scala.js

@@ -0,0 +1,2 @@
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.0")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frozen tests at this Scala.js version as it's the last one that was working in the old setup of the plugin, so we want to keep it here to make sure future changes won't break older versions.

@keynmol keynmol requested review from tgodzik and olafurpg February 22, 2022 16:11
Copy link
Contributor

@tgodzik tgodzik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just some questions from me, but nothing blocking.

.fromClasspath(x.toSeq)
.map(_._1)
.flatMap(cache.cached),
Duration.Inf
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this not cause any issues to use Duration.Inf? Maybe we should use CompletableFuture as the result value instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest the entire JsModifier process is very synchronous and futures are always waited on, even in the current main branch.

We can always change the interface as it is internal, so I wanted to keep it simple and avoid introducing potentially configurable timeouts because its brittle.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok sure!

val linking =
linker.link(virtualIrFiles ++ sjsir, Nil, LinkerOutput.apply(output), sjsLogger)
Await.result(linking, Duration.Inf)
val linkingReport = scalajsApi.get.link(sjsirFiles.toArray)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we match on the Option instead? Or maybe throw a better exception if scalajsApi is empty?

Copy link
Member

@olafurpg olafurpg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍 Just a few minor comments, feel free to ignore. Thank you for looking into this! It's hairy business to fiddle with classloaders, but it's really wonderful IMO that the JVM makes it possible at all

build.sbt Outdated
@@ -395,22 +414,30 @@ lazy val plugin = project
)
.enablePlugins(ScriptedPlugin)

lazy val jsApi =
project
.in(file("mdoc-js-api"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we name this mdoc-js-interfaces for consistency with mtags-interfaces? (and other artifacts using the -interfaces suffix)

ScalaJSClassloader.create(linkerClasspath.entries.map(_.toURI.toURL()).toArray)
scalajsApi = Some(
loader
.loadClass("mdoc.js.worker.ScalaJSWorker")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use service loaders here instead of manual reflection? Service loaders have several tooling benefits, for example they are automatically understood by native-image (although that's not relevant here). https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html

import java.net.URLClassLoader

final class FilteringClassLoader(parent: ClassLoader) extends ClassLoader(parent) {
private val parentPrefixes = List(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the loadClass is a fairly hot path so I'd try to avoid List here

Suggested change
private val parentPrefixes = List(
private val parentPrefixes = Array(

Comment on lines +153 to +159
MdocJSConfiguration(
scalacOptions = options.options,
compileClasspath = options.classpath,
linkerClassPath = getJars(linkerDependency) ++ workerClasspath,
moduleKind = options.moduleKind,
jsLibraries = libraries
).writeTo(props)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sbt on 2.13 would be so nice.

case Level.Error => Error
}
}
def create(config: i.ScalajsConfig, logger: i.ScalajsLogger) = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing result type

.newInstance(scalajsConfig(new ScalajsConfig, config), sjsLogger)
.asInstanceOf[ScalajsWorkerApi]
ServiceLoader
.load(classOf[ScalajsWorkerProvider], loader)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@keynmol keynmol merged commit 9014e2d into scalameta:main Feb 24, 2022
@keynmol keynmol deleted the scalajs-classloaders branch February 24, 2022 09:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

mdoc.js is brittle to Scala.js updates
6 participants