Your Android Application Silently Skips Frames

By | 2019-10-24T15:11:31+00:00 October 12th, 2019|Android development|4 Comments

Last week I’ve been doing some benchmarking of multithreaded code on my old Galaxy S4 (2000 threads, 3000 lock acquisitions). 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).

Reflective Access Denied

Unfortunately, as I suspected, reflective access to this field seems to be forbidden when your app targets higher versions of Android. As reported by one of the readers, if you use the above code, you might see an error similar to this:

Accessing hidden field Landroid/view/Choreographer;->SKIPPED_FRAME_WARNING_LIMIT:I (greylist-max-o, reflection, denied)

If that’s the case, then the only workaround I can think of is to lower the target API of your app for debug builds. Pretty sure not all projects will be able to do that, though.

Meanwhile, I submitted this request to open this API to Google’s issue tracker (issues requesting reflective access to APIs are hidden, so you won’t be able to vote for it):

[FEATURE/USE CASE]
Choreographer class has an internal private field called SKIPPED_FRAME_WARNING_LIMIT (https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/Choreographer.java#146). This field controls the number of skipped frames at which developers will start seeing warnings in logcat.

As per the commit message that introduced this functoinality into AOSP (https://github.com/aosp-mirror/platform_frameworks_base/commit/4fdf9c6e2a177845bb4cc20f69b83555de209144), SKIPPED_FRAME_WARNING_LIMIT was intended to be modified through “debug.choreographer.skipwarning” system property (see the source code).

However, in practice, this method doesn’t work. I suspect that this field is initialized in Zygote and then all new processes inherit it with a default value, such that changes to system properties can’t affect its value. And since “debug” system properties are reset on reboot, rebooting the device doesn’t help either.

[NON-SDK INTERFACES USED]
Accessing hidden field Landroid/view/Choreographer;->SKIPPED_FRAME_WARNING_LIMIT:I (greylist-max-o, reflection, denied)

[ALTERNATIVES CONSIDERED]
This is a “passive”, always-on watchdog for skipped frames. As far as I know, there are no alternatives to this functionality.
Systrace and gfxinfo, which also have functionality related to skipped frames, have different use cases and aren’t “passive” and “always-on”.

Let’s hope Google engineers will understand the importance of this feature and open reflective access to this field.

Edit: looks like there is a way to bypass the protection of APIs from reflective access with “double reflection”. Read Ligol’s comment to this post for details.

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.

If you liked this post, then you'll surely like my courses

Subscribe for new posts!

4 Comments

  1. Luca Nicoletti October 18, 2019 at 11:06 am - Reply

    Can you link the issue?

  2. Ligol October 21, 2019 at 8:04 am - Reply

    You could use double reflection to make it work on latest android version without having to wait for the issue to be resolved by Google.
    https://stackoverflow.com/questions/55970137/bypass-androids-hidden-api-restrictions/55970138#55970138
    It’s not the most elegant solution but as you said, sometime we need to go bruteforce and if it stay only for debug it’s not a big issue.

    • Vasiliy October 24, 2019 at 3:01 pm - Reply

      Interesting approach, thanks for letting us know! I added this infromation to the post.

Leave A Comment