Android Vitals — Профилируем запуск приложения +1


Мои предыдущие статьи были посвящены мониторингу запуска Android-приложений в эксплуатационной среде. После того, как мы разобрались с метриками и сценариями, которые результируют в медленном запуске приложения, следующим шагом будет повышение производительности.

Чтобы понять, почему приложение медленно запускается, нам нужно его профилировать. Android Studio предоставляет несколько типов конфигураций записи профилирования:

  • Трассировка системных вызовов (systrace, perfetto): почти не влияет на время выполнения, отлично подходит для понимания того, как приложение взаимодействует с системой и процессорами, но не вызовы методов Java, которые происходят внутри виртуальной машины приложения.

  • Сэмплирование функций C/C++ (Simpleperf): не особо меня заинтересовал, приложения, с которыми я работаю, выполняют гораздо больше байт-кода, чем нативного. На Q+ он также должен с минимальными накладными расходами сэмплировать Java-стеки, но мне так и не удалось это реализовать.

  • Трассировка методов Java: захватывает все вызовы методов виртуальной машины, что приводит к таким большим накладным расходам, что результаты мало что могут показать.

  • Сэмплирование методов Java: меньше накладных расходов, чем при трассировке, но показывает вызовы методов Java, происходящие внутри виртуальной машины. При профилировании запуска приложения это мой предпочтительный вариант.

Как начать записи во время запуска приложения

В профилировщике Android Studio есть пользовательский интерфейс для начала трассировки через подключение к уже запущенному процессу, но нет очевидного способа начать запись во время запуска приложения.

Такая возможность все-таки существует, но она скрывается в конфигурации запуска вашего приложения: поставьте галочку возле “Start this recording on startup” на вкладке profiling .

Затем запустите приложение через Run > Profile app.

Профилирование релизных билдов

Android-разработчики в своей повседневной работе обычно используют дебажные билды, а дебажные билды зачастую включают в себя debug drawer, дополнительные библиотеки, такие как LeakCanary и т. д. Разработчикам следует профилировать релизные билды, а не дебажные, чтобы гарантировать, что они работают над актуальными проблемами, с которыми сталкиваются их клиенты.

К сожалению, релизные билды не подлежат отладке, поэтому профилировщик Android не может записывать трейсы релизных билдах.

Есть несколько вариантов решения этой проблемы.

1. Создание отлаживаемого релизного билда

Мы могли бы временно сделать наш релизный билд отлаживаемым или создать новый тип релизного билда только для профилирования.

android {
  buildTypes {
    release {
      debuggable true
      // ...
    }
  }
}

Библиотеки и код Android Framework часто ведут себя иначе, если APK-файл является отлаживаемым. ART отключает множество оптимизаций для подключения дебагера, что значительно и порой непредсказуемо влияет на производительность (смотрите этот доклад). Так что это решение не идеальное.

2. Профилирование на рутованном устройстве 

Рутованные устройства позволяют профилировщику Android Studio записывать трейсы на недебажных билдах.

Не рекомендую профилировать на эмуляторе - производительность каждого компонента системы будет отличаться (скорость процессора, размер кэша, дисковая производительность), “оптимизация” может даже замедлить работу, переключив работу на что-то, что медленнее телефоне. Если у вас нет рутованого устройства, вы можете создать эмулятор без Play Services, а затем запустить adb root.

3. Использование simpleperf на Android Q

Есть такой инструмент под названием simpleperf, который предположительно позволяет профилировать релизные билды на нерутованных устройства Q+, если у них есть специальный флаг в манифесте. В документации это называется profileableFromShell, в примере XML есть тег profileable с атрибутом android: shell, но в официальной документация по манифесту ничего про это не сказано.

<manifest ...>
    <application ...>
      <profileable android:shell="true" />
    </application>
</manifest>

Я просмотрел код парсинга манифеста на cs.android.com:

if (tagName.equals("profileable")) {
  sa = res.obtainAttributes(
    parser,
    R.styleable.AndroidManifestProfileable
  );
  if (sa.getBoolean(
    R.styleable.AndroidManifestProfileable_shell,
    false
  )) {
    ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_PROFILEABLE_BY_SHELL;
  }
}

Похоже, вы можете запустить профилирование из командной строки, если в манифесте есть <profileable android:shell = "true" /> (сам я не пробовал). Насколько я понимаю, команда Android Studio все еще работает над интеграцией этой возможности.

Профилирование загруженного APK

В Square наши релизы создаются в рамках CI. Как мы видели ранее, запуск приложения для профилирования из Android Studio предполагает проверку параметра в конфигурации запуска. Как же сделать это с загруженным APK?

Оказывается, это возможно, но скрыто в File > Profile or Debug APK. Это откроет новое окно с разархивированным APK, в котором вы можете настроить конфигурацию запуска и начать профилирование.

Профилировщик Android Studio все замедляет

К сожалению, когда я тестировал эти методы в реальном приложении, профилирование из Android Studio сильно замедляло запуск приложения (примерно в 10 раз) даже на последних версиях Android. Я не знаю, почему так происходит. Возможно, в этом виновато “расширенное профилирование”, которое, похоже, не может быть отключено. Нам нужно найти другой способ!

Профилирование из кода

Вместо профилирования из Android Studio мы можем запустить трассировку непосредственно из кода:

val tracesDirPath = TODO("path for trace directory")
val fileNameFormat = SimpleDateFormat(
  "yyyy-MM-dd_HH-mm-ss_SSS'.trace'",
  Locale.US
)
val fileName = fileNameFormat.format(Date())
val traceFilePath = tracesDirPath + fileName
// Save up to 50Mb data.
val maxBufferSize = 50 * 1000 * 1000
// Sample every 1000 microsecond (1ms)
val samplingIntervalUs = 1000
Debug.startMethodTracingSampling(
  traceFilePath, 
  maxBufferSize,
  samplingIntervalUs
)

// ...

Debug.stopMethodTracing()

Затем мы можем извлечь файл трассировки из устройства и загрузить его в Android Studio.

Когда начинать запись

Мы должны начать запись трейсов как можно раньше в жизненном цикле приложения. Как я уже говорил в Android Vitals — Разбираем холодный запуск, самый ранний код, который может запускаться при запуске приложения до Android P, - это ContentProvider, а на Android P+ - это AppComponentFactory.

До Android P / API < 28

class AppStartListener : ContentProvider() {
  override fun onCreate(): Boolean {
    Debug.startMethodTracingSampling(...)
    return false
  }
  // ...
}
<?xml version="1.0" encoding="utf-8"?>
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools">

  <application>
    <provider
        android:name=".AppStartListener"
        android:authorities="com.example.appstartlistener"
        android:exported="false" />
  </application>

</manifest>

К сожалению, мы не можем контролировать порядок, в котором создаются инстансы ContentProvider, поэтому мы можем не захватить часть раннего кода запуска.

Примечание: @anjalsaneen указал в комментариях, что при определении провайдера мы можем установить тег initOrder, и провайдер с наибольшим этим значением будет инициализироваться первым.

Android P+ / API 28+

<?xml version="1.0" encoding="utf-8"?>
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools">

  <application>
    <provider
        android:name=".AppStartListener"
        android:authorities="com.example.appstartlistener"
        android:exported="false" />
  </application>

</manifest>
<?xml version="1.0" encoding="utf-8"?>
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools">

  <application
    android:appComponentFactory=".MyAppComponentFactory"
    tools:replace="android:appComponentFactory"
    tools:targetApi="p">
  </application>

</manifest>

 Где хранить трейсы

val tracesDirPath = TODO("path for trace directory")
  • API <28: широковещательный приемник (broadcast receiver) имеет доступ к контексту, в котором мы можем вызвать Context.getDataDir() для сохранения трейсов в каталоге приложения.

  • API 28: AppComponentFactory.instantiateApplication() отвечает за создание нового инстанса приложения, поэтому контекст пока недоступен. Мы можем захардкодить путь к /sdcard/ напрямую, хотя для этого требуется разрешение WRITE_EXTERNAL_STORAGE.

Когда прекращать запись

В Android Vitals - первая отрисовка ????‍????, мы узнали, что холодный запуск заканчивается, когда полностью загружается первый кадр приложения. Мы можем использовать код из этой статьи, чтобы узнать, когда остановить трассировку методов:

class MyApp : Application() {

  override fun onCreate() {
    super.onCreate()

    var firstDraw = false
    val handler = Handler()

    registerActivityLifecycleCallbacks(
      object : ActivityLifecycleCallbacks {
      override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
      ) {
        if (firstDraw) return
        val window = activity.window
        window.onDecorViewReady {
          window.decorView.onNextDraw {
            if (firstDraw) return
            firstDraw = true
            handler.postAtFrontOfQueue {
              Debug.stopMethodTracing()
            }
          }
        }
      }
    })
  }
}

Мы также можем производить запись в течение фиксированного периода времени, превышающего запуск приложения, например 5 секунд:

Handler(Looper.getMainLooper()).postDelayed({
  Debug.stopMethodTracing()
}, 5000)

Профилирование с помощью Nanoscope

Еще один вариант профилирования во время запуска приложения - uber/nanoscope. Это образ Android со встроенной трассировкой с минимальными накладными расходами. Что само по себе здорово, но имеет несколько ограничений:

  • Отслеживает только основной поток (main thread).

  • Большое приложение скорее всего переполнит буфер трассировки в памяти.

@lelandtakamine @Piwai @kurtisnelson @speekha Круто - с нетерпением жду поста.

Да - Nanoscope пока отслеживает только main thread, но мы в @perf_dev работаем над поддержкой многопоточности. Следите за обновлениями.

Этапы запуска приложения

После того, как у нас на руках будет трассировка запуска, мы можем начать выяснять, на что было потрачено время. Скорее всего это будет что-то из этих трех категорий:

  1. ActivityThread.handlingBindApplication() отвечает за работу по запуску перед созданием activity. Если он работает медленно, то нам, вероятно, нужно оптимизировать Application.onCreate().

  2. TransactionExecutor.execute() отвечает за создание и возобновление activity, что включает инфлейтинг иерархии представлений.

  3. ViewRootImpl.performTraversals() - это то место, где фреймворк выполняет первое измерение, лейаут и отрисовку. Если это происходит медленно, то причиной этому может быть слишком сложная иерархией представлений, или представления с кастомной графикой, которую необходимо оптимизировать.

Если вы заметили, что служба запускается до первого view traversal, возможно, стоит отложить запуск этой службы, чтобы это произошло после view traversal.

Заключение

Выводов несколько:

  • Профилируйте релизные билды, чтобы сосредоточиться на актуальных проблемах.

  • Состояние профилирования приложения со старта на Android далеко от идеального. По сути, готового хорошего решения нет, но команда Jetpack Benchmark работает над этим.

  • Производите запись из кода, чтобы Android Studio не замедляла работу.

Огромное спасибо многим людям, которые помогали мне в Slack и Twitter: Kurt Nelson, Justin Wong, Leland Takamine, Yacine Rezgui, Raditya Gumay, Chris Craik, Mike Nakhimovich, Artem Chubaryan, Rahul Ravikumar, Yacine Rezgui, Eugen Pechanec, Louis CAD, Max Kovalev.


Перевод статьи подготовлен в преддверии старта нового потока курса Android Developer. Professional.




К сожалению, не доступен сервер mySQL