Вчера у меня возникла идея:
Кому-нибудь интересна серия статей, посвященная мониторингу производительности 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». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.