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

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

Комментарии (0)