Merge pull request #7866 from adpi2/2.x-reduce-heap

[2.x] Reduce number of long-living instances to speed up loading
This commit is contained in:
adpi2 2024-11-06 08:44:03 +01:00 committed by GitHub
commit aa5e252d22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 91 additions and 77 deletions

View File

@ -10,6 +10,8 @@ package sbt.internal.util
import java.util.Locale import java.util.Locale
import scala.collection.concurrent.TrieMap
object Util { object Util {
def makeList[T](size: Int, value: T): List[T] = List.fill(size)(value) def makeList[T](size: Int, value: T): List[T] = List.fill(size)(value)
@ -71,4 +73,9 @@ object Util {
extension [A](value: A) { extension [A](value: A) {
def some: Option[A] = (Some(value): Option[A]) def some: Option[A] = (Some(value): Option[A])
} }
private[sbt] def withCaching[A1, A2](f: A1 => A2): A1 => A2 = {
val cache = TrieMap.empty[A1, A2]
x => cache.getOrElseUpdate(x, f(x))
}
} }

View File

@ -15,6 +15,8 @@ import sbt.internal.util.Dag
import sbt.internal.util.complete.Parser import sbt.internal.util.complete.Parser
import sbt.internal.util.complete.DefaultParsers import sbt.internal.util.complete.DefaultParsers
import Scope.ThisScope import Scope.ThisScope
import sbt.Scope.ThisBuildScope
import sbt.internal.util.Util
sealed trait ProjectDefinition[PR <: ProjectReference] { sealed trait ProjectDefinition[PR <: ProjectReference] {
@ -335,20 +337,16 @@ object Project:
[a] => (k: ScopedKey[a]) => ScopedKey(f(k.scope), k.key) [a] => (k: ScopedKey[a]) => ScopedKey(f(k.scope), k.key)
def transform(g: Scope => Scope, ss: Seq[Def.Setting[?]]): Seq[Def.Setting[?]] = def transform(g: Scope => Scope, ss: Seq[Def.Setting[?]]): Seq[Def.Setting[?]] =
val f = mapScope(g) // We use caching to avoid creating new Scope instances too many times
ss.map { setting => // Creating a new Scope is CPU expensive because of the uniqueness cache
setting.mapKey(f).mapReferenced(f) val f = mapScope(Util.withCaching(g))
} ss.map(_.mapKey(f).mapReferenced(f))
def transformRef(g: Scope => Scope, ss: Seq[Def.Setting[?]]): Seq[Def.Setting[?]] =
val f = mapScope(g)
ss.map(_.mapReferenced(f))
def inThisBuild(ss: Seq[Setting[?]]): Seq[Setting[?]] = def inThisBuild(ss: Seq[Setting[?]]): Seq[Setting[?]] =
inScope(ThisScope.copy(project = Select(ThisBuild)))(ss) inScope(ThisBuildScope)(ss)
private[sbt] def inThisBuild[T](i: Initialize[T]): Initialize[T] = private[sbt] def inThisBuild[T](i: Initialize[T]): Initialize[T] =
inScope(ThisScope.copy(project = Select(ThisBuild)), i) inScope(ThisBuildScope, i)
private[sbt] def inConfig[T](conf: Configuration, i: Initialize[T]): Initialize[T] = private[sbt] def inConfig[T](conf: Configuration, i: Initialize[T]): Initialize[T] =
inScope(ThisScope.copy(config = Select(conf)), i) inScope(ThisScope.copy(config = Select(conf)), i)

View File

@ -14,17 +14,30 @@ import sbt.internal.util.{ AttributeKey, AttributeMap, Dag }
import sbt.internal.util.Util._ import sbt.internal.util.Util._
import sbt.io.IO import sbt.io.IO
import scala.collection.concurrent.TrieMap
import scala.runtime.ScalaRunTime
final case class Scope( final case class Scope private (
project: ScopeAxis[Reference], project: ScopeAxis[Reference],
config: ScopeAxis[ConfigKey], config: ScopeAxis[ConfigKey],
task: ScopeAxis[AttributeKey[?]], task: ScopeAxis[AttributeKey[?]],
extra: ScopeAxis[AttributeMap] extra: ScopeAxis[AttributeMap]
): ):
// Since we use a uniqueness cache we can pre-compute the hashCode for free
// It is always going to be used at least once
override val hashCode = ScalaRunTime._hashCode(this)
def rescope(project: Reference): Scope = copy(project = Select(project)) def rescope(project: Reference): Scope = copy(project = Select(project))
def rescope(config: ConfigKey): Scope = copy(config = Select(config)) def rescope(config: ConfigKey): Scope = copy(config = Select(config))
def rescope(task: AttributeKey[?]): Scope = copy(task = Select(task)) def rescope(task: AttributeKey[?]): Scope = copy(task = Select(task))
def copy(
project: ScopeAxis[Reference] = this.project,
config: ScopeAxis[ConfigKey] = this.config,
task: ScopeAxis[AttributeKey[?]] = this.task,
extra: ScopeAxis[AttributeMap] = this.extra
): Scope = Scope(project, config, task, extra)
override def toString: String = this match override def toString: String = this match
case Scope(Zero, Zero, Zero, Zero) => "Global" case Scope(Zero, Zero, Zero, Zero) => "Global"
case Scope(_, _, _, This) => s"$project / $config / $task" case Scope(_, _, _, This) => s"$project / $config / $task"
@ -32,8 +45,23 @@ final case class Scope(
end Scope end Scope
object Scope: object Scope:
val ThisScope: Scope = Scope(This, This, This, This) // We use a global uniqueness cache to avoid duplicating Scope.
val Global: Scope = Scope(Zero, Zero, Zero, Zero) // At the time of writing, it divides the number of long-living instances by 15
// reducing the pressure on the heap, and speed up the loading.
private val uniquenessCache = TrieMap.empty[Scope, Scope]
def apply(
project: ScopeAxis[Reference],
config: ScopeAxis[ConfigKey],
task: ScopeAxis[AttributeKey[?]],
extra: ScopeAxis[AttributeMap]
): Scope =
val scope = new Scope(project, config, task, extra)
uniquenessCache.getOrElseUpdate(scope, scope)
val ThisScope: Scope = new Scope(This, This, This, This)
val Global: Scope = new Scope(Zero, Zero, Zero, Zero)
val ThisBuildScope: Scope = Scope(Select(ThisBuild), This, This, This)
val GlobalScope: Scope = Global val GlobalScope: Scope = Global
private[sbt] final val inIsDeprecated = private[sbt] final val inIsDeprecated =
@ -250,33 +278,6 @@ object Scope:
def showProject012Style = (ref: Reference) => Reference.display(ref) + "/" def showProject012Style = (ref: Reference) => Reference.display(ref) + "/"
@deprecated("No longer used", "1.1.3")
def transformTaskName(s: String) = {
val parts = s.split("-+")
(parts.take(1) ++ parts.drop(1).map(_.capitalize)).mkString
}
@deprecated("Use variant without extraInherit", "1.1.1")
def delegates[Proj](
refs: Seq[(ProjectRef, Proj)],
configurations: Proj => Seq[ConfigKey],
resolve: Reference => ResolvedReference,
rootProject: URI => String,
projectInherit: ProjectRef => Seq[ProjectRef],
configInherit: (ResolvedReference, ConfigKey) => Seq[ConfigKey],
taskInherit: AttributeKey[?] => Seq[AttributeKey[?]],
extraInherit: (ResolvedReference, AttributeMap) => Seq[AttributeMap]
): Scope => Seq[Scope] =
delegates(
refs,
configurations,
resolve,
rootProject,
projectInherit,
configInherit,
taskInherit,
)
// *Inherit functions should be immediate delegates and not include argument itself. Transitivity will be provided by this method // *Inherit functions should be immediate delegates and not include argument itself. Transitivity will be provided by this method
def delegates[Proj]( def delegates[Proj](
refs: Seq[(ProjectRef, Proj)], refs: Seq[(ProjectRef, Proj)],
@ -291,17 +292,7 @@ object Scope:
scope => indexedDelegates(resolve, index, rootProject, taskInherit)(scope) scope => indexedDelegates(resolve, index, rootProject, taskInherit)(scope)
} }
@deprecated("Use variant without extraInherit", "1.1.1") private def indexedDelegates(
def indexedDelegates(
resolve: Reference => ResolvedReference,
index: DelegateIndex,
rootProject: URI => String,
taskInherit: AttributeKey[?] => Seq[AttributeKey[?]],
extraInherit: (ResolvedReference, AttributeMap) => Seq[AttributeMap]
)(rawScope: Scope): Seq[Scope] =
indexedDelegates(resolve, index, rootProject, taskInherit)(rawScope)
def indexedDelegates(
resolve: Reference => ResolvedReference, resolve: Reference => ResolvedReference,
index: DelegateIndex, index: DelegateIndex,
rootProject: URI => String, rootProject: URI => String,
@ -367,19 +358,20 @@ object Scope:
} }
private val zeroL = List(Zero) private val zeroL = List(Zero)
private val globalL = List(GlobalScope)
def withZeroAxis[T](base: ScopeAxis[T]): Seq[ScopeAxis[T]] = def withZeroAxis[T](base: ScopeAxis[T]): Seq[ScopeAxis[T]] =
if (base.isSelect) List(base, Zero: ScopeAxis[T]) if (base.isSelect) base :: zeroL else zeroL
else zeroL
def withGlobalScope(base: Scope): Seq[Scope] = def withGlobalScope(base: Scope): Seq[Scope] =
if (base == GlobalScope) GlobalScope :: Nil else base :: GlobalScope :: Nil if (base == GlobalScope) globalL else base :: globalL
def withRawBuilds(ps: Seq[ScopeAxis[ProjectRef]]): Seq[ScopeAxis[ResolvedReference]] = def withRawBuilds(ps: Seq[ScopeAxis[ProjectRef]]): Seq[ScopeAxis[ResolvedReference]] =
(ps: Seq[ScopeAxis[ResolvedReference]]) ++ ps ++ ps.flatMap(rawBuild).distinct :+ Zero
((ps flatMap rawBuild).distinct: Seq[ScopeAxis[ResolvedReference]]) :+
(Zero: ScopeAxis[ResolvedReference])
def rawBuild(ps: ScopeAxis[ProjectRef]): Seq[ScopeAxis[BuildRef]] = ps match { def rawBuild(ps: ScopeAxis[ProjectRef]): Seq[ScopeAxis[BuildRef]] = ps match {
case Select(ref) => Select(BuildRef(ref.build)) :: Nil; case _ => Nil case Select(ref) => Select(BuildRef(ref.build)) :: Nil
case _ => Nil
} }
def delegates[Proj]( def delegates[Proj](
@ -417,19 +409,17 @@ object Scope:
): Seq[ScopeAxis[T]] = ): Seq[ScopeAxis[T]] =
axis match { axis match {
case Select(x) => topologicalSort[T](x, appendZero)(inherit) case Select(x) => topologicalSort[T](x, appendZero)(inherit)
case Zero | This => if (appendZero) Zero :: Nil else Nil case Zero | This => if (appendZero) zeroL else Nil
} }
def topologicalSort[T](node: T, appendZero: Boolean)( def topologicalSort[T](node: T, appendZero: Boolean)(
dependencies: T => Seq[T] dependencies: T => Seq[T]
): Seq[ScopeAxis[T]] = { ): Seq[ScopeAxis[T]] = {
val o = Dag.topologicalSortUnchecked(node)(dependencies).map(x => Select(x): ScopeAxis[T]) val o = Dag.topologicalSortUnchecked(node)(dependencies).map(x => Select(x): ScopeAxis[T])
if (appendZero) o ::: (Zero: ScopeAxis[T]) :: Nil if (appendZero) o ::: zeroL else o
else o
} }
def globalProjectDelegates(scope: Scope): Seq[Scope] = def globalProjectDelegates(scope: Scope): Seq[Scope] =
if (scope == GlobalScope) if (scope == GlobalScope) globalL
GlobalScope :: Nil
else else
for { for {
c <- withZeroAxis(scope.config) c <- withZeroAxis(scope.config)

View File

@ -35,6 +35,7 @@ import java.net.URI
import java.nio.file.{ Path, Paths } import java.nio.file.{ Path, Paths }
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.collection.mutable import scala.collection.mutable
import sbt.internal.util.Util
private[sbt] object Load { private[sbt] object Load {
// note that there is State passed in but not pulled out // note that there is State passed in but not pulled out
@ -266,11 +267,16 @@ private[sbt] object Load {
} }
val projects = loaded.units val projects = loaded.units
lazy val rootEval = lazyEval(loaded.units(loaded.root).unit) lazy val rootEval = lazyEval(loaded.units(loaded.root).unit)
val settings0 = timed("Load.apply: buildConfigurations", log) {
buildConfigurations(loaded, getRootProject(projects), config.injectSettings)
}
val settings = timed("Load.apply: finalTransforms", log) { val settings = timed("Load.apply: finalTransforms", log) {
finalTransforms(buildConfigurations(loaded, getRootProject(projects), config.injectSettings)) finalTransforms(settings0)
} }
val delegates = timed("Load.apply: config.delegates", log) { val delegates = timed("Load.apply: config.delegates", log) {
config.delegates(loaded) // We use caching to avoid creating new Scope instances too many times
// Creating a new Scope is CPU expensive because of the uniqueness cache
Util.withCaching(config.delegates(loaded))
} }
val (cMap, data) = timed("Load.apply: Def.make(settings)...", log) { val (cMap, data) = timed("Load.apply: Def.make(settings)...", log) {
// When settings.size is 100000, Def.make takes around 10s. // When settings.size is 100000, Def.make takes around 10s.

View File

@ -33,7 +33,6 @@ package object sbt
fillTaskAxis, fillTaskAxis,
mapScope, mapScope,
transform, transform,
transformRef,
inThisBuild, inThisBuild,
inScope, inScope,
normalizeModuleID normalizeModuleID

View File

@ -40,7 +40,10 @@ abstract class EvaluateSettings[ScopeType]:
private val transform: [A] => Initialize[A] => INode[A] = [A] => private val transform: [A] => Initialize[A] => INode[A] = [A] =>
(fa: Initialize[A]) => (fa: Initialize[A]) =>
fa match fa match
case k: Keyed[s, A] => single(getStatic(k.scopedKey), k.transform) case g: GetValue[s, A] => single(getStatic(g.scopedKey), g.transform)
case k: KeyedInitialize[A] =>
// TODO create a Single node with no transform?
single(getStatic(k.scopedKey), identity)
case u: Uniform[s, A] => UniformNode(u.inputs.map(transform[s]), u.f) case u: Uniform[s, A] => UniformNode(u.inputs.map(transform[s]), u.f)
case a: Apply[k, A] => case a: Apply[k, A] =>
MixedNode[k, A](TupleMapExtension.transform(a.inputs)(transform), a.f) MixedNode[k, A](TupleMapExtension.transform(a.inputs)(transform), a.f)

View File

@ -811,9 +811,13 @@ trait Init[ScopeType]:
*/ */
sealed trait Keyed[S, A1] extends Initialize[A1]: sealed trait Keyed[S, A1] extends Initialize[A1]:
def scopedKey: ScopedKey[S] def scopedKey: ScopedKey[S]
def transform: S => A1
override final def dependencies = scopedKey :: Nil override final def dependencies = scopedKey :: Nil
private[sbt] override def processAttributes[A2](init: A2)(f: (A2, AttributeMap) => A2): A2 =
init
end Keyed
final class GetValue[S, A1](val scopedKey: ScopedKey[S], val transform: S => A1)
extends Keyed[S, A1]:
override final def apply[A2](g: A1 => A2): Initialize[A2] = override final def apply[A2](g: A1 => A2): Initialize[A2] =
GetValue(scopedKey, g compose transform) GetValue(scopedKey, g compose transform)
override final def evaluate(ss: Settings[ScopeType]): A1 = transform(getValue(ss, scopedKey)) override final def evaluate(ss: Settings[ScopeType]): A1 = transform(getValue(ss, scopedKey))
@ -829,20 +833,27 @@ trait Init[ScopeType]:
g(scopedKey) match g(scopedKey) match
case None => this case None => this
case Some(const) => Value(() => transform(const)) case Some(const) => Value(() => transform(const))
end GetValue
private[sbt] override def processAttributes[A2](init: A2)(f: (A2, AttributeMap) => A2): A2 =
init
end Keyed
private final class GetValue[S, A1](val scopedKey: ScopedKey[S], val transform: S => A1)
extends Keyed[S, A1]
/** /**
* A `Keyed` where the type of the value and the associated `ScopedKey` are the same. * A `Keyed` where the type of the value and the associated `ScopedKey` are the same.
* @tparam A1 the type of both the value this `Initialize` defines and the type of the associated `ScopedKey`. * @tparam A1 the type of both the value this `Initialize` defines and the type of the associated `ScopedKey`.
*/ */
trait KeyedInitialize[A1] extends Keyed[A1, A1]: trait KeyedInitialize[A1] extends Keyed[A1, A1]:
final val transform = identity[A1] override final def apply[A2](g: A1 => A2): Initialize[A2] =
GetValue(scopedKey, g)
override final def evaluate(ss: Settings[ScopeType]): A1 = getValue(ss, scopedKey)
override final def mapReferenced(g: MapScoped): Initialize[A1] = g(scopedKey)
private[sbt] override final def validateKeyReferenced(g: ValidateKeyRef): ValidatedInit[A1] =
g(scopedKey, false) match
case Left(un) => Left(un :: Nil)
case Right(nk) => Right(nk)
override final def mapConstant(g: MapConstant): Initialize[A1] =
g(scopedKey) match
case None => this
case Some(const) => Value(() => const)
end KeyedInitialize end KeyedInitialize
private[sbt] final class TransformCapture(val f: [x] => Initialize[x] => Initialize[x]) private[sbt] final class TransformCapture(val f: [x] => Initialize[x] => Initialize[x])