Skip to content

Commit

Permalink
Cache Scalafix instances globally (#206)
Browse files Browse the repository at this point in the history
* Cache Scalafix instances globally

* Use ConcurrentHashMap for better thread safety

* Use `compute` to avoid concurrent initialization of the same key

* Cache also `withToolClasspath` using two-layered cache
  • Loading branch information
lolgab authored Oct 1, 2024
1 parent 54b8d07 commit 8e6c57f
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 9 deletions.
47 changes: 47 additions & 0 deletions mill-scalafix/src/com/goyeau/mill/scalafix/ScalafixCache.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.goyeau.mill.scalafix

import com.goyeau.mill.scalafix.CoursierUtils
import coursier.core.Repository
import mill.Agg
import mill.scalalib.Dep
import scalafix.interfaces.Scalafix
import scalafix.interfaces.ScalafixArguments

import java.util.concurrent.ConcurrentHashMap
import scala.jdk.CollectionConverters._
import scala.ref.SoftReference

private[scalafix] object ScalafixCache {

private val scalafixCache = new Cache[(String, java.util.List[coursierapi.Repository]), Scalafix](createFunction = {
case (scalaVersion, repositories) =>
Scalafix.fetchAndClassloadInstance(scalaVersion, repositories)
})

private val scalafixArgumentsCache =
new Cache[(String, Seq[Repository], Agg[Dep]), ScalafixArguments](createFunction = {
case (scalaVersion, repositories, scalafixIvyDeps) =>
val repos = repositories.map(CoursierUtils.toApiRepository).asJava
val deps = scalafixIvyDeps.map(CoursierUtils.toCoordinates).iterator.toSeq.asJava
scalafixCache
.getOrElseCreate((scalaVersion, repos))
.newArguments()
.withToolClasspath(Seq.empty.asJava, deps, repos)
})

def getOrElseCreate(scalaVersion: String, repositories: Seq[Repository], scalafixIvyDeps: Agg[Dep]) =
scalafixArgumentsCache.getOrElseCreate((scalaVersion, repositories, scalafixIvyDeps))

private class Cache[A, B <: AnyRef](createFunction: A => B) {
private val cache = new ConcurrentHashMap[A, SoftReference[B]]

def getOrElseCreate(a: A) =
cache.compute(
a,
{
case (_, v @ SoftReference(_)) => v
case _ => SoftReference(createFunction(a))
}
)()
}
}
11 changes: 2 additions & 9 deletions mill-scalafix/src/com/goyeau/mill/scalafix/ScalafixModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import mill.api.{Logger, PathRef, Result}
import mill.scalalib.{Dep, ScalaModule}
import mill.define.Command

import scalafix.interfaces.Scalafix
import scalafix.interfaces.ScalafixError.*

import scala.compat.java8.OptionConverters.*
Expand Down Expand Up @@ -77,21 +76,15 @@ object ScalafixModule {
wd: os.Path
): Result[Unit] =
if (sources.nonEmpty) {
val scalafix = Scalafix
.fetchAndClassloadInstance(scalaVersion, repositories.map(CoursierUtils.toApiRepository).asJava)
.newArguments()
val scalafix = ScalafixCache
.getOrElseCreate(scalaVersion, repositories, scalafixIvyDeps)
.withParsedArguments(args.asJava)
.withWorkingDirectory(wd.toNIO)
.withConfig(scalafixConfig.map(_.toNIO).asJava)
.withClasspath(classpath.map(_.toNIO).asJava)
.withScalaVersion(scalaVersion)
.withScalacOptions(scalacOptions.asJava)
.withPaths(sources.map(_.toNIO).asJava)
.withToolClasspath(
Seq.empty.asJava,
scalafixIvyDeps.map(CoursierUtils.toCoordinates).iterator.toSeq.asJava,
repositories.map(CoursierUtils.toApiRepository).asJava
)

log.info(s"Rewriting and linting ${sources.size} Scala sources against ${scalafix.rulesThatWillRun.size} rules")
val errors = scalafix.run()
Expand Down

0 comments on commit 8e6c57f

Please sign in to comment.