29

Your Android Application Silently Skips Frames

 4 years ago
source link: https://www.tuicool.com/articles/yABZv2z
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Last week I’ve been doing some benchmarking of multithreaded code on my old Galaxy S4. This involved starting 2000 threads which competed for some resources and performed about 3000 lock acquisitions before terminating. This antique device demonstrated surprisingly good performance (about six seconds for the entire flow in most cases), but there was one result I simply couldn’t believe: test application didn’t skip any frames!

Skipped Frames

Android application should aim for a magic number of 60 frames per second. It means that every 16 millisecond it should show a new frame on the screen. However, if the app does too much work on User Interface (UI) thread, it might be unable to complete frame preparation in this time window. When this happens, the system will show the previous frame instead of a new one. Then, your app will get a chance to update the screen only after additional 16 ms. This frame, which wasn’t shown because it hadn’t been ready on time, is called “skipped frame”.

Skipped frames are a major issue because your application looks bad when it skips frames. Depending on the number and the frequency of skipped frames, the app might feel anywhere from “not smooth” to totally stuck. This issue affects end users directly, so it’s not something you can ignore.

Skipped Frames Warnings

Luckily for us, we get automatic warnings about skipped frames when we develop our applications. They come in the form of messages like this one in logcat:

2019-10-13 00:15:27.738 8466-8466/com.your.app I/Choreographer: Skipped <number> frames!  The application may be doing too much work on its main thread.

Choreographer class is part of Android’s UI framework, and UI thread and “main” thread are synonyms for pretty much all practical purposes. So, Choreographer not only warns you about skipped frames, but it even suggests where the problem might come from.

By testing on low end devices and watching for these messages when you develop your app, you can ensure that it will work smoothly on an average Android device out there. If you do automated UI testing, then you can further add these checks to your regression suite. Fantastic!

Thread Starvation

Despite the suggestion in Choreographer’s messages, even if your app utilizes UI thread properly, it can still skip frames.

Imagine that there are many live threads (in addition to UI thread) which compete for CPU time. The system attempts to be fair and gives each one of them an opportunity to utilize CPU, but, as a result, UI thread doesn’t get enough CPU time to complete frame preparation in 16 ms. Consequently, some frames will be skipped. This situation is called “UI thread starvation”.

Back to my “benchmarking” app. It had trivially simple user interface, so UI thread didn’t need to work too hard to prepare a new frame. But, still, I expected UI thread to become starved, even if just a bit, when I started many concurrent threads. However, I didn’t see any Choreographer warnings in the logcat. It all felt too good to be true.

Skipped Frames Warning Threshold

After I hadn’t seen the expected warning messages, I decided to test Choreographer’s warnings functionality manually. To do that, I put UI thread to sleep for different time periods. Using this approach, I quickly found out that Choreographer doesn’t issue warnings for less than 30 skipped frames, or approximately 500 ms.

Let me repeat that again: if your app skips less than 30 frames at a time, Choreographer won’t notify you about skipped frames at all!

At 60 frames per second, 30 frames is half a second of real time. This is a considerable period of time and users will undoubtedly notice if your app becomes frozen for that long. Therefore, this threshold of 30 skipped frames before you see Choreographer’s warnings is clearly too high.

Changing Skipped Frames Warning Threshold

To understand where the aforementioned threshold comes from, I decided to review Choreographer’s source code. It didn’t take much time to find this statement :

private static final int SKIPPED_FRAME_WARNING_LIMIT = SystemProperties.getInt("debug.choreographer.skipwarning", 30);

Looks like 30 is the default limit, but it can be overridden with debug.choreographer.skipwarning system property. Luckily, this property starts with “debug”, which means that we can change it even on non-rooted devices.

So, I issued the following shell command:

adb shell setprop debug.choreographer.skipwarning 5

and, to make sure that it worked:

$ adb shell getprop debug.choreographer.skipwarning
5

After this change, Choreographer should’ve warned me when my app skipped more than five frames at a time. In theory. However, in practice, the threshold remained the same 30 skipped frames.

I didn’t dig much into why this change didn’t affect Choreographer, but I suspect it’s related to the fact that Zygote process is initialized on device boot. Therefore, by the time I change this property, Zygote already set the threshold to the default 30 frames. And since all new apps’ processes are forks of Zygote, I can’t affect any of them either. Reboot could help, but system properties that start with “debug” get reset on reboot, so this change gets reverted. Too bad.

It’s quite evident that the author of that code wanted this threshold to be configurable through this system property, but I couldn’t get it to work. Maybe I don’t understand something here?

Anyway, when something doesn’t work using the “officially recommended” approach in Android, we go brute force. I mean reflection, of course.

This piece of code did the trick:

private void reduceChoreographerSkippedFramesWarningThreshold() {
        if (BuildConfig.DEBUG) {
            Field field = null;
            try {
                field = Choreographer.class.getDeclaredField("SKIPPED_FRAME_WARNING_LIMIT");
                field.setAccessible(true);
                field.setInt(field, field.getModifiers() & ~Modifier.FINAL);
                field.set(null, 5);
            } catch (Throwable e) {
                Log.e(TAG, "failed to change choreographer's skipped frames threshold");
            }
        }
    }

Call this method from your Application’s onCreate(), and you’ll be alright. I added DEBUG guard because with Google’s ongoing war on reflection I wouldn’t dare to ship this code to end users (didn’t even bother to check whether this call is in the list of approved exceptions; not worth the risk, IMHO).

Conclusion

It was a bit of a shock when I found out that I lived in a bubble of a false sense of safety for years. I was sure that if I don’t see Choreographer’s warnings, then my apps don’t skip frames. Naive me.

After I reduced the threshold to one skipped frame, I discovered that all my apps, even the tutorials with simple UIs that I write for my courses, skip one-two frames here and there. Looks like it’s natural and unavoidable. Therefore, I recommend setting this threshold to at least three skipped frames to filter out the noise. I, personally, decided to use a threshold of five.

In my opinion, changing this threshold to get stricter (and, therefore, more reliable) monitoring of skipped frames is absolutely mandatory in all Android projects.

That’s all for today. Thanks for reading.

Check out my advanced Android development courses on Udemy


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK