Android Vitals - What time is it?

pyricau

Py ⚔

Posted on July 13, 2020

Android Vitals - What time is it?

Note: the pretty header photo is from Romain Guy.

Yesterday I had an idea:

I received enthusiastic replies and decided to just start writing. This blog series will be focused on stability and performance monitoring of Android apps in production. It is entitled Android Vitals because it closely relates to Google's own Android vitals:

Android vitals is an initiative by Google to improve the stability and performance of Android devices. When an opted-in user runs your app, their Android device logs various metrics, including data about app stability, app startup time, battery usage, render time, and permission denials.

If you have questions or suggestions for future blogs, don't hesitate to reach out on Twitter!

To warm up, I'll start with a simple question:

What time is it?

To track performance, we need to measure time intervals, i.e. the difference between two points in time. The JDK provides us with 2 ways to get the current time:

// Milliseconds since Unix epoch (00:00:00 UTC on 1 January 1970)
System.currentTimeMillis()
// Nanoseconds since the VM started.
System.nanoTime()

Android provides a SystemClock class which adds a few more:

// (API 29) Clock that starts at Unix epoch.
// Synchronized using the device's location provider.
SystemClock.currentGnssTimeClock()
// Milliseconds running in the current thread.
SystemClock.currentThreadTimeMillis()
// Milliseconds since boot, including time spent in sleep.
SystemClock.elapsedRealtime()
// Nanoseconds since boot, including time spent in sleep.
SystemClock.elapsedRealtimeNanos()
// Milliseconds since boot, not counting time spent in deep sleep.
SystemClock.uptimeMillis()

Which one should we pick? The SystemClock javadoc helps answer that question:

  • System#currentTimeMillis can be set by the user or the phone network, so the time may jump backwards or forwards unpredictably. Interval or elapsed time measurements should use a different clock.
  • SystemClock#uptimeMillis stops when the system enters deep sleep. This is the basis for most interval timing such as Thread#sleep(long), Object#wait(long), and System#nanoTime. This clock is suitable for interval timing when the interval does not span device sleep.
  • SystemClock#elapsedRealtime and SystemClock#elapsedRealtimeNanos include deep sleep. This clock is the recommended basis for general purpose interval timing.

The performance of an app has no influence over what happens in deep sleep, so our best options are SystemClock.uptimeMillis() and System.nanoTime()

uptimeMillis() vs nanoTime()

System.nanoTime() is more precise than uptimeMillis(), but that's only useful for micro-benchmarks. When tracking performance in production, we need millisecond resolution.

Let's compare their performance impact. I cloned the Android Benchmark Samples repository and added the following test:

@LargeTest
@RunWith(AndroidJUnit4::class)
class TimingBenchmark {
    @get:Rule
    val benchmarkRule = BenchmarkRule()

    @Test
    fun nanoTime() {
        benchmarkRule.measureRepeated {
            System.nanoTime()
        }
    }

    @Test
    fun uptimeMillis() {
        benchmarkRule.measureRepeated {
            SystemClock.uptimeMillis()
        }
    }
}

Results on a Pixel 3 running Android 10:

  • System.nanoTime() median time: 208 ns
  • SystemClock.uptimeMillis() median time: 116 ns

SystemClock.uptimeMillis() is almost twice as fast! While that difference should not have any meaningful impact on an app, can we figure out why it's much faster?

uptimeMillis() implementation

SystemClock.uptimeMillis() is implemented as a native method annotated with @CriticalNative. CriticalNative provides faster JNI transitions for methods that contain no objects.

public final class SystemClock {
    @CriticalNative
    native public static long uptimeMillis();
}

(source)

The native implementation is in SystemClock.cpp:

int64_t uptimeMillis()
{
    int64_t when = systemTime(SYSTEM_TIME_MONOTONIC);
    return (int64_t) nanoseconds_to_milliseconds(when);
}

(source)

systemTime() is defined in Timers.cpp:

nsecs_t systemTime(int clock) {
    static constexpr clockid_t clocks[] = {
        CLOCK_REALTIME,
        CLOCK_MONOTONIC,
        CLOCK_PROCESS_CPUTIME_ID,
        CLOCK_THREAD_CPUTIME_ID,
        CLOCK_BOOTTIME
    };
    timespec t = {};
    clock_gettime(clocks[clock], &t);
    return nsecs_t(t.tv_sec)*1000000000LL + t.tv_nsec;
}

(source)

nanoTime() implementation

System.nanoTime() is also implemented as a native method annotated with @CriticalNative.

public final class System {
    @CriticalNative
    public static native long nanoTime();
}

(source)

The native implementation is in System.c:

static jlong System_nanoTime() {
  struct timespec now;
  clock_gettime(CLOCK_MONOTONIC, &now);
  return now.tv_sec * 1000000000LL + now.tv_nsec;
}

(source)

The two implementations are actually very similar, both call clock_gettime().

Turns out, @CriticalNative was only recently added to System.nanoTime(), which explains why it was slower!

Conclusion

When tracking performance in production apps:

  • A millisecond resolution is enough for most use cases.
  • To measure time intervals, use either SystemClock.uptimeMillis() or System.nanoTime(). The latter is slower on older Android versions but that doesn't matter here.
  • I prefer SystemClock.uptimeMillis() as I relate to milliseconds more easily.
    • 100 ms is the limit where humans stop feeling that they are directly manipulating objects in the UI (ie having an "intuitive" experience), and instead start feeling that they are ordering a computer to do the action for them and then waiting for an answer (source)
    • It's easy to remember that 100 ms is 1/10th of a second. I don't have the same quick frame of reference for nanoseconds, I have to remember that 1 ms = 1,000,000 ns and then do math.
  • SystemClock is not in the JDK, so if you're writing portable code then System.nanoTime() will do just fine.
💖 💪 🙅 🚩
pyricau
Py ⚔

Posted on July 13, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related