Android Vitals - What time is it?
Py ⚔
Posted on July 13, 2020
Note: the pretty header photo is from Romain Guy.
Yesterday I had an idea:
Py ⚔@piwaiAnyone interested in a blog series on Android performance monitoring in production?
Not like "set up firebase perf monitoring" (it's kinda meh) but more "here's how Android works" twitter.com/Piwai/status/1…22:26 PM - 12 Jul 2020Py ⚔ @PiwaiI wrote code that detects when the first onDraw of an app happens, and it wasn't working on API 25. Took me a while to figure that one out!
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()
orSystem.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 thenSystem.nanoTime()
will do just fine.
Posted on July 13, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.