Skip to content

Commit

Permalink
Add --longRunning option to link incrementally in a long living proce…
Browse files Browse the repository at this point in the history
…ss (#64)

This works via a `stdin`-`stdout` protocol
the program works like this:
1.  it performs one linking
2. prints `SCALA_JS_LINKING_DONE\n` to stdin
3. waits until a `\n` is printed to `stdin`
4. goes to 1.

This allows the Scala.js toolchain to perform incremental linking and be much faster to execute
  • Loading branch information
lolgab authored May 22, 2024
1 parent 70b88e6 commit 05c15ae
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 31 deletions.
80 changes: 49 additions & 31 deletions cli/src/org/scalajs/cli/Scalajsld.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ object Scalajsld {
stdLib: Seq[File] = Nil,
jsHeader: String = "",
logLevel: Level = Level.Info,
importMap: Option[File] = None
importMap: Option[File] = None,
longRunning: Boolean = false
)

private def moduleInitializer(
Expand Down Expand Up @@ -234,6 +235,9 @@ object Scalajsld {
opt[String]("jsHeader")
.action { (jsHeader, c) => c.copy(jsHeader = jsHeader) }
.text("A header that will be added at the top of generated .js files")
opt[Unit]("longRunning")
.action { (_, c) => c.copy(longRunning = true) }
.text("Run linking incrementally every time a line is printed to stdin")
opt[Unit]('d', "debug")
.action { (_, c) => c.copy(logLevel = Level.Debug) }
.text("Debug mode: Show full log")
Expand Down Expand Up @@ -315,39 +319,53 @@ object Scalajsld {
val logger = new ScalaConsoleLogger(options.logLevel)
val cache = StandardImpl.irFileCache().newCache

val result = PathIRContainer
.fromClasspath(classpath)
.flatMap(containers => cache.cached(containers._1))
.flatMap { irFiles: Seq[IRFile] =>
val stdinLinesIterator = scala.io.Source.stdin.getLines()

val irImportMappedFiles = options.importMap match {
case None => irFiles
case Some(importMap) => ImportMapJsonIr.remapImports(importMap, irFiles)
}
while({
val result = PathIRContainer
.fromClasspath(classpath)
.flatMap(containers => cache.cached(containers._1))
.flatMap { irFiles: Seq[IRFile] =>

val irImportMappedFiles = options.importMap match {
case None => irFiles
case Some(importMap) => ImportMapJsonIr.remapImports(importMap, irFiles)
}

(options.output, options.outputDir) match {
case (Some(jsFile), None) =>
(DeprecatedLinkerAPI: DeprecatedLinkerAPI).link(
linker,
irImportMappedFiles.toList,
moduleInitializers,
jsFile,
logger
)
case (None, Some(outputDir)) =>
linker.link(
irImportMappedFiles,
moduleInitializers,
PathOutputDirectory(outputDir.toPath()),
logger
)
case _ =>
throw new AssertionError(
"Either output or outputDir have to be defined."
)
(options.output, options.outputDir) match {
case (Some(jsFile), None) =>
(DeprecatedLinkerAPI: DeprecatedLinkerAPI).link(
linker,
irImportMappedFiles.toList,
moduleInitializers,
jsFile,
logger
)
case (None, Some(outputDir)) =>
linker.link(
irImportMappedFiles,
moduleInitializers,
PathOutputDirectory(outputDir.toPath()),
logger
)
case _ =>
throw new AssertionError(
"Either output or outputDir have to be defined."
)
}
}
}
Await.result(result, Duration.Inf)
Await.result(result, Duration.Inf)

if (options.longRunning) {
// print SCALA_JS_LINKING_DONE\n everytime one linking succeeds
println("SCALA_JS_LINKING_DONE")

if (stdinLinesIterator.hasNext) {
stdinLinesIterator.next()
true
} else false
} else false
}) {}
}
}

Expand Down
106 changes: 106 additions & 0 deletions tests/test/src/org/scalajs/cli/tests/Tests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,112 @@ class Tests extends munit.FunSuite {
assert(splitRunOutput == "asdf 2")
}

test("longRunning") {
val dir = os.temp.dir()
def writePrintlnMain(stringToPrint: String) = {
os.write.over(
dir / "foo.scala",
s"""object Foo {
| def main(args: Array[String]): Unit = {
| println("$stringToPrint")
| }
|}
|""".stripMargin
)
}

val scalaJsLibraryCp = getScalaJsLibraryCp(dir)

os.makeDir.all(dir / "bin")

def compile() = {
val compileCommand = os.proc(
"cs",
"launch",
"scalac:2.13.14",
"--",
"-classpath",
scalaJsLibraryCp,
s"-Xplugin:${getScalaJsCompilerPlugin(dir)}",
"-d",
"bin",
"foo.scala"
)
compileCommand.call(cwd = dir, stdin = os.Inherit, stdout = os.Inherit)
}

val command = os
.proc(
launcher,
"--stdlib",
scalaJsLibraryCp,
"-s",
"-o",
"test.js",
"-mm",
"Foo.main",
"--longRunning",
"bin",
)

writePrintlnMain("first version")
compile()
val process = command.spawn(cwd = dir)

def waitForLinkingToFinish() = {
while({
val line = process.stdout.readLine()
assert(line != null, "Got null from reading stdout")
line != "SCALA_JS_LINKING_DONE"
}) {}
}

try {
locally {
waitForLinkingToFinish()
val testJsSize = os.size(dir / "test.js")
val testJsMapSize = os.size(dir / "test.js.map")
assert(testJsSize > 0)
assert(testJsMapSize > 0)

val runRes = os.proc("node", "test.js").call(cwd = dir)
val runOutput = runRes.out.trim()
assert(runOutput == "first version")
}

writePrintlnMain("second version")
compile()

// trigger new linking
process.stdin.writeLine("")
process.stdin.flush()

waitForLinkingToFinish()

locally {
val testJsSize = os.size(dir / "test.js")
val testJsMapSize = os.size(dir / "test.js.map")
assert(testJsSize > 0)
assert(testJsMapSize > 0)

val runRes = os.proc("node", "test.js").call(cwd = dir)
val runOutput = runRes.out.trim()
assertEquals(runOutput, "second version")
}

// close stdin to allow the process to terminate gracefully
process.stdin.close()

// wait some time for the process to terminate
Thread.sleep(100)

assert(!process.isAlive(), "process did not terminate gracefully")
} finally {

process.close()
}
}

test("fullLinkJs mode does not throw") {
val dir = os.temp.dir()
os.write(
Expand Down

0 comments on commit 05c15ae

Please sign in to comment.