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 scala.collection.concurrent.TrieMap
object Util {
def makeList[T](size: Int, value: T): List[T] = List.fill(size)(value)
@ -71,4 +73,9 @@ object Util {
extension [A](value: 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.DefaultParsers
import Scope.ThisScope
import sbt.Scope.ThisBuildScope
import sbt.internal.util.Util
sealed trait ProjectDefinition[PR <: ProjectReference] {
@ -335,20 +337,16 @@ object Project:
[a] => (k: ScopedKey[a]) => ScopedKey(f(k.scope), k.key)
def transform(g: Scope => Scope, ss: Seq[Def.Setting[?]]): Seq[Def.Setting[?]] =
val f = mapScope(g)
ss.map { setting =>
setting.mapKey(f).mapReferenced(f)
}
def transformRef(g: Scope => Scope, ss: Seq[Def.Setting[?]]): Seq[Def.Setting[?]] =
val f = mapScope(g)
ss.map(_.mapReferenced(f))
// We use caching to avoid creating new Scope instances too many times
// Creating a new Scope is CPU expensive because of the uniqueness cache
val f = mapScope(Util.withCaching(g))
ss.map(_.mapKey(f).mapReferenced(f))
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] =
inScope(ThisScope.copy(project = Select(ThisBuild)), i)
inScope(ThisBuildScope, i)
private[sbt] def inConfig[T](conf: Configuration, i: Initialize[T]): Initialize[T] =
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.io.IO
import scala.collection.concurrent.TrieMap
import scala.runtime.ScalaRunTime
final case class Scope(
final case class Scope private (
project: ScopeAxis[Reference],
config: ScopeAxis[ConfigKey],
task: ScopeAxis[AttributeKey[?]],
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(config: ConfigKey): Scope = copy(config = Select(config))
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
case Scope(Zero, Zero, Zero, Zero) => "Global"
case Scope(_, _, _, This) => s"$project / $config / $task"
@ -32,8 +45,23 @@ final case class Scope(
end Scope
object Scope:
val ThisScope: Scope = Scope(This, This, This, This)
val Global: Scope = Scope(Zero, Zero, Zero, Zero)
// We use a global uniqueness cache to avoid duplicating Scope.
// 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
private[sbt] final val inIsDeprecated =
@ -250,33 +278,6 @@ object Scope:
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
def delegates[Proj](
refs: Seq[(ProjectRef, Proj)],
@ -291,17 +292,7 @@ object Scope:
scope => indexedDelegates(resolve, index, rootProject, taskInherit)(scope)
}
@deprecated("Use variant without extraInherit", "1.1.1")
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(
private def indexedDelegates(
resolve: Reference => ResolvedReference,
index: DelegateIndex,
rootProject: URI => String,
@ -367,19 +358,20 @@ object Scope:
}
private val zeroL = List(Zero)
private val globalL = List(GlobalScope)
def withZeroAxis[T](base: ScopeAxis[T]): Seq[ScopeAxis[T]] =
if (base.isSelect) List(base, Zero: ScopeAxis[T])
else zeroL
if (base.isSelect) base :: zeroL else zeroL
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]] =
(ps: Seq[ScopeAxis[ResolvedReference]]) ++
((ps flatMap rawBuild).distinct: Seq[ScopeAxis[ResolvedReference]]) :+
(Zero: ScopeAxis[ResolvedReference])
ps ++ ps.flatMap(rawBuild).distinct :+ Zero
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](
@ -417,19 +409,17 @@ object Scope:
): Seq[ScopeAxis[T]] =
axis match {
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)(
dependencies: T => Seq[T]
): Seq[ScopeAxis[T]] = {
val o = Dag.topologicalSortUnchecked(node)(dependencies).map(x => Select(x): ScopeAxis[T])
if (appendZero) o ::: (Zero: ScopeAxis[T]) :: Nil
else o
if (appendZero) o ::: zeroL else o
}
def globalProjectDelegates(scope: Scope): Seq[Scope] =
if (scope == GlobalScope)
GlobalScope :: Nil
if (scope == GlobalScope) globalL
else
for {
c <- withZeroAxis(scope.config)

View File

@ -35,6 +35,7 @@ import java.net.URI
import java.nio.file.{ Path, Paths }
import scala.annotation.tailrec
import scala.collection.mutable
import sbt.internal.util.Util
private[sbt] object Load {
// note that there is State passed in but not pulled out
@ -266,11 +267,16 @@ private[sbt] object Load {
}
val projects = loaded.units
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) {
finalTransforms(buildConfigurations(loaded, getRootProject(projects), config.injectSettings))
finalTransforms(settings0)
}
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) {
// When settings.size is 100000, Def.make takes around 10s.

View File

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

View File

@ -40,7 +40,10 @@ abstract class EvaluateSettings[ScopeType]:
private val transform: [A] => Initialize[A] => INode[A] = [A] =>
(fa: Initialize[A]) =>
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 a: Apply[k, A] =>
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]:
def scopedKey: ScopedKey[S]
def transform: S => A1
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] =
GetValue(scopedKey, g compose transform)
override final def evaluate(ss: Settings[ScopeType]): A1 = transform(getValue(ss, scopedKey))
@ -829,20 +833,27 @@ trait Init[ScopeType]:
g(scopedKey) match
case None => this
case Some(const) => Value(() => transform(const))
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]
end GetValue
/**
* 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`.
*/
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
private[sbt] final class TransformCapture(val f: [x] => Initialize[x] => Initialize[x])