-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
JVM build time improvements via tracking unused deps or classes [Java part] #16457
base: master
Are you sure you want to change the base?
JVM build time improvements via tracking unused deps or classes [Java part] #16457
Conversation
Hello @oliviernotteghem, Could you please resolve the branch conflicts. Thanks! |
Before resolving the conflicts, let's discuss the general approach first; I don't want @oliviernotteghem to spend more time on this than necessary before validating whether the general idea works or not (I'm saying this without having read the source code) |
What this change seems to do is to embed Java-specific knowledge into the generic action execution machinery of Bazel which tells which inputs files were unused in a Java compilation so that changes to those files don't cause a rebuild. This is contrary to the direction in which we are going because we are trying to remove language-specific knowledge from Bazel. Granted, it's going to take a long time for Java due to the optimization in I don't have all knowledge about Java swapped into my brain so I only have generic questions:
|
Correct : the changes under /usage folder are JVM specific (Java/Kotlin), as these are the rules that currently can report, via .jdeps output artifact, used dependencies (and used classes, which is jvm-specific obviously and do not make sense for other languages/rules). However, the rest of the changes are language agnostic.
Definitely. The code is currently encapsulated in the
Sure, these are good questions - happy to respond. This optimization helps for incremental builds, think of a developer making a simple code change to an XL android app. Let's say the code change is in java library XYZ. By default, Bazel will rebuild all targets that are XYZ has a transitive dependency (first scenario in benchmark), even when using ABI as compile-time jar. Why? Because the ABI jar of XYZ is an input to all these targets... This is the #1 reason why Bazel jvm build times are slow is slow (when compared to, let's say, a build system like Buck). From there, several things can be done. Enabling classpath reduction, as you mention, can help... but is limited as it prunes only second-level dependencies (i.e deps of direct dependencies). A more efficient way is to make sure your project adheres to However, this is not optimal nor sufficient : direct dependencies specified in BUILD.bazel might still be unecessary if they are unused, causing unnecessary rebuild if their ABI changes. Also, exported dependencies are also not handled and can create additional unnecessary invalidations. Last, tracking ABI changes at the jar level (i.e rebuilding a target whenever a dependency's ABI jar changes) causes unnecessary rebuild when the classes that changed within this dependencies are actually not used during compilation of our target. Simply put, if a target needs only class A from dependency XYZ to compile, and change to class B of XYZ should not cause target to invalidate. I hope this answer your question about better understanding what files can be removed from the set of inputs. And to add further clarification, because any change to input set would cause invalidation, the implementation does not change or remove files from the input : it contains always 100% of the files defined at analysis time. What it does it simply ignores some of the inputs when computing the cache key and/or uses instead hashes computed from classes within ABI jar themselves (as opposed to the hash of the whole jar file).
Exported dependencies can't be removed statically (one of the solution is to ban export entirely, which is unfortunately not as option for most companies, in order to preserve a seamless developer experience with things like dependency injection). Also, as said above, the approach of removing dependencies has limitation and doing it alone will not match Buck build system build time for example, which has the equivalent of the behavior I described above (https://github.com/facebook/buck/blob/5444652af489563ddaf595a634341dd14fd41399/src/com/facebook/buck/jvm/java/ClassUsageTracker.java)
Unfortunately, this is not granular enough to implement per-class usage tracking / compilation avoidance, as this applies to action's inputs (which are dependency ABI jars in our case, not the files used by the dependency). Also, this is meant to be used rather by rules written in starlark, and, as you know, Bazel java rule is native. I will be happy to jump in a call and help clarify some of my responses here, if this help. Don't hesitate! |
How much does this increase Bazel's retained heap size for your example builds? ( |
src/main/java/com/google/devtools/build/lib/actions/usage/ActionInputUsageTracker.java
Show resolved
Hide resolved
I think the conversation should be split into two threads: one about unused .jar files and the other about unused classes in used .jar files. At Google, we, as you recommend, enable Do you have a good sense of how far you could get with these two optimizations? I'd much rather have only one "officially sanctioned" way to deal with unused .jar files and the Bazel way so far seems to be strict Java deps and unused deps. The part about unused .class files is intriguing because it offers theoretically optimal rebuilds but it also comes with a lot of risk; there are probably a number of subtle scenarios where a rebuild is required even though it's not obvious at first glance (adding or removing a class early in the classpath comes to mind, but that's very probably not the only case). It also comes at a large cost of complexity in Bazel. @comius @cushon do you think this is a direction worth exploring at Google? Absent the opinion of @comius and @cushon , I would be inclined to ask you to implement pruning changes to unused classes as as stateful worker instead (cc @larsrc-google ). That, AFAIU you can do without any changes to Bazel core and would provide most of the benefits you seek. |
@alexjski also mentioned the possibility of offline analysis of .jar files: if you find that a particular set of classes in a .jar file is unused by its reverse dependencies, that's a good indication that they should be split from the .jar . Granted, this is not the theoretically best possible solution, but seems much less risky than what the change does a the moment. |
Update: we had an accidentally non-public discussion about this pull request and the prevailing opinion seems to be that while this is very interesting work, no one is eager to take on the maintenance of such a complex piece of code and that most of the benefits can be realized by cutting dependencies in the BUILD file. This change would also increase the heap use of Bazel and add some risk of incrementally incorrect builds, especially if we want to defend against malice; the example @alexjski brought up was when a .class is added to a .jar file early in the classpath: then it's a new class so it's not used, but at the same time overrides classes with the same name later in the classpath (I haven't checked whether this particular case is covered by your change) So I recommend experimenting with having a |
As for the idea of letting the worker handle unused class pruning: Yes, theoretically a worker could hold a cache of "compiling this class only required these other classes" and avoid recompiling if those classes or the src itself hasn't changed. However, our experience from creating the bootclass cache (a0e5e45 etc) is not promising. The main problem is that reading the jar files is one of the most expensive steps, and we would have to do that anyway - any jar file we didn't need any classes from would have been subject to jar file pruning. The promising part of this PR is that it works on the action cache level, so the decision to not execute the action is much earlier. I'm worried though that there are some scenarios where this would lead to incorrect compilation due to how Java works, but @cushon would be the expert on that. |
The implementation_deps attribute aims at solving the same problem for C++, it may be useful to use a common approach. [Edited to not point at an internal doc] |
There was actually a previous attempt at doing this at Google, back in 2012. One important outcome was finding that different JDK releases can have different requirements. Between that and the manual work that would be involved in keeping the deps up to date, the project was abandoned. It's hard to say how often the JDK release breakage would happen, and automation has gotten a lot better, so these findings may not be relevant any more. |
We've been discussing this quite a bit internally. The idea is definitely of interest, expect for the reuse of |
@larsrc-google Do you have a writeup or some more details about |
The design as such would be as described here, except with a new attribute instead of reusing |
Thank you! I think I am confused because I can not find directly in the PR files where |
Yeah, that's a bit confusing too, but as mentioned above there are worse issues with using |
@lberki Not sure if your questions were all answered but I'd like to give a perspective why this is important to us.
The problem that is biting us heavily is that all transitive jars (not just the direct bazel/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java Lines 233 to 235 in 70b43db
Given the following (simple) graph:
Given the following change:
Expected outcome:
Actual outcome:
The reduced classpath mode is a weird one. Even though it reduced the number of transitive jars, this only helps in combination with remote/disk_cache and a stable reduction. However, the reduction isn't stable. For example, the implementation change can result in an unused direct deps, which would be reduced and you end up rebuilding B and C unnecessarily again.
I think in general pruning direct deps should really happen with a tool via
I don't see this supported currently with JavaCompileAction. However, if there is interest I am willing to look into allowing a Java toolchain to respond to Bazel with such an unused list (we have one based on the Eclipse Java compiler). I can probably make this configurable. But I think this requires some significant updates to JavaCompileAction to make that work. I need to know if that is something The Bazel team is willing to consider. Right now we are seeing a lot unnecessary rebuilds, which result into a 7-10 minutes time waste for some developers working in some areas of our graph. Thus, we really like to do something but we need to know which approach The Bazel team would be willing to merge. |
That... would seem like a bug. However, it occurs to me that B and C might still be rebuilt if A's jdeps file changes. Is that's what's happening? Maybe just file a separate bug with a repro we can look at.
I guess I'm mildly surprised that the reduced classpath optimization would only work with a remote cache. If that's the case, I'd file an FR to see if that restriction can be removed.
This is because the .jdeps inputs change, right? If reduced classpath mode isn't working anyway, you might consider disabling it, which I'd hope would avoid passing .jdeps files to JavaBuilder and at least avoid this scenario.
The unused_inputs file would basically contain the inverse of the .jdeps file, so I feel supporting it should be eminently feasible. How effective it would be I'm not sure: I suspect it wouldn't address any of the issues mentioned above where direct inputs did change. But it should be helpful in scenarios with longer chains of dependencies. |
Can you give a (preferably minimal) reproduction for this? I'm with @kevin1e100 in that this looks like a bug.
I'm afraid I don't understand this; you're right that this only works with
I'll defer to @hvadehra on this. I'm not against adding some sort functionality that allows the Java toolchain to signal to Bazel that some inputs were not required. In fact, it might even be that you don't need to do anything extra other than changing
|
Since I got pretty confused about what works and what doesn't, I created a small test bed for Bazel Java compilation avoidance here, based on a very recent HEAD commit. If you run @guw I can't reproduce the rebuilds caused by implementation-only changes with this. It would be very helpful if you could try out the reproducer and add a case for the situations in which you would like Bazel to avoid more rebuilds, in particular one in which the reduction is "unstable". |
Thanks to @fmeum for the repo we now have a simple reproducible scenario with shows unnecessary compilation and/or cache misses. |
The cases we have identified so far in which compilation could be avoided if Bazel was "smarter" so far fall into the following categories:
(If anyone can identify any other situation, please speak up :-)) Given the above, I would suggest the following plan:
@cushon @hvadehra for their feedback on 1. and 2. in particular. |
I drafted a PR to get started on the "native turbine image" step of this plan: #19361 |
Thank you for contributing to the Bazel repository! This pull request has been marked as stale since it has not had any activity in the last 1+ years. It will be closed in the next 90 days unless any other activity occurs. If you think this PR is still relevant and should stay open, please post any comment here and the PR will no longer be marked as stale. |
This PR is a working implementation / acts as starting point for discussing the need for the (controversial) optimization of jvm build time in Bazel, done via dynamically tracking & pruning unused dependencies, and classes (for optimal results).
Context
In some companies (including Uber), regression of local build time is a blocker for adopting Bazel for Android builds. Therefore, in-depth optimizations of incremental build present in build system like Buck are needed in order to have acceptable build time.
PR Summary
Command line options
--experimental_action_input_usage_tracker=disabled|unused_dependencies|unused_classes
--experimental_track_class_usage
andexperimental_track_class_usage = "on"
(in your kotlin toolchain definition)Risks
Benchmark
The following benchmark was done on a XL production android mobile app, forcing deep ABI-code change incremental builds.
JAVA
KOTLIN
(*) #16426, bazelbuild/rules_kotlin#842
(**) bazelbuild/rules_kotlin#849