mirror of https://github.com/sbt/sbt.git
Improve legacy launcher
To minimize classloading and consistency between sbt instances launched with the latest launcher compared to old launchers, I overhauled code that replaces the app configuration and meta build classloader at startup. The goals of this change for legacy launchers were: 1) Do not ever load the scala-library.jar from the app provider class loader. 2) Close the class loaders that are below the topLoader in the class loading hierarcy For the new launcher, we simply want to avoid modifying the loader at all. I added the SbtParserInit class so that it was more straightforward to preload the global instance using reflection. We now use reflection to instantiate an SbtParserInit instance for both the legacy and new launcher cases to simplify the logic. After this change, the legacy loader still uses somewhat more metaspace than the new loader, but the difference seems to be O(10MB), which should only impact projects that were close their MaxMetaspaceSize to begin with. I verified using javap that none of the code in this class uses the scala standard library which should help metaspace since we don't load much of the scala standard library until we enter xMainImpl.run.
This commit is contained in:
parent
e6d2b32902
commit
df628d4f87
|
|
@ -8,7 +8,6 @@
|
|||
package sbt
|
||||
|
||||
import java.io.{ File, IOException }
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.net.URI
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.{ Locale, Properties }
|
||||
|
|
@ -27,7 +26,6 @@ import sbt.io._
|
|||
import sbt.io.syntax._
|
||||
import sbt.util.{ Level, Logger, Show }
|
||||
import xsbti.compile.CompilerCache
|
||||
import xsbti.{ AppMain, AppProvider, ComponentProvider, Launcher, ScalaProvider }
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
|
@ -35,67 +33,8 @@ import scala.util.control.NonFatal
|
|||
|
||||
/** This class is the entry point for sbt. */
|
||||
final class xMain extends xsbti.AppMain {
|
||||
def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = {
|
||||
val modifiedConfiguration = new ModifiedConfiguration(configuration)
|
||||
val loader = modifiedConfiguration.provider.loader
|
||||
// No need to memoize the old class loader. It is reset by the launcher anyway.
|
||||
Thread.currentThread.setContextClassLoader(loader)
|
||||
val clazz = loader.loadClass("sbt.xMainImpl$")
|
||||
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 =>
|
||||
// This propogates xsbti.FullReload to the launcher
|
||||
throw e.getCause
|
||||
} finally {
|
||||
loader match {
|
||||
case a: AutoCloseable => a.close()
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Replaces the AppProvider.loader method with a new loader that puts the sbt test interface
|
||||
* jar ahead of the rest of the sbt classpath in the classloading hierarchy.
|
||||
*/
|
||||
private class ModifiedConfiguration(val configuration: xsbti.AppConfiguration)
|
||||
extends xsbti.AppConfiguration {
|
||||
private[this] val metaLoader: ClassLoader = SbtMetaBuildClassLoader(configuration.provider)
|
||||
|
||||
private class ModifiedAppProvider(val appProvider: AppProvider) extends AppProvider {
|
||||
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")
|
||||
override def mainClass(): Class[_ <: AppMain] = appProvider.mainClass()
|
||||
override def entryPoint(): Class[_] = appProvider.entryPoint()
|
||||
override def newMain(): AppMain = appProvider.newMain()
|
||||
override def mainClasspath(): Array[File] = appProvider.mainClasspath()
|
||||
override def components(): ComponentProvider = appProvider.components()
|
||||
}
|
||||
override def arguments(): Array[String] = configuration.arguments
|
||||
override def baseDirectory(): File = configuration.baseDirectory
|
||||
override def provider(): AppProvider = new ModifiedAppProvider(configuration.provider)
|
||||
}
|
||||
def run(configuration: xsbti.AppConfiguration): xsbti.MainResult =
|
||||
new XMainConfiguration().runXMain(configuration)
|
||||
}
|
||||
private[sbt] object xMainImpl {
|
||||
private[sbt] def run(configuration: xsbti.AppConfiguration): xsbti.MainResult =
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ package sbt
|
|||
package internal
|
||||
|
||||
import java.io.File
|
||||
import java.net.{ URL, URLClassLoader }
|
||||
import java.net.URLClassLoader
|
||||
|
||||
import sbt.ClassLoaderLayeringStrategy._
|
||||
import sbt.Keys._
|
||||
|
|
@ -20,7 +20,6 @@ import sbt.internal.util.Attributed
|
|||
import sbt.internal.util.Attributed.data
|
||||
import sbt.io.IO
|
||||
import sbt.librarymanagement.Configurations.{ Runtime, Test }
|
||||
import xsbti.AppProvider
|
||||
|
||||
private[sbt] object ClassLoaders {
|
||||
private[this] val interfaceLoader = classOf[sbt.testing.Framework].getClassLoader
|
||||
|
|
@ -200,26 +199,3 @@ private[sbt] object ClassLoaders {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
private[sbt] object SbtMetaBuildClassLoader {
|
||||
def apply(appProvider: AppProvider): ClassLoader = {
|
||||
val interfaceFilter: URL => Boolean = _.getFile.endsWith("test-interface-1.0.jar")
|
||||
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(urls(scalaProvider.jars), interfaceLoader) {
|
||||
override def toString: String = s"ScalaClassLoader(jars = {${getURLs.mkString(", ")}}"
|
||||
}
|
||||
new URLClassLoader(rest, updatedLibraryLoader) {
|
||||
override def toString: String = s"SbtMetaBuildClassLoader"
|
||||
override def close(): Unit = {
|
||||
super.close()
|
||||
updatedLibraryLoader.close()
|
||||
interfaceLoader.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
package sbt.internal
|
||||
|
||||
import java.io.File
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.net.{ URL, URLClassLoader }
|
||||
import java.util.regex.Pattern
|
||||
|
||||
import xsbti._
|
||||
|
||||
/**
|
||||
* Generates a new app configuration and invokes xMainImpl.run. For AppConfigurations generated
|
||||
* by recent launchers, it is unnecessary to modify the original configuration, but configurations
|
||||
* generated by older launchers need to be modified to place the test interface jar higher in
|
||||
* the class hierarchy. The methods this object are implemented without using the scala library
|
||||
* so that we can avoid loading any classes from the old scala provider. Verified as of
|
||||
* sbt 1.3.0 that there are no references to the scala standard library in any of the methods
|
||||
* in this file.
|
||||
*/
|
||||
private[sbt] class XMainConfiguration {
|
||||
private def close(classLoader: ClassLoader): Unit = classLoader match {
|
||||
case a: AutoCloseable => a.close()
|
||||
case _ =>
|
||||
}
|
||||
def runXMain(configuration: xsbti.AppConfiguration): xsbti.MainResult = {
|
||||
val updatedConfiguration =
|
||||
if (configuration.provider.scalaProvider.launcher.topLoader.getClass.getCanonicalName
|
||||
.contains("TestInterfaceLoader")) {
|
||||
configuration
|
||||
} else {
|
||||
makeConfiguration(configuration)
|
||||
}
|
||||
val loader = updatedConfiguration.provider.loader
|
||||
Thread.currentThread.setContextClassLoader(loader)
|
||||
val clazz = loader.loadClass("sbt.xMainImpl$")
|
||||
val instance = clazz.getField("MODULE$").get(null)
|
||||
val runMethod = clazz.getMethod("run", classOf[xsbti.AppConfiguration])
|
||||
try {
|
||||
loader.loadClass("sbt.internal.parser.SbtParserInit").getConstructor().newInstance()
|
||||
runMethod.invoke(instance, updatedConfiguration).asInstanceOf[xsbti.MainResult]
|
||||
} catch {
|
||||
case e: InvocationTargetException =>
|
||||
// This propogates xsbti.FullReload to the launcher
|
||||
throw e.getCause
|
||||
}
|
||||
}
|
||||
|
||||
private def makeConfiguration(configuration: xsbti.AppConfiguration): xsbti.AppConfiguration = {
|
||||
val baseLoader = classOf[XMainConfiguration].getClassLoader
|
||||
val url = baseLoader.getResource("sbt/internal/XMainConfiguration.class")
|
||||
val urlArray = new Array[URL](1)
|
||||
urlArray(0) = new URL(url.getPath.replaceAll("[!][^!]*class", ""))
|
||||
val topLoader = configuration.provider.scalaProvider.launcher.topLoader
|
||||
// This loader doesn't have the scala library in it so it's critical that none of the code
|
||||
// in this file use the scala library.
|
||||
val modifiedLoader = new URLClassLoader(urlArray, topLoader) {
|
||||
override def loadClass(name: String, resolve: Boolean): Class[_] = {
|
||||
if (name.startsWith("sbt.internal.XMainConfiguration")) {
|
||||
val clazz = findClass(name)
|
||||
if (resolve) resolveClass(clazz)
|
||||
clazz
|
||||
} else {
|
||||
super.loadClass(name, resolve)
|
||||
}
|
||||
}
|
||||
}
|
||||
val xMainConfigurationClass = modifiedLoader.loadClass("sbt.internal.XMainConfiguration")
|
||||
val instance: AnyRef =
|
||||
xMainConfigurationClass.getConstructor().newInstance().asInstanceOf[AnyRef]
|
||||
|
||||
val method = xMainConfigurationClass.getMethod("makeLoader", classOf[AppProvider])
|
||||
val modifiedConfigurationClass =
|
||||
modifiedLoader.loadClass("sbt.internal.XMainConfiguration$ModifiedConfiguration")
|
||||
|
||||
val loader = method.invoke(instance, configuration.provider).asInstanceOf[ClassLoader]
|
||||
|
||||
Thread.currentThread.setContextClassLoader(loader)
|
||||
val cons = modifiedConfigurationClass.getConstructors()(0)
|
||||
close(configuration.provider.loader)
|
||||
val scalaProvider = configuration.provider.scalaProvider
|
||||
val providerClass = scalaProvider.getClass
|
||||
val _ = try {
|
||||
val method = providerClass.getMethod("loaderLibraryOnly")
|
||||
close(method.invoke(scalaProvider).asInstanceOf[ClassLoader])
|
||||
1
|
||||
} catch { case _: NoSuchMethodException => 1 }
|
||||
close(scalaProvider.loader)
|
||||
close(configuration.provider.loader)
|
||||
cons.newInstance(instance, configuration, loader).asInstanceOf[xsbti.AppConfiguration]
|
||||
}
|
||||
|
||||
/*
|
||||
* Replaces the AppProvider.loader method with a new loader that puts the sbt test interface
|
||||
* jar ahead of the rest of the sbt classpath in the classloading hierarchy.
|
||||
*/
|
||||
private[sbt] class ModifiedConfiguration(
|
||||
val configuration: xsbti.AppConfiguration,
|
||||
val metaLoader: ClassLoader
|
||||
) extends xsbti.AppConfiguration {
|
||||
|
||||
private class ModifiedAppProvider(val appProvider: AppProvider) extends AppProvider {
|
||||
private val delegate = configuration.provider.scalaProvider
|
||||
object ModifiedScalaProvider extends ScalaProvider {
|
||||
override def launcher(): Launcher = new Launcher {
|
||||
private val delegateLauncher = delegate.launcher
|
||||
private val interfaceLoader = metaLoader.loadClass("sbt.testing.Framework").getClassLoader
|
||||
override def getScala(version: String): ScalaProvider = getScala(version, "")
|
||||
override def getScala(version: String, reason: String): ScalaProvider =
|
||||
getScala(version, reason, "org.scala-lang")
|
||||
override def getScala(version: String, reason: String, scalaOrg: String): ScalaProvider =
|
||||
delegateLauncher.getScala(version, reason, scalaOrg)
|
||||
override def app(id: xsbti.ApplicationID, version: String): AppProvider =
|
||||
delegateLauncher.app(id, version)
|
||||
override def topLoader(): ClassLoader = interfaceLoader
|
||||
override def globalLock(): GlobalLock = delegateLauncher.globalLock()
|
||||
override def bootDirectory(): File = delegateLauncher.bootDirectory()
|
||||
override def ivyRepositories(): Array[xsbti.Repository] =
|
||||
delegateLauncher.ivyRepositories()
|
||||
override def appRepositories(): Array[xsbti.Repository] =
|
||||
delegateLauncher.appRepositories()
|
||||
override def isOverrideRepositories: Boolean = delegateLauncher.isOverrideRepositories
|
||||
override def ivyHome(): File = delegateLauncher.ivyHome()
|
||||
override def checksums(): Array[String] = delegateLauncher.checksums()
|
||||
}
|
||||
override def version(): String = delegate.version
|
||||
override def loader(): ClassLoader = metaLoader.getParent
|
||||
override def jars(): Array[File] = delegate.jars
|
||||
@deprecated("Implements deprecated api", "1.3.0")
|
||||
override def libraryJar(): File = delegate.libraryJar
|
||||
@deprecated("Implements deprecated api", "1.3.0")
|
||||
override def compilerJar(): File = delegate.compilerJar
|
||||
override def app(id: xsbti.ApplicationID): AppProvider = delegate.app(id)
|
||||
def loaderLibraryOnly(): ClassLoader = metaLoader.getParent.getParent
|
||||
}
|
||||
override def scalaProvider(): ModifiedScalaProvider.type = ModifiedScalaProvider
|
||||
override def id(): xsbti.ApplicationID = appProvider.id()
|
||||
override def loader(): ClassLoader = metaLoader
|
||||
@deprecated("Implements deprecated api", "1.3.0")
|
||||
override def mainClass(): Class[_ <: AppMain] = appProvider.mainClass()
|
||||
override def entryPoint(): Class[_] = appProvider.entryPoint()
|
||||
override def newMain(): AppMain = appProvider.newMain()
|
||||
override def mainClasspath(): Array[File] = appProvider.mainClasspath()
|
||||
override def components(): ComponentProvider = appProvider.components()
|
||||
}
|
||||
override def arguments(): Array[String] = configuration.arguments
|
||||
override def baseDirectory(): File = configuration.baseDirectory
|
||||
override def provider(): AppProvider = new ModifiedAppProvider(configuration.provider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rearrange the classloaders so that test-interface is above the scala library. Implemented
|
||||
* without using the scala standard library to minimize classloading.
|
||||
* @param appProvider the appProvider that needs to be modified
|
||||
* @return a ClassLoader with a URLClassLoader for the test-interface-1.0.jar above the
|
||||
* scala library.
|
||||
*/
|
||||
private[sbt] def makeLoader(appProvider: AppProvider): ClassLoader = {
|
||||
val pattern = Pattern.compile("test-interface-[0-9.]+\\.jar")
|
||||
val cp = appProvider.mainClasspath
|
||||
val interfaceURL = new Array[URL](1)
|
||||
val rest = new Array[URL](cp.length - 1)
|
||||
|
||||
{
|
||||
var i = 0
|
||||
var j = 0 // index into rest
|
||||
while (i < cp.length) {
|
||||
val file = cp(i)
|
||||
if (pattern.matcher(file.getName).find()) {
|
||||
interfaceURL(0) = file.toURI.toURL
|
||||
} else {
|
||||
rest(j) = file.toURI.toURL
|
||||
j += 1
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
val scalaProvider = appProvider.scalaProvider
|
||||
val topLoader = scalaProvider.launcher.topLoader
|
||||
class InterfaceLoader extends URLClassLoader(interfaceURL, topLoader) {
|
||||
override def toString: String = "SbtTestInterfaceClassLoader(" + interfaceURL(0) + ")"
|
||||
}
|
||||
val interfaceLoader = new InterfaceLoader
|
||||
val siJars = scalaProvider.jars
|
||||
val lib = new Array[URL](1)
|
||||
val scalaRest = new Array[URL](siJars.length - 1)
|
||||
|
||||
{
|
||||
var i = 0
|
||||
var j = 0 // index into scalaRest
|
||||
while (i < siJars.length) {
|
||||
val file = siJars(i)
|
||||
if (file.getName.equals("scala-library.jar")) {
|
||||
lib(0) = file.toURI.toURL
|
||||
} else {
|
||||
scalaRest(j) = file.toURI.toURL
|
||||
j += 1
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
class LibraryLoader extends URLClassLoader(lib, interfaceLoader) {
|
||||
override def toString: String = "ScalaLibraryLoader( " + lib(0) + ")"
|
||||
}
|
||||
val libraryLoader = new LibraryLoader
|
||||
class FullLoader extends URLClassLoader(scalaRest, libraryLoader) {
|
||||
private val jarString: String = {
|
||||
val res = new java.lang.StringBuilder
|
||||
var i = 0
|
||||
while (i < scalaRest.length) {
|
||||
res.append(scalaRest(i).getPath)
|
||||
res.append(", ")
|
||||
i += 1
|
||||
}
|
||||
res.toString
|
||||
}
|
||||
override def toString: String = "ScalaClassLoader(jars = " + jarString + ")"
|
||||
}
|
||||
val fullLoader = new FullLoader
|
||||
class MetaBuildLoader extends URLClassLoader(rest, fullLoader) {
|
||||
override def toString: String = "SbtMetaBuildClassLoader"
|
||||
override def close(): Unit = {
|
||||
super.close()
|
||||
libraryLoader.close()
|
||||
fullLoader.close()
|
||||
interfaceLoader.close()
|
||||
}
|
||||
}
|
||||
new MetaBuildLoader
|
||||
}
|
||||
}
|
||||
|
|
@ -163,6 +163,16 @@ private[sbt] object SbtParser {
|
|||
}
|
||||
}
|
||||
|
||||
private class SbtParserInit {
|
||||
new Thread("sbt-parser-init-thread") {
|
||||
setDaemon(true)
|
||||
start()
|
||||
override def run(): Unit = {
|
||||
val _ = SbtParser.defaultGlobalForParser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method solely exists to add scaladoc to members in SbtParser which
|
||||
* are defined using pattern matching.
|
||||
|
|
|
|||
Loading…
Reference in New Issue