By Francisco Sánchez Garcia, SwiftKey IME Team Lead
SwiftKey Tech Blog – posts by developers, for developers. Find more here: swiftkey.com/tech-blog
When non-technical people think about the performance of their smartphone, usually the first thing that comes to mind is speed. But for software engineers, the word ‘memory’ comes into the equation. Why?
Mobile developers – on any platform – need to always be aware of the technical limitations of the handsets that are running our apps. This means we need to be really careful not only with providing an acceptable speed but also a relatively low memory consumption. Finding the right balance is hard, and it’s even harder when you have to deal with thousands of different devices and configurations, as is the case on Android.
In order to speed up the keyboard, we normally have to make a sacrifice and increase the memory consumption. However, there is always a line where loading more things in memory doesn’t help speed up the app but instead it slows it down… and with SwiftKey on Android, we learned where this limit is through some difficult trial and error.
At the end of May, we released version 5.3.0 of SwiftKey Keyboard for Android to market. Our internal performance testing framework showed good results. After incremental test rollouts to the user base, everything appeared stable and ready to go – and so we pushed the app to 100% of our Android users.
We began to hear reports of crashes, lag and high RAM usage shortly after the update, so the first step was to try to reproduce the issues experienced by our users. We suspected it had something to do with memory consumption and started running extra tests and looking for evidence that could confirm our hypothesis.
After reproducing the issue on different devices and looking through the logs, we found the root of the lag problem. The garbage collector, a mechanism in Java that releases objects and the associated memory that are no longer needed, was being triggered too often. In some cases, when this happens, the mechanism blocks the thread for a few milliseconds – which can ultimately be perceived as freezing or lag on keypresses while using the keyboard.
But, what about the crashes? Since it takes longer for Android to increase the heap limit in some devices, these crashes were also produced as a side effect of the high memory consumption. As a result, large memory allocations (as we have when opening the theme selector from the SwiftKey Hub) were producing OutOfMemory errors.
Now that we’d identified the cause, it was time to fix it. We set up a meeting across teams to share knowledge and develop a plan to resolve the issue. Any information about Android memory management or tools was shared, and then we split up the tasks and assigned them to relevant teams. We used two main tools to get to the bottom of the issues and start fixing them:
- Eclipse Memory Analyzer – this tool allows us to load/import a dump of the Java heap and analyze the allocated objects and the relationships between them. Using this tool, we could then zero in on which objects were retaining more memory – and how to free them.
- Android Device Monitor – this tool allows us to take a dump of the Java heap and convert it to the format supported by the Eclipse Memory Analyzer. It also lets us monitor the memory and CPU consumption, then track these allocations over time. This tool also has many other features that we did not end up needing in this case, such as Method Profiling, File Explorer, Network Statistics and more.
So what did we find and how did we fix it?
When we originally created the Hub, we thought that loading one tab on demand and then releasing it when the user switches to a new one was the best way to design it. Lazy loading is a good solution, and why would we want to keep the tabs in memory if they were fast enough to load anyway? That seemed alright – however, during our investigations of the memory issues, we concluded that Android couldn’t keep up with the amount of memory being allocated, particularly when switching from and to the Themes Selector tab.
The issue was not in allocating many bitmaps at once and releasing them when switching to another tab; rather, the issue lay in the user returning to a previously opened tab, and the memory from this earlier visit not having been released completely yet. The garbage collector was being fed more and more work – and we were getting closer to the Java heap limit, even reaching it on some devices. We also knew that on some particular handsets (Galaxy S series, Xiaomi Mi4i, for example), it takes longer for Android to increase the heap limit thus making it easier for an OutofMemory error to trigger.
Ultimately, we kept the lazy loading for the tabs, but they are only released when the Hub has been completely closed so that no releasing occurs during tab switching. With this solution, Android was able to keep up with the garbage collection properly, and crashes related to memory issues were reduced by 90%.
We currently use the popup window widget from Android. However, we found that we were caching the popups we use if the user presses the same key, but not the views in it. To counter this, we added a pool to cache those for a certain amount of time and avoid allocating memory for them again.
This change was mostly about increasing the view object lifetime and preventing us from creating and releasing extra ones – therefore reducing the need to trigger the garbage collector.
After the keyboard UI refactor we did for 5.3, we found that the view hierarchy was still too deep and that we needed to flatten it a bit. This hierarchy was a bit complicated as well and we found some subtle leaks when switching keyboard layouts, rotating the device, undocking the keyboard and changing the mode. A reload of almost the entire hierarchy was being triggered each time.
We dedicated a few weeks to shaving off all of the unnecessary view creations and reusing them properly.
Android target SDK version
During the SwiftKey 5.3 development, we also upgraded some of our tools and the target SDK version to Lollipop. We didn’t see any big side effects at that point.
After our investigation, we found that hardware acceleration is enabled by default when targeting this SDK version for windows without a parent activity (such as our keyboard) and it is not possible to disable it at a window level programmatically. This means that this change was giving us a big increase on memory usage when the keyboard was on the screen. We contacted Google about the issue and hopefully, it will be fixed in Android M.
These were the changes that had the biggest impact, but we also made a few other tweaks that helped to reduce the memory consumption and avoid some subtle leaks, such as reducing the amount of listeners registered across the view hierarchy.
What we learned
Our take on this release was that finding the right balance is really hard, and we, as developers, should be always aware that memory is a valuable and limited resource, so its consumption should only be increased in favour of speed when it’s really needed and when it’s not going to affect the rest of the app.
Another good lesson we learned the hard way is that our internal performance framework had a big flaw: it was using phones under controlled environments with a limited set of apps, so it wasn’t reproducing real situations where our keyboard has to coexist with lots and lots of other applications with the same amount of available memory.
To wrap up, we must admit we had a lot of fun while doing this work, and that seeing the amount of lag reports turning into nice reviews over the time was one of the most rewarding things we’ve ever seen as developers. Please send us any feedback you’d like! We’d be happy to hear them or discuss with other developers You can find us on Twitter and Facebook!