As a member of the performance engineering group at Mozilla, I am one of the people helping Firefox Preview’s development team with optimizing its startup time. Over several weeks, we have implemented many "easy" optimizations that significantly improved application startup time. Easy is in quotes to emphasize the irony -- none of those startup performance optimizations were truly easy. The entire Firefox Preview team contributed to that effort and we were able to improve application startup by, primarily but not exclusively, deferring or parallelizing time-consuming startup tasks. More blog posts to come detailing the team’s earlier startup optimization achievements; the scope of this entry is more limited.
After shaking free all the low-hanging fruit from the startup speed tree, we wanted to step back and look at application startup time from a wider perspective. I began to wonder if there was a way that we could optimize the application's on-disk representation to make it easier for the Android OS to load it into memory. I invited Jonathan Almeida, a talented, smart colleague into the investigation and the first thing we noticed was that on a Pixel 2 it took the OS more than three quarters of a second to load the application before even starting the Activity lifecycle.
Using the Android Profiler, we found that almost the entirety of application loading was being spent reading and parsing the application's dex files. dex files contain the application's byte code in Dalvik Executable format. Using Android Studio's tools for analyzing an apk, the compressed file that contains all the application resources (including code, images, layouts, etc), Jonathan and I were able to inspect the size and composition of Firefox Preview's dex files.
Figure 1 shows Studio’s Apk Analyzer at work. We saw that this version of Firefox Preview's code requires three dex files, making it a so-called multi-dex application. Upon discovering that Firefox Preview's code did not fit in a single dex file and because Jonathan knew that multi-dex files have implications for runtime performance, we hypothesized that they also negatively affect startup performance.
There are several reasons an application may end up with multiple dex files. Size is one of them. Firefox Preview has a robust set of dependencies and we weren't sure how well we minimized and tracked the library dependencies listed in our Gradle build files. If there were dependencies listed that we did not use, removing those would reduce the size of Firefox Preview's code and, Jonathan and I hoped, shrink the size of the bytecode to the point where it could fit in a single dex file.
Following best practices of Android development, we already used ProGuard within our build process to strip out unused methods and functions. The use of ProGuard means that a manual accounting of dependencies like this should have been unnecessary. Jonathan and I decided to do the manual inspection for excess code regardless, mostly out of curiosity. We found just a single dependent library that was listed in the Gradle build file but not actually used: Glide.
Glide is a library that 'offers an easy to use API, a performant and extensible resource decoding pipeline and automatic resource pooling' for 'image loading … focused on smooth scrolling'.
The next step was to see if that code made it into Firefox Preview's dex files. Again, because we were already using ProGuard, that code should not have been present. But, lo and behold, it was.
Figure 2 shows that the code for Glide did end up in Firefox Preview's dex file. Relative to the overall size of Firefox Preview’s three dex files, removing these unused classes and methods was not going to have a major impact on their size.
Critically, though, it did indicate that something about our use of ProGuard was amiss.
There are a bewildering number of options available for configuring ProGuard. By complete accident, we noticed the
-whyareyoukeeping option and configured ProGuard to tell us why it thought that it could not remove Glide from the output dex file despite the fact that Firefox Preview never instantiated any of its classes, used any of its static methods, etc.
Figure 3 shows ProGuard thought it needed to keep Glide’s code not because it was used but rather because it was obligated to follow a rule that told it to keep public and static methods from all classes. Those public methods, in turn, introduced a cascading set of dependencies on most of the library's methods and classes.
The presence of such a catch-all rule was odd -- it wasn't anywhere in Firefox Preview's codebase! Perhaps it was in the baseline rules included in the Android SDK? Nope, not there. We widened our search into the source code for the libraries that Firefox Preview uses. We didn't get far before we found the rule in the ProGuard configuration for GeckoView (GV).
Neither Jonathan nor I is an expert at ProGuard, so it was a riddle how a ProGuard rule in a dependent library could "escape" and "infect" higher-level packages. We started to search through ProGuard information on the Internet and stumbled upon this "helpful" tidbit:
For library modules and for libraries distributed as AARs, the maintainer of the library can specify rules that will be supplied with the AAR and automatically exposed to the library consumer's build system by adding this snippet to the module's build.gradle file: ... The rules that you put in the consumer-proguard.txt file will be appended to the main ProGuard configuration and used during the full application build.
Well, oops. GeckoView's conservative ProGuard configuration seemed to be effectively negating Firefox Preview's ProGuard code-shrinking optimizations. We were cautiously optimistic that fixing this issue would result in not only the removal of Glide’s methods and classes from Firefox Preview but an overall smaller amount of byte code and, perhaps, an application whose code fit entirely within a single dex file.
It was time to do some work. Jonathan and I removed that directive from GeckoView's ProGuard configuration, rebuilt Firefox Preview and went back to our friend, the Android Studio Apk Analyzer.
Wow. From three dex files totaling almost 5.5MB to a single dex file totaling 3.4MB all with a single change. Major progress! But, there are still two outstanding questions.
First, why was Glide still referenced in the new, smaller dex file? Although there are fewer methods kept, it was puzzling to see it there at all. Remember, Firefox Preview never uses it! To answer this question, we went back to the trusty
-whyareyoukeeping option in ProGuard. Again, we found that ProGuard kept those methods because of a configuration rule.
Funny, though, this rule existed nowhere in any of Mozilla’s code.
greping through the Firefox Preview source code and the codebases of the various Mozilla components used by Firefox Preview confirmed this. The only remaining option was that a dependency used by Firefox Preview was doing the same thing that GeckoView was doing: leaking a static ProGuard configuration to its users that was overriding ProGuard’s willingness to strip Glide entirely from the output dex file. We guessed that it was more than likely in Glide itself.
Jonathan and I found that very ProGuard configuration directive in the Glide source code. The sad implication of this finding is that we could not rely on ProGuard to automatically remove Glide entirely and we would have to manually update Firefox Preview's dependency configuration. Following the appropriate two-line change to Firefox Preview’s
build.gradle file, we rebuilt Firefox Preview’s apk to test our work.
Whew. Glide is finally exorcised from the dex file!
Second, and more importantly: Did going from a multi-dex to a single-dex application have an impact on startup performance? The Android profiler could help Jonathan and I definitively answer this question. Figures 7 and 8 show the relevant portion of the profile of Firefox Preview startup before and after the ProGuard configuration change:
Profiling proved our initial hypothesis: multi-dex Android applications negatively effect startup performance and runtime performance.
The path Jonathan and I followed to achieve a decrease in the time that the Android OS took to load Firefox Preview led us on an Alice-In-Wonderland-like trip where we found ourselves profiling application runtime, analyzing dex files and apks and learning more about ProGuard than we ever wanted to know. The savings we gained was far more than we could have imagined when we started and more than justified the time we spent traveling this long and winding road.
Update: This post has been edited to remove references to Mozilla-internal names and to explicitly reference my previously unnamed, mystery collaborator.
Update 2: Removed the first paragraph because it contained an implicit promise that I did not intend.