
Вчера у меня возникла идея:
Кому-нибудь интересна серия статей, посвященная мониторингу производительности Android в эксплуатационной среде?
Не что-то типа "настраиваем Firebase Performance Monitoring" (что меня не особо воодушевляет), а скорее "как работает Android".
“Я написал код, который определяет, когда происходит первый onDraw в приложении, но он не работает на API 25. Мне понадобилось какое-то время, чтобы понять причину!”
На что я получил положительные отзывы и сразу решил начать писать. Эта серия статей будет посвящена мониторингу производительности и стабильности приложений Android в эксплуатации. Она озаглавлена Android Vitals поскольку она тесно связана с Android vitals от самих Google:
Android vitals является инициативой Google, нацеленной на повышение производительности и стабильности Android-устройств. Когда пользователь, разрешивший сбор данных, запускает ваше приложение, его Android-устройство регистрирует различные метрики, включая данные о стабильности приложения, времени его запуска, использовании батареи, времени рендеринга и отказах в разрешениях.
Если у вас есть вопросы или идеи для будущих статей, не стесняйтесь писать мне в Twitter!
Для разогрева начну с простого вопроса:
Сколько времени?
Чтобы отслеживать производительность, нам нужно измерять временные интервалы, то есть разницу между двумя моментами времени. JDK предоставляет нам 2 способа получить текущее время:
// Миллисекунды с “эпохи Unix” (00:00:00 UTC 1 января 1970 г.)
System.currentTimeMillis()
// Наносекунды с момента запуска виртуальной машины.
System.nanoTime()
Android предоставляет класс SystemClock, который добавляет еще несколько вариантов:
// (API 29) Таймер, который ведет отсчет с “эпохи Unix”.
// Синхронизируется с помощью провайдера местоположения устройства.
SystemClock.currentGnssTimeClock()
// Миллисекунды работы в текущем потоке.
SystemClock.currentThreadTimeMillis()
// Миллисекунды с момента загрузки, включая время, проведенное в спящем режиме.
SystemClock.elapsedRealtime()
// Наносекунды с момента загрузки, включая время, проведенное в спящем режиме.
SystemClock.elapsedRealtimeNanos()
// Миллисекунды с момента загрузки, не считая времени, проведенного в глубоком сне.
SystemClock.uptimeMillis()
Что из этого многообразия выбрать? Ответить на этот вопрос может помочь javadoc по SystemClock:
// (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()
System#currentTimeMillis может быть установлен пользователем или телефонной сетью, поэтому время может непредсказуемо прыгать назад или вперед. При измерениях интервалов или прошедшего времени следует использовать другой таймер.
SystemClock#uptimeMillis останавливается, когда система переходит в режим глубокого сна. Он является основой для измерений временных интервалов при использовании Thread#sleep(long), Object#wait(long) (это касается и System#nanoTime). Этот таймер подходит для измерения временных интервалов, когда интервал не захватывает спящий режим устройства.
SystemClock#elapsedRealtime и SystemClock#elapsedRealtimeNanos захватывают и глубокий сон. Эти таймеры являются рекомендуемой универсальной основой для измерения временных интервалов.
Работа приложения не влияет на то, что происходит в глубоком сне, поэтому наши лучшие варианты - SystemClock.uptimeMillis() и System.nanoTime().
uptimeMillis() или nanoTime()?
System.nanoTime() более точен, чем uptimeMillis(), но полезно это только для микробенчмарков. При отслеживании производительности в эксплуатации нам достаточно разрешения в миллисекундах.
Давайте сравним их влияние на производительность. Я клонировал репозиторий Android Benchmark Samples и добавил следующий тест:
@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()
}
}
}
Результаты на Pixel 3 под Android 10:
System.nanoTime()среднее время: 208 нс
SystemClock.uptimeMillis()среднее время: 116 нс
SystemClock.uptimeMillis() почти в два раза быстрее! Хотя эта разница не должна иметь сколько-нибудь значимого влияния на приложение, можем ли мы понять, почему он намного быстрее?
Реализация uptimeMillis()
SystemClock.uptimeMillis() реализована как нативный метод, аннотированный @CriticalNative. CriticalNative обеспечивает более быстрые JNI-преобразования для методов, не содержащих объектов.
public final class SystemClock {
@CriticalNative
native public static long uptimeMillis();
}
(источник)
Нативная реализация находится в SystemClock.cpp:
int64_t uptimeMillis()
{
int64_t when = systemTime(SYSTEM_TIME_MONOTONIC);
return (int64_t) nanoseconds_to_milliseconds(when);
}
(источник)
systemTime() определен в 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;
}
(источник)
Реализация nanoTime()
System.nanoTime() также реализован как нативный метод, аннотированный @CriticalNative.
public final class System {
@CriticalNative
public static native long nanoTime();
}
(источник)
Нативная реализация находится в System.c:
static jlong System_nanoTime() {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return now.tv_sec * 1000000000LL + now.tv_nsec;
}
(источник)
Две эти реализации на самом деле очень похожи, они обе вызывают clock_gettime().
Оказывается, @CriticalNative только недавно была добавлена в System.nanoTime(), что объясняет, почему он был медленнее!
Заключение
При отслеживании производительности во время работы приложения:
Разрешения в миллисекундах достаточно для большинства случаев.
Для измерения временных интервалов используйте
SystemClock.uptimeMillis()илиSystem.nanoTime(). Последний работает медленнее на старых версиях Android, но сейчас это не имеет особого значения.Я предпочитаю
SystemClock.uptimeMillis(), так как мне проще использовать миллисекунды.100 мс — это предел, при котором люди перестают чувствовать, что они напрямую манипулируют объектами в пользовательском интерфейсе (т.е. имеют «интуитивный» опыт), и вместо этого начинают чувствовать, что они приказывают компьютеру выполнить действие за них, а затем ждут ответа. (источник)
Легко запомнить, что 100 мс — это 1/10 секунды. У меня нет той же легкой напоминалки для наносекунд, я должен помнить, что 1 мс = 1 000 000 нс, а затем производить вычисления.
SystemClock отсутствует в JDK, поэтому, если вы стремитесь писать переносимый код, вам лучше использовать
System.nanoTime().
Материал подготовлен в рамках курса «Android Developer. Professional». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.