Android Performance - The Why and The How


Every now and then we receive feedback on the performance and APK size of some NativeScript Android application, especially built with Angular 2. In this post I will try to elaborate a bit more on the technical challenges behind the Android Runtime, Core Modules and Angular 2 integration projects as well as to share some easy steps that would boost the performance several times.

Loading (Start-Up) Time

There are several tasks that happen while a NativeScript Android application loads:

  1. The NativeScript native library (*.so) is loaded and the V8 JS VM is initialized.
  2. The metadata files are extracted, read and their in-memory representation is built.
  3. All JavaScript files are extracted from the APK archive to the file system for faster access later on upon require. Currently, if not web-packed, there are some ~3500 JS files in a blank angular application. Execution complexity of the file extraction task is linear to the number of files - the more JS files in the package the more IO operations are performed.
  4. The main application JS file is required. A require operation includes script parsing, compilation, execution and caching on the native side. The initial require graph is populated and cached.

Note: File extraction happens only upon the first application run after fresh installation. On next runs the files are read directly from the file system.

The first two steps are constant in term of execution time and are of no interest for optimization. Steps 3 and 4, however, are the main suspects for increased loading time. How do we optimize these? Meet the android-snapshot plugin.

What this snapshot plugin optimizes is:

  • It provides a precompiled binary representation of all the framework JS files, including Angular and Core Modules.
  • What gets inside the APK is a single binary file vs. 3500 separate files. Only the application logic - that is what resides within the “app” folder - gets packed as separate JS files. This saves a lot on the extraction time of step 3.
  • The precompiled JS source saves a lot on step 4 - all Angular and Core Modules are preloaded in the V8 heap.

The only trade-off is that, due to snapshot files being CPU-dependent, application package size is increased with additional 5MB. Still, we have plans how to further improve this (read more in the Package Size section).

If you take a look at the Readme file on the snapshot repo you will see the approximate numbers of our tests. With snapshot enabled a blank Anular 2 application’s loading time is improved nearly twice - 4 seconds vs. 2 seconds.!

Synchronizing Two Garbage Collectors

V8 runs its own Garbage Collector and so does Android (Dalvik VM). Due to the architectural paradigms of NativeScript, especially the 100% native access through JavaScript one, native Android objects are sometimes proxied in JavaScript. Because of this, the Android Runtime uses complex mechanism to keep proxied native instances alive until the JavaScript proxy object gets collected. The problem is that the two Garbage Collectors live in separate worlds and run at their own pace. The Android Runtime tries to put equal memory pressure when allocating large native object - like Image - on the JavaScript side but this is just a hint. Sometimes the Dalvik GC needs to clean some large objects - like Bitmaps - to free heap memory but because these objects are proxied in JavaScript and proxies are still alive there nothing happens and the Dalvik heap is not cleaned. Most of the time the JS proxies can be collected by that time but V8’s GC hasn’t started yet, while the Dalvik one is already running. This eventually leads to a memory leak and OutOfMemory Android exceptions, especially when working with large native objects like Bitmaps and/or streams. A good read and example that covers this topic in deeper technical details may be found here.

With the above said - sometimes we need a mechanism to synchronize the two Garbage Collectors to ensure proper memory reclaim. V8 exposes a `gc()` call behind a flag and this flag is enabled by default for a NativeScript Android application. The NativeScript core modules take advantage of this behavior and call V8’s `gc` upon navigation. Why upon navigation? Because typically after a navigation action the previous page’s JavaScript Visual Tree becomes reachable by V8’s Garbage Collector. As I already mentioned collecting the proxies will make the corresponding native object reachable by Dalvik’s Garbage Collector.

All of this sounds like a perfect solution but unfortunately it leads to some performance issues when V8’s GC is unconditionally forced because it is a blocking operation running on the main UI thread. We are trying to call V8’s gc only upon main application loop idle but it seems this heuristic is not working as expected during navigation.

Calling JavaScript GC explicitly during navigation is one of the major performance bottlenecks within the NativeScript core modules. If you experience this today you may simply comment this line and see how it goes. We are currently working on a more generic solution that will be handled within the Android Runtime directly. Although it also uses some heuristics, it looks promising and seems to work in some 99% of the memory leak scenarios we’ve isolated so far.

Application Package Size

As I explained in this blog post some time ago, the NativeScript Android Runtime ships with three separate builds for the three major CPU architectures available nowadays. This makes a blank Hello World application some 12MB big. If APK size is something that is critical for your clients then you may produce separate APKs for each CPU architecture your application needs to run on. For more information you may refer to the ABI splits section in the Publishing for Android help article.

How Do I Improve Performance Today?

Here is what you can do to improve the performance of your NativeScript Android application:

  • Make sure you use the android-snapshot plugin (for iOS use the WebPack tool).
  • Comment the explicit `gc` call within the core modules. We are working on a fix that will handle GC synchronization directly within the Android Runtime.
  • Use ABI splits to reduce APK size

Future Improvements

We have plans to further improve all of the above three major aspects of the overall performance in a NativeScript Android application:

  • Enable the snapshot plugin by default for Android applications.
  • Improve snapshot generation. Currently the plugin ships precompiled versions with all the Angular and Core Modules code. Instead, we can first use WebPack on the client machine, to skip all unnecessary code and then run the snapshot tool over the bundled result. This way we will be able to ship only small portion of today’s binary.
  • Handle the two Garbage Collectors synchronization internally within the Android Runtime instead of calling `gc` manually from JavaScript.
  • Improve the default APK size (see this GitHub issue for more details). If ABI split is enabled apply it on the snapshot plugin as well.

Conclusion

Although NativeScript applications are 100% native and run fast and fluid in most of the cases, sometimes an application may need additional fine tuning, to become even faster. The NativeScript engineering team is actively working towards making all the above mentioned improvements enabled by default. As always, feedback is most welcome and much appreciated - please share it within the GitHub NativeScript and Android Runtime repositories.

Comments


Comments are disabled in preview mode.
NativeScript is licensed under the Apache 2.0 license
© 2020 All Rights Reserved.