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