From 418e7e09fd8c86c0e1a3d580ebd85b42be398639 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Mon, 13 May 2019 11:02:08 -0700 Subject: [PATCH 1/2] Shave O(500ms) off of sbt startup It turns out that it can take roughly one second to instantiate a scala.nsc.tools.Global instance for the first time. When sbt is starting up, it also takes nearly 2 seconds to initialize logging. We can speed up the boot time by doing these two things concurrently. On my machine, I saw on average a 500ms decrease in startup time after this change. --- main/src/main/scala/sbt/Main.scala | 8 ++++++++ main/src/main/scala/sbt/internal/parser/SbtParser.scala | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 05bdda9c1..134299a51 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -44,6 +44,14 @@ final class xMain extends xsbti.AppMain { val instance = clazz.getField("MODULE$").get(null) val runMethod = clazz.getMethod("run", classOf[xsbti.AppConfiguration]) try { + new Thread("sbt-load-global-instance") { + setDaemon(true) + override def run(): Unit = { + // This preloads the scala.tools.nsc.Global as a performance optimization" + loader.loadClass("sbt.internal.parser.SbtParser$").getField("MODULE$").get(null) + () + } + }.start() runMethod.invoke(instance, modifiedConfiguration).asInstanceOf[xsbti.MainResult] } catch { case e: InvocationTargetException => diff --git a/main/src/main/scala/sbt/internal/parser/SbtParser.scala b/main/src/main/scala/sbt/internal/parser/SbtParser.scala index 22020d30d..c6d0ac2ca 100644 --- a/main/src/main/scala/sbt/internal/parser/SbtParser.scala +++ b/main/src/main/scala/sbt/internal/parser/SbtParser.scala @@ -108,7 +108,7 @@ private[sbt] object SbtParser { private[sbt] final val globalReporter = new UniqueParserReporter private[sbt] var scalacGlobalInitReporter: Option[ConsoleReporter] = None - private[sbt] final lazy val defaultGlobalForParser = { + private[sbt] final val defaultGlobalForParser = { val options = "-cp" :: s"$defaultClasspath" :: "-Yrangepos" :: Nil val reportError = (msg: String) => System.err.println(msg) val command = new CompilerCommand(options, reportError) From 54412d8c594cf0e4d163db3dc516b6807e91e15e Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Mon, 13 May 2019 13:42:11 -0700 Subject: [PATCH 2/2] Improve ScalaMetaBuildClassLoader construction I realized that all of the data structures that I needed to isolate the classpath are contained in the AppProvider interface so there was no need to use structural reflection on the top class loader. --- main/src/main/scala/sbt/Main.scala | 18 +++++++--- .../scala/sbt/internal/ClassLoaders.scala | 36 ++++--------------- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 134299a51..32b314ed4 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -27,7 +27,7 @@ import sbt.io._ import sbt.io.syntax._ import sbt.util.{ Level, Logger, Show } import xsbti.compile.CompilerCache -import xsbti.{ AppMain, AppProvider, ComponentProvider, ScalaProvider } +import xsbti.{ AppMain, AppProvider, ComponentProvider, Launcher, ScalaProvider } import scala.annotation.tailrec import scala.concurrent.ExecutionContext @@ -70,11 +70,19 @@ final class xMain extends xsbti.AppMain { */ private class ModifiedConfiguration(val configuration: xsbti.AppConfiguration) extends xsbti.AppConfiguration { - private[this] val initLoader = configuration.provider.loader - private[this] val scalaLoader = configuration.provider.scalaProvider.loader - private[this] val metaLoader: ClassLoader = SbtMetaBuildClassLoader(scalaLoader, initLoader) + private[this] val metaLoader: ClassLoader = SbtMetaBuildClassLoader(configuration.provider) + private class ModifiedAppProvider(val appProvider: AppProvider) extends AppProvider { - override def scalaProvider(): ScalaProvider = appProvider.scalaProvider + override def scalaProvider(): ScalaProvider = new ScalaProvider { + val delegate = configuration.provider.scalaProvider + override def launcher(): Launcher = delegate.launcher + override def version(): String = delegate.version + override def loader(): ClassLoader = metaLoader.getParent + override def jars(): Array[File] = delegate.jars + override def libraryJar(): File = delegate.libraryJar + override def compilerJar(): File = delegate.compilerJar + override def app(id: xsbti.ApplicationID): AppProvider = delegate.app(id) + } override def id(): xsbti.ApplicationID = appProvider.id() override def loader(): ClassLoader = metaLoader @deprecated("Implements deprecated api", "1.3.0") diff --git a/main/src/main/scala/sbt/internal/ClassLoaders.scala b/main/src/main/scala/sbt/internal/ClassLoaders.scala index 0222c1da5..8e4221f59 100644 --- a/main/src/main/scala/sbt/internal/ClassLoaders.scala +++ b/main/src/main/scala/sbt/internal/ClassLoaders.scala @@ -20,8 +20,7 @@ import sbt.internal.util.Attributed import sbt.internal.util.Attributed.data import sbt.io.IO import sbt.librarymanagement.Configurations.{ Runtime, Test } - -import scala.annotation.tailrec +import xsbti.AppProvider private[sbt] object ClassLoaders { private[this] val interfaceLoader = classOf[sbt.testing.Framework].getClassLoader @@ -202,36 +201,15 @@ private[sbt] object ClassLoaders { } private[sbt] object SbtMetaBuildClassLoader { - private[this] implicit class Ops(val c: ClassLoader) { - def urls: Array[URL] = c match { - case u: URLClassLoader => u.getURLs - case cl => - throw new IllegalStateException(s"sbt was launched with a non URLClassLoader: $cl") - } - } - def apply(scalaProviderLoader: ClassLoader, fullLoader: ClassLoader): ClassLoader = { - val libFilter: URL => Boolean = _.getFile.endsWith("scala-library.jar") - def bootLoader(loader: ClassLoader): ClassLoader = { - @tailrec def getAllParents( - classLoader: ClassLoader, - parents: List[ClassLoader] - ): List[ClassLoader] = classLoader.getParent match { - case null => parents - case cl => getAllParents(cl, classLoader :: parents) - } - @tailrec def getLoader(remaining: List[ClassLoader]): ClassLoader = remaining match { - case head :: (next: URLClassLoader) :: _ if next.getURLs.exists(libFilter) => head - case head :: Nil => head - case _ :: tail => getLoader(tail) - } - getLoader(getAllParents(loader, Nil)) - } + def apply(appProvider: AppProvider): ClassLoader = { val interfaceFilter: URL => Boolean = _.getFile.endsWith("test-interface-1.0.jar") - val (interfaceURL, rest) = fullLoader.urls.partition(interfaceFilter) - val interfaceLoader = new URLClassLoader(interfaceURL, bootLoader(scalaProviderLoader)) { + def urls(jars: Array[File]): Array[URL] = jars.map(_.toURI.toURL) + val (interfaceURL, rest) = urls(appProvider.mainClasspath).partition(interfaceFilter) + val scalaProvider = appProvider.scalaProvider + val interfaceLoader = new URLClassLoader(interfaceURL, scalaProvider.launcher.topLoader) { override def toString: String = s"SbtTestInterfaceClassLoader(${getURLs.head})" } - val updatedLibraryLoader = new URLClassLoader(scalaProviderLoader.urls, interfaceLoader) { + val updatedLibraryLoader = new URLClassLoader(urls(scalaProvider.jars), interfaceLoader) { override def toString: String = s"ScalaClassLoader(jars = {${getURLs.mkString(", ")}}" } new URLClassLoader(rest, updatedLibraryLoader) {