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

Fix 0.9.18 caching/performance regressions #142

Merged
merged 5 commits into from
Jul 7, 2020

Conversation

bjaglin
Copy link
Collaborator

@bjaglin bjaglin commented Jul 6, 2020

We'll probably need some performance tests to catch those regressions in the future...

Testing on a real project

I have timed the initialization overhead of scalafix by running a warm all scalafix test:scalafix it:scalafix (third invocation with scalafixCaching := true) on a build with 25 sub-projects which have Scalafix enabled.

  • With scalafixDependencies

    • 0.9.16: ~10s
    • 0.9.18: ~50s
    • This PR: ~8s
  • With scalafixDependencies & dependsOn(customRules % ScalafixConfig)

    • 0.9.16: ~10s
    • 0.9.18: ~50s
    • This PR: ~9s

The impact of (not) JITing the rules is not exposed by this test, but rule execution should be back to pre-0.9.18 level as well.

@bjaglin bjaglin changed the title Fix performance regression on multi-project builds Fix 0.9.18 performance regression Jul 7, 2020
@bjaglin bjaglin changed the title Fix 0.9.18 performance regression Fix 0.9.18 performance regressions Jul 7, 2020
@bjaglin bjaglin changed the title Fix 0.9.18 performance regressions Fix 0.9.18 caching/performance regressions Jul 7, 2020
@bjaglin bjaglin marked this pull request as ready for review July 7, 2020 10:05
@bjaglin bjaglin requested review from olafurpg and mlachkar July 7, 2020 10:09
Regression introduced in 2ca68e4: to be memoized, settings must be
bound to a setting key and defined on a scope.
This avoids initialization of a classloader that has the exact same
class files as its parent for each invocation.
Since d81dd72 (see commit description), when `scalafix` or `scalafixAll`
is run on a build root that aggregates many projects, many identical
classloaders may be created when an external rule is provided on the
CLI or when projects use local rules via ScalafixConfig.

This allows aggregated tasks across projects & configs to share a
single, warm classloader when possible (same Scalafix classpath).

Note that cache is bound on purpose to a task value, so two successive
invocations of `scalafix` with the exact same arguments & environment
will not share the same classloader. This is for simplicity, as local
rules can be updated from one run to the other, and to my knowledge,
there is no simple way to invalidate a URLClassLoader if the underlying
class files change.
Copy link
Contributor

@olafurpg olafurpg left a comment

Choose a reason for hiding this comment

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

LGTM 👍 This was a tricky issue to catch, thank you for tracking it down! It's difficult to have integration tests to prevent regressions like these, sometimes it's most effective to just manually test/benchmark the fix as you have done

import scala.collection.mutable

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

Choose a reason for hiding this comment

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

FWIW, if you don't mind using Java collections you can replace this class with

import java.{util => ju}
object BlockingCache {
  def empty[A, B] =
    ju.Collections.synchronizedMap(new ju.HashMap[A, B])
}

val cache = BlockingCache.empty[Int, String]
cache.computeIfAbsent(10, _ => "9")

cache // {10=9}

Those APIs are quite quirky however (return AnyRef in many places) so I like rolling a custom Scala solution instead.

Copy link
Collaborator Author

@bjaglin bjaglin Jul 7, 2020

Choose a reason for hiding this comment

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

good idea - I kept the facade but swapped the implementation in 28e70a7

)
if (customToolClasspath) {
val toolClasspath = ToolClasspath(
projectDepsInternal0.map(_.toURI.toURL),
Copy link
Contributor

Choose a reason for hiding this comment

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

Unrelated to this PR, I see that we're using java.net.URL as cache keys. This is problematic because URL.equals does remote blocking calls

Two hosts are considered equivalent if both host names can be resolved into the same IP addresses; else if either host name can't be resolved, the host names must be equal without regard to case; or both host names equal to null.

https://docs.oracle.com/javase/6/docs/api/java/net/URL.html#equals(java.lang.Object)

It's probably better to use URI instead of URL

Copy link
Collaborator Author

@bjaglin bjaglin Jul 7, 2020

Choose a reason for hiding this comment

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

👍 already got bitten by that in another project, but apparently not enough to care... will change that in a separate PR

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.

updating to sbt-scalafix 0.9.18 causes crash
4 participants