Android Vitals - Tap Response Time 👉
Tags: android, vitals, performance, monitoring
Published: 2021-04-15 14:10:20

> Header image: *Alone Together* [by Romain Guy](https://www.flickr.com/photos/romainguy/10982291363/).


Android users expect apps to respond to their actions within a **short time window**.

> ### 💡 Did you know?
> UX research teaches us that a response time shorter than **100ms** feels **immediate**, and a response time beyond **1s** makes users **lose focus**. When the response time gets closer to 10 seconds, users simply abandon their task ([source](https://web.dev/rail/#focus-on-the-user)).

# 👉📱

Measuring **user action response times** is critical to ensure a **good user experience**. **Taps** are the most common action apps must respond to. Can we measure *Tap Response Time*?

> ### 🎓 Tap Response Time
> The *Tap Response Time* is the time from when the user is **done pressing** a button to when the app has **visibly reacted** to the tap.
> More precisely, it's the time from when the finger leaves the touch screen to when the display has rendered a frame with a visible reaction to that tap (e.g. the start of a navigation animation). The *Tap Response Time* does not include any animation time.

# Naive Tap Response Time

I opened the [Navigation Advanced Sample](https://github.com/android/architecture-components-samples/tree/master/NavigationAdvancedSample) project and added a call to [measureTimeMillis()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.system/measure-time-millis.html) to measure the *Tap Response Time* when tapping on the [about button](https://github.com/android/architecture-components-samples/blob/main/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/homescreen/Title.kt#L37-L39).

```kotlin
aboutButton.setOnClickListener {
  val tapResponseTimeMs = measureTimeMillis {
    findNavController().navigate(R.id.action_title_to_about)
  }
  PerfAnalytics.logTapResponseTime(tapResponseTimeMs)
}
```

Simple enough! However this approach presents several drawbacks:

* ⌛️ It can return a **negative time**.
* 📈 It **doesn't scale** with the codebase size.
* 👉 It doesn't account for the time from when the **finger leaves the touch screen** to when the click listener is called.
* 📱 It doesn't account for the time from when we're done calling [NavController.navigate()](https://developer.android.com/reference/androidx/navigation/NavController#navigate(int)) to when the **display has rendered a frame** with the new screen visible.

## ⌛️ Negative time

[measureTimeMillis()](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.system/measure-time-millis.html) calls `System.currentTimeMillis()` whihch can be set by the user or the phone network, so the time may jump backwards or forwards unpredictably. Elapsed time measurements should not use `System.currentTimeMillis()` (learn more: [Android Vitals - What time is it?
](https://dev.to/pyricau/android-vitals-what-time-is-it-2oih)).

## 📈 Large codebases

Adding measuring code to every single meaningful click listener is a daunting task. We need a solution that scales with the codebase size, which means we need **central hooks** to detect when meaningful actions are triggered by taps.

## 👉 Touch pipeline

When a finger leaves the touch screen, the following happens:

- The `system_server` process receives the information from the touch screen and determines which window should receive a `MotionEvent.UP` touch event.

> ### 💡 Did you know?
> Every window is associated with an input event [socket pair](https://www.gnu.org/software/libc/manual/html_node/Socket-Pairs.html): the first socket is owned by `system_server` to send input events. That first socket is paired with a second socket owned by the app that created the window, to receive input events.

- The `system_server` process sends the touch event to the input event socket for the targeted window.
- The app receives the touch event on its listening socket, stores it in a queue ([ViewRootImpl.QueuedInputEvent](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewRootImpl.java;l=7866-7878;drc=344f4d71075b98ee15696ef81484f03bab1848cb)) and schedules a Choreographer frame to consume input events.

> ### 💡 Did you know?
> The `system_server` process detects when an input event stays in the queue for more than 5 seconds, and that's when it knows it should show an Application Not Responding (ANR) dialog.

- When the Choreographer frame triggers, the touch event is dispatched to the root view of a window, which then dispatches it through its view hierarchy.
- The tapped view receives the `MotionEvent.UP` touch event and **posts a click listener callback** ([source](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/View.java;l=15724?q=performClickInternal)). *This lets other visual state of the view update before click actions start.*
- Then finally when the main thread runs that posted callback the view click listener is called.

As you can see, quite a lot happens from when the finger leaves the touch screen to when the click listener is called. Every motion event includes the time at which the event occurred ([MotionEvent.getEventTime()](https://developer.android.com/reference/android/view/MotionEvent#getEventTime())). If we could get access to that `MotionEvent.UP` event leading to a click we could measure the true start of the Tap Response Time.

## 📱 Traversal and rendering

What does this do?

```kotlin
findNavController().navigate(R.id.action_title_to_about)
```

- In most apps, the above code starts a fragment transaction. That transaction may be immediate (`commitNow()`) or posted (`commit()`).
- When the transaction execute, the view hierarchy is updated and a layout traversal is scheduled.
- When the layout traversal executes, a new frame is drawn into a surface.
- It is then composited with frames from other windows and sent to the display. To learn more, watch this talk from Romain Guy and Chet Haase: [Drawn out: How Android renders](https://youtu.be/zdQRIYOST64).

Ideally we'd like to know exactly when a change to the view hierarchy becomes actually visible on the display. Unfortunately, as far as I know there's no Java API for that, so we'll have to get creative.

# 🤔 What now?

We've seen that the naive approach to measuring *Tap Response Time* isn't accurate and doesn't scale. We need to build something better!

The good news is, I've implemented exactly that, on top of Jetpack Navigation. ~~The bad news is, you'll have to wait for me to publish Part 2!~~. The even better news is, you can read about it in Part 2: [Tap Response Time: Jetpack Navigation 🗺](https://dev.to/pyricau/tap-response-time-jetpack-navigation-4738).

> ⚖️ This work is licensed under a [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/).