-
Notifications
You must be signed in to change notification settings - Fork 43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
centralize ScalafixInterface memoization within a setting key #409
Changes from all commits
6e05af8
5bb9167
52d677b
ff8e682
d834da8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ import java.nio.file.Path | |
import java.{util => jutil} | ||
import coursierapi.Repository | ||
import sbt._ | ||
import sbt.io.RegularFileFilter | ||
import scalafix.interfaces.{Scalafix => ScalafixAPI, _} | ||
import scalafix.sbt.InvalidArgument | ||
|
||
|
@@ -31,6 +32,16 @@ object Arg { | |
customDependencies.map(_.asCoursierCoordinates).asJava, | ||
repositories.asJava | ||
) | ||
|
||
def mutableFiles: Seq[File] = | ||
customURIs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
.map(uri => java.nio.file.Paths.get(uri).toFile) | ||
.flatMap { | ||
case classDirectory if classDirectory.isDirectory => | ||
classDirectory.**(RegularFileFilter).get | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not directly related to this PR: this was changed from |
||
case jar => | ||
Seq(jar) | ||
} | ||
} | ||
|
||
case class Rules(rules: Seq[String]) extends Arg with CacheKey { | ||
|
@@ -81,7 +92,7 @@ object Arg { | |
|
||
class ScalafixInterface private ( | ||
scalafixArguments: ScalafixArguments, // hide it to force usage of withArgs so we can intercept arguments | ||
val args: Seq[Arg] | ||
val args: Seq[Arg] = Nil | ||
) { | ||
|
||
def withArgs(args: Arg*): ScalafixInterface = { | ||
|
@@ -112,31 +123,42 @@ class ScalafixInterface private ( | |
} | ||
|
||
object ScalafixInterface { | ||
private class LazyValue[T](thunk: () => T) extends (() => T) { | ||
private lazy val _value = scala.util.Try(thunk()) | ||
override def apply(): T = _value.get | ||
} | ||
private val fromToolClasspathMemo: BlockingCache[ | ||
(String, Seq[ModuleID], Seq[Repository]), | ||
ScalafixInterface | ||
] = new BlockingCache | ||
|
||
type Cache = BlockingCache[ | ||
( | ||
String, // scalafixScalaBinaryVersion | ||
Option[Arg.ToolClasspath] | ||
), | ||
( | ||
ScalafixInterface, | ||
Seq[Long] // mutable files stamping | ||
) | ||
] | ||
|
||
private[scalafix] val defaultLogger: Logger = ConsoleLogger(System.out) | ||
|
||
def fromToolClasspath( | ||
def apply( | ||
cache: Cache, | ||
scalafixScalaBinaryVersion: String, | ||
scalafixDependencies: Seq[ModuleID], | ||
scalafixCustomResolvers: Seq[Repository], | ||
toolClasspath: Arg.ToolClasspath, | ||
logger: Logger, | ||
callback: ScalafixMainCallback | ||
): () => ScalafixInterface = | ||
new LazyValue({ () => | ||
fromToolClasspathMemo.getOrElseUpdate( | ||
( | ||
scalafixScalaBinaryVersion, | ||
scalafixDependencies, | ||
scalafixCustomResolvers | ||
), { | ||
): ScalafixInterface = { | ||
|
||
// Build or retrieve from the cache the scalafix interface that can run | ||
// built-in rules. This is the most costly instantiation, which should be | ||
// shared as much as possible. | ||
val (buildinRulesInterface, _) = cache.compute( | ||
( | ||
scalafixScalaBinaryVersion, | ||
None | ||
Comment on lines
+153
to
+154
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Memoizing first an interface per Scala version (ie.e ignoring the requested tool classpath) is key for the tool classpath instances created below to share the common classloader with the entire Scala standard library. |
||
), | ||
{ | ||
case Some(_) => | ||
// cache hit, don't update | ||
None | ||
case None => | ||
// cache miss, resolve scalafix artifacts and classload them | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
if (scalafixScalaBinaryVersion.startsWith("3")) | ||
logger.error( | ||
"To use Scalafix on Scala 3 projects, you must unset `scalafixScalaBinaryVersion`. " + | ||
|
@@ -151,27 +173,54 @@ object ScalafixInterface { | |
val scalafixArguments = ScalafixAPI | ||
.fetchAndClassloadInstance( | ||
scalafixScalaBinaryVersion, | ||
scalafixCustomResolvers.asJava | ||
toolClasspath.repositories.asJava | ||
) | ||
.newArguments() | ||
.withMainCallback(callback) | ||
val printStream = | ||
|
||
val printStream = Arg.PrintStream( | ||
new PrintStream( | ||
LoggingOutputStream( | ||
logger, | ||
Level.Info | ||
) | ||
) | ||
new ScalafixInterface(scalafixArguments, Nil) | ||
.withArgs( | ||
Arg.PrintStream(printStream), | ||
Arg.ToolClasspath( | ||
Nil, | ||
scalafixDependencies, | ||
scalafixCustomResolvers | ||
) | ||
) | ||
|
||
Some( | ||
( | ||
new ScalafixInterface(scalafixArguments).withArgs(printStream), | ||
Nil | ||
) | ||
} | ||
) | ||
}) | ||
) | ||
} | ||
) | ||
|
||
// stamp the files that might change, to potentialy force invalidation | ||
// of the corresponding cache entry | ||
val currentStamps = | ||
toolClasspath.mutableFiles.map(IO.getModifiedTimeOrZero) | ||
|
||
val (toolClasspathInterface, _) = cache.compute( | ||
( | ||
scalafixScalaBinaryVersion, | ||
Some(toolClasspath) | ||
), | ||
{ | ||
case Some((_, oldStamps)) if (currentStamps == oldStamps) => | ||
// cache hit, don't update | ||
None | ||
case _ => | ||
// cache miss or stale stamps, resolve custom rules artifacts and classload them | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
Some( | ||
( | ||
buildinRulesInterface.withArgs(toolClasspath), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. by replacing the former value with this, we make the former interface, together with its toolClasspath classloader, eligible for garbage collection |
||
currentStamps | ||
) | ||
) | ||
} | ||
) | ||
|
||
toolClasspathInterface | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ConcurrentHashMap
was ruled out in the past, but I confirmed that there is a per-key lock oncompute()
.