-
Notifications
You must be signed in to change notification settings - Fork 5k
Restored cache for the ZincScalaCompiler #12866
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
Conversation
@bot-gradle test RFM |
OK, I've already triggered ReadyForMerge build for you. |
@big-guy At first sight, I can't see how that cache breaks the reason for removing it in the first place. But I may be missing something. |
@ljacomet I'm +1 for fixing this. I should have added a perf test when I made this switch before. I'm -1 with fixing it in this way. There are a few problems:
I believe the actual problem is this classloader is not reused/cached: Line 85 in 4d87cd9
By caching the ZincScalaCompiler, we also reuse this classloader. If you change that code to...
I see a similar level of improvement. This is obviously the wrong kind of fix too, but I think it demonstrates that reusing that classloader has some impact. I suspect this may be something that the old zinc compiler was doing for us since we didn't worry about caching these things before. With 6.4-rc-2: https://e.grdev.net/s/cceccmm6swwxg/performance/execution 38 minutes of task execution (sorry, these are private links) I think a proper fix would be to limit the change to just caching this classloader. It would be nice if we had access to some of the infrastructure we have for this (e.g., Maybe we can do something a little simpler:
WDYT? |
I agree with all your points. I think I can prepare a better fix according to this guidelines. Unless someone else wants to tackle the issue. |
@SheliakLyr If you are willing to keep working on this, please do so. Otherwise I can't guarantee it will be done in the short term. |
17192e1
to
44bee9b
Compare
I've just pushed a new commit.
Currently, this branch contains "FixV2". Unfortunately performance is still worse than 5.6.x, but compared to 6.x improvement is significant! Keep in mind that my test-project is specifically designed to showcase the problem. In branch cachedScalaCompilerV4 you can see an additional improvement: private static final ClassLoaderCache ANALYZING_COMPILER_CLASSLOADER_CACHE =
new ClassLoaderCache(new URLClassLoader(new URL[0]));
/// ...
if (scalaCompiler instanceof AnalyzingCompiler) {
scalaCompiler = ((AnalyzingCompiler) scalaCompiler).withClassLoaderCache(ANALYZING_COMPILER_CLASSLOADER_CACHE);
} This is something I found while trying to speed up the compilation even further. Seems a little risky to include, as I haven't found any documentation about this cache usage. IMO, FixV2 should be good for now. Further optimizations can be be added later. |
Cc @retronym in case he has thoughts. |
As you've found, In the context of Depending on which factory methods you are using, it might be simpler to get the cache from the first compiler you create and then call Here's where SBT uses a process-wide instance of that cache when instantiating Zinc. It seems to use some non-standard factories. I'm not sure why. The There may be a resource leak there in |
More context on how sbt manages its |
Thank you all for the hints. If I understand correctly, I can use the ClassLoaderCache for different AnalysisCompiler instances. This seems to be the reason for it's existence. After reading your comments, I understood that my current cache is already buggy: uses weak references instead of soft references (that's trivial) and does not close the constructed classloaders. Default implementation of a ClassLoaderCache (actually AbstractClassLoaderCache) is also leaky. My idea right now is to create a simple implementation of classloader cache based on guava. I have 3 variants:
Currently, I have an experimental version of the 2nd approach on branch cachedScalaCompilerV5 . Speedup is significant (~28s). Personally however, I think that (1) should do the job. We can use very small cache sizes (5). Typical project will probably use the same classloader for all modules. Guava'a soft/weak values cannot be used, as they lack a proper mechanism for resource release. For ClassLoaderCache, as a key, I use a pair (filename, timestamp). This is not a gradle-way, but consistent with the way sbt works and @big-guy noticed that the hashing infrastructure is not available in the worker. |
44bee9b
to
89ebf83
Compare
After some thinking, I decided to use the 1st approach from my previous post. Meaning no soft values, caches are limited only by a number of entries. This is less cumbersome and I personally don't have a very good experience with soft references (or rather... the way they impact gc). I have just force-pushed a new version. Please review & tell me what you think. |
89ebf83
to
dae3c19
Compare
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.
@SheliakLyr Thanks a lot for your dedication in this and sorry for the delay in providing feedback.
Your PR currently is breaking some tests, see below why.
Happy to discuss (and learn more on my side) so that we can find a solution.
As a side note, the Gradle 6.5 release is quite sensitive for different reasons and about to get to a first RC.
In that context and given the changes done here, the merging of the PR will be postponed so that the changes only make it into Gradle 6.6.
Fixing this issue for the Gradle Scala users remains important though!
* | ||
* Cache is based on file timestamps, because this method is used in sbt implementation. | ||
*/ | ||
static class TimeCheckingClassLoaderCache implements AbstractClassLoaderCache { |
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.
The AbstractClassLoaderCache
does not exist in Zinc 1.2.0 which is supported by Gradle.
ZincUtil.constantBridgeProvider(scalaInstance, bridgeJar), | ||
ClasspathOptionsUtil.auto(), | ||
k -> scala.runtime.BoxedUnit.UNIT, | ||
Option.apply(new ClassLoaderCache(COMPILER_CLASSLOADER_CACHE)) |
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.
Since we cannot use the AbstractClassLoaderCache
, what are the options here?
I admit not being 100% at ease with what this exactly means.
From basic testing, it seems that replacing new ClassLoaderCache(COMPILER_CLASSLOADER_CACHE)
with new ClassLoaderCache(scalaInstance.loader())
works (in the sense that it allows Gradle tests to pass) though I did not check the performance impact.
An alternative is to forget about the internal Zinc class loader cache and go back to invoking ZincCompilerUtil.scalaCompiler
.
In short, the changes done should not make UpToDateScalaCompileIntegrationTest
fail. Note that in that test the @Requires
on the first test method can be removed.
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.
Test shows that ClassLoaderCache(scalaInstance.loader())
does not bring any noticeable gains over ZincCompilerUtil.scalaCompiler
. The idea is pretty interesting, but it seems that compiler loads different classes than loader (bridge jar?). I also think that it is a little hackish to assume which classes AnalyzingCompiler will read.
Before we cross out AbstractClassLoaderCache... how about using reflection to load AbstractClassLoaderCache/TimeCheckingClassLoaderCache only when it is possible and otherwise use default ClassLoaderCache (which might leak some resources) or recreate it each time (which is slower by ~33%). By default Zinc 1.3.5 is used, so most of the users will benefit fully from the fix.
I've pushed a new commit with such a fix.
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.
Yeah, I think it makes a lot of sense to optimize for Zinc 1.3.5 since that's what is shipped by default. Is there a good reason to use older versions of Zinc?
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.
Is there a good reason to use older versions of Zinc?
Not sure there, not a Scala user myself. However we try hard to not break compatibility in minors. We can certainly discuss dropping Zinc 1.2.x support for Gradle 7.0 for example.
For now, I think having the code handle both cases is best.
Let's see how the PR behaves through the full test suite.
Thanks again for being reactive and following up.
Issue: gradle#12591 Signed-off-by: Sheliak Lyr <[email protected]>
Signed-off-by: Sheliak Lyr <[email protected]>
91e1048
to
f666121
Compare
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.
@SheliakLyr Thanks for updating this. It is good to go from our end.
I have rebased the PR on the latest state of the release
branch and am running one last build.
Once it passes, I will merge it so that it ends up part of Gradle 6.5 in the end.
We had further reports of Scala users being impacted by performance when adopting Gradle 6.x so we decided it was more important to get it sooner out there than later.
This is great news. We updated Apache Kafka to Gradle 6.3 (and later Gradle 6.4) and there have been multiple complaints regarding compilation speed. So, I'm encouraged by this development. |
This is now merged, thanks again! Please let others know so that we get testing with 6.5 RC1 which should be out early next week. |
Thanks @ljacomet! I am really glad you merged this already. Eagerly awaiting RC1. |
Looks like the milestone associated should be updated from 6.6 RC1 to 6.5 RC1. |
🤦 Fixed. |
Gradle 6.5 includes a fix for gradle/gradle#12866, which affects the performance of Scala compilation. I profiled the scalac build with async profiler and 54% of the time was on GC even after the Gradle upgrade (it was more than 60% before), so I switched to the throughput GC (GC latency is less important for batch builds) and it was reduced to 38%. I also centralized the jvm configuration in `build.gradle` and simplified it a bit by removing the minHeapSize configuration from the test tasks. On my desktop, the time to execute clean builds with no cached Gradle daemon was reduced from 127 seconds to 97 seconds. With a cached daemon, it was reduced from 120 seconds to 88 seconds. The performance regression when we upgraded to Gradle 6.x was 27 seconds with a cached daemon (#7677 (comment)), so it should be fixed now. Gradle 6.4 with no cached daemon: ``` BUILD SUCCESSFUL in 2m 7s 115 actionable tasks: 112 executed, 3 up-to-date ./gradlew clean compileScala compileJava compileTestScala compileTestJava 1.15s user 0.12s system 0% cpu 2:08.06 total ``` Gradle 6.4 with cached daemon: ``` BUILD SUCCESSFUL in 2m 0s 115 actionable tasks: 111 executed, 4 up-to-date ./gradlew clean compileScala compileJava compileTestScala compileTestJava 0.95s user 0.10s system 0% cpu 2:01.42 total ``` Gradle 6.5 with no cached daemon: ``` BUILD SUCCESSFUL in 1m 46s 115 actionable tasks: 111 executed, 4 up-to-date ./gradlew clean compileScala compileJava compileTestScala compileTestJava 1.27s user 0.12s system 1% cpu 1:47.71 total ``` Gradle 6.5 with cached daemon: ``` BUILD SUCCESSFUL in 1m 37s 115 actionable tasks: 111 executed, 4 up-to-date ./gradlew clean compileScala compileJava compileTestScala compileTestJava 1.02s user 0.10s system 1% cpu 1:38.31 total ``` This PR with no cached Gradle daemon: ``` BUILD SUCCESSFUL in 1m 37s 115 actionable tasks: 81 executed, 34 up-to-date ./gradlew clean compileScala compileJava compileTestScala compileTestJava 1.27s user 0.10s system 1% cpu 1:38.70 total ``` This PR with cached Gradle daemon: ``` BUILD SUCCESSFUL in 1m 28s 115 actionable tasks: 111 executed, 4 up-to-date ./gradlew clean compileScala compileJava compileTestScala compileTestJava 1.02s user 0.10s system 1% cpu 1:29.35 total ``` Reviewers: Manikumar Reddy <[email protected]>, Chia-Ping Tsai <[email protected]>
Gradle 6.5 includes a fix for gradle/gradle#12866, which affects the performance of Scala compilation. I profiled the scalac build with async profiler and 54% of the time was on GC even after the Gradle upgrade (it was more than 60% before), so I switched to the throughput GC (GC latency is less important for batch builds) and it was reduced to 38%. I also centralized the jvm configuration in `build.gradle` and simplified it a bit by removing the minHeapSize configuration from the test tasks. On my desktop, the time to execute clean builds with no cached Gradle daemon was reduced from 127 seconds to 97 seconds. With a cached daemon, it was reduced from 120 seconds to 88 seconds. The performance regression when we upgraded to Gradle 6.x was 27 seconds with a cached daemon (#7677 (comment)), so it should be fixed now. Gradle 6.4 with no cached daemon: ``` BUILD SUCCESSFUL in 2m 7s 115 actionable tasks: 112 executed, 3 up-to-date ./gradlew clean compileScala compileJava compileTestScala compileTestJava 1.15s user 0.12s system 0% cpu 2:08.06 total ``` Gradle 6.4 with cached daemon: ``` BUILD SUCCESSFUL in 2m 0s 115 actionable tasks: 111 executed, 4 up-to-date ./gradlew clean compileScala compileJava compileTestScala compileTestJava 0.95s user 0.10s system 0% cpu 2:01.42 total ``` Gradle 6.5 with no cached daemon: ``` BUILD SUCCESSFUL in 1m 46s 115 actionable tasks: 111 executed, 4 up-to-date ./gradlew clean compileScala compileJava compileTestScala compileTestJava 1.27s user 0.12s system 1% cpu 1:47.71 total ``` Gradle 6.5 with cached daemon: ``` BUILD SUCCESSFUL in 1m 37s 115 actionable tasks: 111 executed, 4 up-to-date ./gradlew clean compileScala compileJava compileTestScala compileTestJava 1.02s user 0.10s system 1% cpu 1:38.31 total ``` This PR with no cached Gradle daemon: ``` BUILD SUCCESSFUL in 1m 37s 115 actionable tasks: 81 executed, 34 up-to-date ./gradlew clean compileScala compileJava compileTestScala compileTestJava 1.27s user 0.10s system 1% cpu 1:38.70 total ``` This PR with cached Gradle daemon: ``` BUILD SUCCESSFUL in 1m 28s 115 actionable tasks: 111 executed, 4 up-to-date ./gradlew clean compileScala compileJava compileTestScala compileTestJava 1.02s user 0.10s system 1% cpu 1:29.35 total ``` Reviewers: Manikumar Reddy <[email protected]>, Chia-Ping Tsai <[email protected]>
Reverts part of 55cc646
Issue: #12591
Signed-off-by: Sheliak Lyr [email protected]
Fixes #12591
Context
Fixes performance regression introduced in version 6.0. See Issue #12591 for details.
Contributor Checklist
<subproject>/src/integTest
) to verify changes from a user perspective<subproject>/src/test
) to verify logic./gradlew <changed-subproject>:check
Gradle Core Team Checklist