diff --git a/main/src/main/scala/sbt/internal/BuildDef.scala b/main/src/main/scala/sbt/internal/BuildDef.scala index 181205a23..6ac7b226e 100644 --- a/main/src/main/scala/sbt/internal/BuildDef.scala +++ b/main/src/main/scala/sbt/internal/BuildDef.scala @@ -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