mirror of https://github.com/sbt/sbt.git
[2.x] Optimize incremental test further (#9364)
This applies a range of optimisations local to ClassStamper that bring the time needed for refinedTestDigests down to acceptable levels (see #9108): * Cache the digests of transitive dependencies (big impact) * Avoid sorting of digest subsets that would later get sorted again (small) * Pre-compute which Analysis instances are required for each class to avoid repeated scanning of the whole list (medium) * Merge two loops on relations.externalDeps into one (small) * Compute the set of extra digests outside loop (small) * Track the digest closure of each test via a BitSet (big)
This commit is contained in:
parent
0ef972706c
commit
ca20f68a14
|
|
@ -59,9 +59,10 @@ object IncrementalTest:
|
|||
val rds = Keys.resourceDigests.value
|
||||
val extra = Keys.extraTestDigests.value
|
||||
val stamper = ClassStamper(cp, converter)
|
||||
val testDigestExtra = extra ++ rds ++ opts
|
||||
// TODO: Potentially do something about JUnit 5 and others which might not use class name
|
||||
Map((testNames.flatMap: name =>
|
||||
stamper.transitiveStamp(name, extra ++ rds ++ opts, s.log) match
|
||||
stamper.transitiveStamp(name, testDigestExtra, s.log) match
|
||||
case Some(ts) => Seq(name -> ts)
|
||||
case None => Nil
|
||||
)*)
|
||||
|
|
@ -174,11 +175,35 @@ class ClassStamper private[sbt] (
|
|||
converter,
|
||||
)
|
||||
|
||||
private val stamps = mutable.Map.empty[String, Set[Digest]]
|
||||
// Leaf digests (class bytecode hashes + library file digests) are interned to dense
|
||||
// ints so transitive digest sets can be held as bit sets: union is a word-parallel OR
|
||||
// and each member costs one bit instead of a 32-byte Digest.
|
||||
private val digestIds = mutable.HashMap.empty[Digest, Int]
|
||||
private val digestList = mutable.ArrayBuffer.empty[Digest]
|
||||
private def idOf(d: Digest): Int =
|
||||
digestIds.getOrElseUpdate(d, { val i = digestList.size; digestList += d; i })
|
||||
|
||||
private val stamps = mutable.Map.empty[String, mutable.BitSet]
|
||||
// Memoizes the full transitive digest set per class name (excluding extraHashes), so the
|
||||
// re-entrant external-dep walk isn't recomputed for every reference. Mapping bits back to
|
||||
// digests and sorting is deferred to the root (`transitiveStamp`); intermediate results
|
||||
// are only ever OR-ed into another bit set, where order and identity are irrelevant.
|
||||
private val transitiveCache = mutable.Map.empty[String, mutable.BitSet]
|
||||
// Cached so by-name `analyses0` is only evaluated once
|
||||
private lazy val analyses = analyses0
|
||||
private val stampVf: VirtualFileRef => Digest =
|
||||
CacheImplicits.virtualFileRefToDigest(_)(converter)
|
||||
// Index of binary class name -> analyses that produce it, so a stamp can dispatch
|
||||
// straight to its owning analyses instead of scanning every analysis on the classpath.
|
||||
private lazy val analysesByProduct: Map[String, Seq[Analysis]] =
|
||||
val acc = mutable.HashMap.empty[String, mutable.ListBuffer[Analysis]]
|
||||
analyses.foreach: a =>
|
||||
a.relations.productClassName._2s.foreach: bin =>
|
||||
acc.getOrElseUpdate(bin, mutable.ListBuffer.empty) += a
|
||||
acc.iterator.map((k, v) => k -> v.toSeq).toMap
|
||||
// Memoized: virtualFileRefToDigest does a filesystem stat per call, and the same
|
||||
// library ref is referenced by many classes.
|
||||
private val vfDigests = mutable.Map.empty[VirtualFileRef, Digest]
|
||||
private def stampVf(vf: VirtualFileRef): Digest =
|
||||
vfDigests.getOrElseUpdate(vf, CacheImplicits.virtualFileRefToDigest(vf)(converter))
|
||||
|
||||
/**
|
||||
* Given a classpath and a class name, this tries to create a SHA-256 digest.
|
||||
|
|
@ -190,22 +215,33 @@ class ClassStamper private[sbt] (
|
|||
extraHashes: Seq[Digest],
|
||||
log: Logger,
|
||||
): Option[Digest] =
|
||||
val digests = transitiveStamps(javaClassName, extraHashes, log)
|
||||
val digests = sortedDigests(transitiveStamps(javaClassName, log)) ++ extraHashes
|
||||
if digests.nonEmpty then Some(Digest.sha256Hash(digests*))
|
||||
else None
|
||||
|
||||
// Map a bit set back to its digests
|
||||
private def sortedDigests(bits: mutable.BitSet): Seq[Digest] =
|
||||
val buf = mutable.ArrayBuffer.empty[Digest]
|
||||
bits.foreach(i => buf += digestList(i))
|
||||
buf.sortInPlace()
|
||||
buf.toSeq
|
||||
|
||||
private def transitiveStamps(
|
||||
javaClassName: String,
|
||||
extraHashes: Seq[Digest],
|
||||
log: Logger,
|
||||
): Seq[Digest] =
|
||||
val builder = Set.newBuilder[Digest]
|
||||
analyses.foreach(internalStamp(builder, javaClassName, _, mutable.Set.empty, log))
|
||||
val digests = builder.result().toSeq.sorted
|
||||
digests ++ extraHashes
|
||||
): mutable.BitSet =
|
||||
transitiveCache.getOrElseUpdate(
|
||||
javaClassName, {
|
||||
val builder = mutable.BitSet.empty
|
||||
analysesByProduct
|
||||
.getOrElse(javaClassName, Nil)
|
||||
.foreach(internalStamp(builder, javaClassName, _, mutable.Set.empty, log))
|
||||
builder
|
||||
}
|
||||
)
|
||||
|
||||
private def internalStamp(
|
||||
builder: mutable.Builder[Digest, Set[Digest]],
|
||||
builder: mutable.BitSet,
|
||||
javaClassName: String,
|
||||
analysis: Analysis,
|
||||
alreadySeen: mutable.Set[String],
|
||||
|
|
@ -215,8 +251,8 @@ class ClassStamper private[sbt] (
|
|||
|
||||
// log.debug(s"test: internalStamp($javaClassName)")
|
||||
def internalStamp0(className: String): Unit =
|
||||
// Use a new builder so we can cache the result in `stamps`
|
||||
val newBuilder = Set.newBuilder[Digest]
|
||||
// Use a new bit set so we can cache the result in `stamps`
|
||||
val newBuilder = mutable.BitSet.empty
|
||||
|
||||
// Zinc doesn't fully track the transitive dependencies
|
||||
relations
|
||||
|
|
@ -227,32 +263,27 @@ class ClassStamper private[sbt] (
|
|||
relations
|
||||
.externalDeps(className)
|
||||
.foreach: libClassName =>
|
||||
newBuilder ++= transitiveStamps(libClassName, Nil, log)
|
||||
relations
|
||||
.externalDeps(className)
|
||||
.foreach: libClassName =>
|
||||
newBuilder |= transitiveStamps(libClassName, log)
|
||||
relations.libraryClassName
|
||||
.reverse(libClassName)
|
||||
.foreach: vf =>
|
||||
newBuilder += stampVf(vf)
|
||||
newBuilder += idOf(stampVf(vf))
|
||||
analysis.apis.internal
|
||||
.get(className)
|
||||
.toSet
|
||||
.foreach: analyzed =>
|
||||
newBuilder += Digest.dummy(
|
||||
37 * (17 + analyzed.transitiveBytecodeHash) + analyzed.bytecodeHash
|
||||
newBuilder += idOf(
|
||||
Digest.dummy(37 * (17 + analyzed.transitiveBytecodeHash) + analyzed.bytecodeHash)
|
||||
)
|
||||
|
||||
val xs = newBuilder.result()
|
||||
if xs.nonEmpty then stamps(className) = xs
|
||||
if newBuilder.nonEmpty then stamps(className) = newBuilder
|
||||
else ()
|
||||
|
||||
builder ++= xs
|
||||
builder |= newBuilder
|
||||
|
||||
if alreadySeen.contains(javaClassName) then ()
|
||||
else
|
||||
stamps.get(javaClassName) match
|
||||
case Some(xs) => builder ++= xs
|
||||
case Some(xs) => builder |= xs
|
||||
case _ =>
|
||||
alreadySeen += javaClassName
|
||||
// Note: internalClassDeps uses Scala-encoded class name for companion objects
|
||||
|
|
|
|||
Loading…
Reference in New Issue