Skip to content
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

Merged
merged 5 commits into from
Apr 18, 2024

Conversation

bjaglin
Copy link
Collaborator

@bjaglin bjaglin commented Apr 17, 2024

Code review showed that we have several types of memoization since #380. It turns that we have actually 3 different types of caching at the moment:

  1. a basic LazyValue memoizer, effectively useless since Support several scalafixScalaBinaryVersion within a build #380 given how it's used
  2. a companion-object-bound cache, to amortize the cost of repeated invocations
  3. a task-bound cache, to amortize a single invocation across several projects/configs with local rules and/or an adhoc CLI dependency: rule

This PR is folding these 3 caches into a single setting-bound one, to (hopefully) make the code more maintainable, as well as improving the UX for developers using local rules:

  • the CPU usage should be lower when repeating scalafix invocations (particularly benefiting users of scalafixOnCompile) thanks to the warm classloader for local rules
  • permanent memory consumption should be insignificantly higher (to persist the custom tool classpath classloader)

@bjaglin bjaglin changed the title Centralize ScalafixInterface memoization centralize ScalafixInterface memoization Apr 17, 2024
@bjaglin bjaglin force-pushed the memoization branch 3 times, most recently from e015cb7 to d46ef1e Compare April 17, 2024 13:35
@bjaglin bjaglin changed the title centralize ScalafixInterface memoization memoize ScalafixInterface instances within a setting key Apr 17, 2024
@bjaglin bjaglin changed the title memoize ScalafixInterface instances within a setting key memoize all ScalafixInterface instances within a setting key Apr 17, 2024
@bjaglin bjaglin force-pushed the memoization branch 2 times, most recently from c28d835 to a7f9da8 Compare April 17, 2024 13:49
@bjaglin bjaglin changed the title memoize all ScalafixInterface instances within a setting key centralize ScalafixInterface memoization within a setting key Apr 17, 2024
@bjaglin bjaglin force-pushed the memoization branch 5 times, most recently from e9d2d5e to 2870da0 Compare April 17, 2024 22:35
// cache hit, don't update
None
case _ =>
// cache miss or stale stamps, resolve custom rules artifacts and classload them
Copy link
Collaborator Author

@bjaglin bjaglin Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// cache hit, don't update
None
case None =>
// cache miss, resolve scalafix artifacts and classload them
Copy link
Collaborator Author

@bjaglin bjaglin Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +152 to +154
scalafixScalaBinaryVersion,
None
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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.

// cache miss or stale stamps, resolve custom rules artifacts and classload them
Some(
(
buildinRulesInterface.withArgs(toolClasspath),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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

@bjaglin bjaglin marked this pull request as ready for review April 17, 2024 23:04
@bjaglin bjaglin force-pushed the memoization branch 2 times, most recently from b7ec5df to 2402659 Compare April 17, 2024 23:45
…zation

The persistent cache is now bound to a setting key so that its lifecycle is
aligned with an sbt session (allowing flushing via a `reload`)

The per-invocation cache is merged into the persistent one, which means that
the local rules classloader will now remain warm/JITed across invocations. The
cache key and the stamping strategy prevents instances from leaking when local
rules are being iterated on and recompiled between invocations.
.map(uri => java.nio.file.Paths.get(uri).toFile)
.flatMap {
case classDirectory if classDirectory.isDirectory =>
classDirectory.**(RegularFileFilter).get
Copy link
Collaborator Author

@bjaglin bjaglin Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not directly related to this PR: this was changed from AllPassFilter to RegularFileFilter as the code was moved, in order to skip directories (that don't have any impact on the behavior)

)

val informationalInvocation = shellArgs.extra
.intersect(List("--help", "-h", "--version", "-v"))
Copy link
Collaborator Author

@bjaglin bjaglin Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bonus feature: this removes the need for triggering compilation when --version or -v is requested

/** A basic thread-safe cache without any eviction. */
class BlockingCache[K, V] {

// Number of keys is expected to be very small so the global lock should not be a bottleneck
private val underlying = ju.Collections.synchronizedMap(new ju.HashMap[K, V])
private val underlying = new ju.concurrent.ConcurrentHashMap[K, V]
Copy link
Collaborator Author

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 on compute().

@@ -31,6 +32,16 @@ object Arg {
customDependencies.map(_.asCoursierCoordinates).asJava,
repositories.asJava
)

def mutableFiles: Seq[File] =
customURIs
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

customURIs is effectively the only mutable part of ToolClasspath since customDependencies are immutable once resolved

@bjaglin bjaglin merged commit 9ceb2c5 into scalacenter:main Apr 18, 2024
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant