Android Vitals — Сколько времени?

от автора

Вчера у меня возникла идея:

Кому-нибудь интересна серия статей, посвященная мониторингу производительности 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.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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *