perf: Cache Zinc Analysis across compilation

**Problem**
Protobuf reading shows up as one of the bottlenecks during no-op compilation.

**Solution**
This adds a local, in-memory cache of Zinc Analysis using the timestamp
and the file size of the persisted protobuf file.
This commit is contained in:
Eugene Yokota 2025-11-12 02:35:18 -05:00
parent 83376f3cea
commit 8023eeee20
1 changed files with 37 additions and 3 deletions

View File

@ -9,7 +9,10 @@
package sbt
package internal
import com.github.benmanes.caffeine.cache.{ Cache as CCache, Caffeine, Weigher }
import java.io.File
import java.nio.file.{ Files, NoSuchFileException }
import java.nio.file.attribute.BasicFileAttributes
import Keys.{ organization, thisProject, autoGeneratedProject, publish, publishLocal, skip }
import Def.Setting
// import sbt.ProjectExtra.apply
@ -18,7 +21,7 @@ import sbt.internal.util.{ Attributed, StringAttributeMap }
import sbt.internal.inc.{ FileAnalysisStore, ReflectUtilities }
import sbt.util.CacheImplicits.given
import xsbti.{ FileConverter, VirtualFileRef }
import xsbti.compile.CompileAnalysis
import xsbti.compile.{ AnalysisContents, CompileAnalysis }
trait BuildDef {
def projectDefinitions(@deprecated("unused", "") baseDirectory: File): Seq[Project] = projects
@ -87,6 +90,26 @@ private[sbt] object BuildDef:
): Seq[xsbti.compile.CompileAnalysis] =
in.flatMap(a => extractAnalysis(a.metadata, converter))
private[sbt] final val localAnalysisCacheByteSize = 100 * 1024L * 1024L
private val weigher: Weigher[String, (Option[AnalysisContents], Long, Long)] = {
case (_, (_, _, sizeBytes)) => sizeBytes.toInt
}
private val inMemoryAnalysisCache: CCache[String, (Option[AnalysisContents], Long, Long)] =
Caffeine
.newBuilder()
.maximumWeight(localAnalysisCacheByteSize)
.weigher(weigher)
.build()
private def getOrElseUpdate(ref: VirtualFileRef, lastModified: Long, sizeBytes: Long)(
value: => Option[AnalysisContents]
): Option[AnalysisContents] =
Option(inMemoryAnalysisCache.getIfPresent(ref.id())) match
case Some((v, mod, i)) if lastModified == mod && sizeBytes == i => v
case _ =>
val v = value
inMemoryAnalysisCache.put(ref.id(), (v, lastModified, sizeBytes))
v
private[sbt] def extractAnalysis(
metadata: StringAttributeMap,
converter: FileConverter
@ -94,10 +117,21 @@ private[sbt] object BuildDef:
import sbt.OptionSyntax.*
def asBinary(file: File) = FileAnalysisStore.binary(file).get.asScala
def asText(file: File) = FileAnalysisStore.text(file).get.asScala
def fallback(file: File) = asBinary(file).orElse(asText(file))
def getContents(ref: VirtualFileRef): Option[AnalysisContents] =
val path = converter.toPath(ref)
val file = path.toFile()
try
val attrs = Files.readAttributes(path, classOf[BasicFileAttributes])
if attrs.isDirectory then fallback(file)
else
val lastModified = attrs.lastModifiedTime().toMillis()
val sizeBytes = attrs.size()
getOrElseUpdate(ref, lastModified, sizeBytes)(fallback(file))
catch case _: NoSuchFileException => fallback(file)
for
ref <- metadata.get(Keys.analysis)
file = converter.toPath(VirtualFileRef.of(ref)).toFile
content <- asBinary(file).orElse(asText(file))
content <- getContents(VirtualFileRef.of(ref))
yield content.getAnalysis
end BuildDef