Отладка базовых профилей

В этом документе представлены рекомендации и шаги по устранению неполадок, которые помогут диагностировать проблемы и убедиться в правильной работе базовых профилей для достижения максимальной эффективности.

Проблемы сборки

Если вы скопировали пример базовых профилей из демонстрационного приложения Now in Android , вы можете столкнуться с ошибками тестирования во время выполнения задачи создания базового профиля, указывающими на невозможность запуска тестов на эмуляторе:

./gradlew assembleDemoRelease
Starting a Gradle Daemon (subsequent builds will be faster)
Calculating task graph as no configuration cache is available for tasks: assembleDemoRelease
Type-safe project accessors is an incubating feature.

> Task :benchmarks:pixel6Api33DemoNonMinifiedReleaseAndroidTest
Starting 14 tests on pixel6Api33

com.google.samples.apps.nowinandroid.foryou.ScrollForYouFeedBenchmark > scrollFeedCompilationNone[pixel6Api33] FAILED
        java.lang.AssertionError: ERRORS (not suppressed): EMULATOR
        WARNINGS (suppressed):
        ...

Сбои возникают из-за того, что Now in Android использует устройство, управляемое Gradle, для генерации базовых профилей. Эти сбои ожидаемы, поскольку обычно не следует запускать тесты производительности на эмуляторе. Однако, поскольку при генерации базовых профилей не собираются метрики производительности, для удобства можно запустить сбор базовых профилей на эмуляторах. Чтобы использовать базовые профили с эмулятором, выполните сборку и установку из командной строки и задайте аргумент для включения правил базовых профилей:

installDemoRelease -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile

В качестве альтернативы, вы можете создать пользовательскую конфигурацию запуска в Android Studio, чтобы включить базовые профили на эмуляторах, выбрав «Запуск» > «Редактировать конфигурации» :

Добавьте пользовательскую конфигурацию запуска для создания базовых профилей в Now in Android.
Рисунок 1. Добавление пользовательской конфигурации запуска для создания базовых профилей в Now в Android.

Проверьте установку и применение профиля.

Чтобы убедиться, что проверяемый вами APK-файл или пакет Android-приложения (AAB) относится к варианту сборки, включающему базовые профили, выполните следующие действия:

  1. В Android Studio выберите Build > Analyze APK .
  2. Откройте файл AAB или APK.
  3. Убедитесь, что файл baseline.prof существует:

    • Если вы проверяете AAB-файл, профиль находится по адресу /BUNDLE-METADATA/com.android.tools.build.profiles/baseline.prof .
    • Если вы просматриваете APK-файл, профиль находится по адресу /assets/dexopt/baseline.prof .

      Наличие этого файла — первый признак правильной конфигурации сборки. Если он отсутствует, это означает, что среда выполнения Android не получит никаких инструкций предварительной компиляции во время установки.

      Проверьте наличие базового профиля с помощью APK Analyzer в Android Studio.
      Рисунок 2. Проверка наличия базового профиля с помощью анализатора APK в Android Studio.

Базовые профили необходимо компилировать на устройстве, на котором запущено приложение. При установке сборок, не предназначенных для отладки, с помощью Android Studio или инструмента командной строки Gradle, компиляция на устройстве происходит автоматически. Если вы устанавливаете приложение из Google Play Store, базовые профили компилируются во время фоновых обновлений устройства, а не во время установки. При установке приложения с помощью других инструментов библиотека Jetpack ProfileInstaller отвечает за добавление профилей в очередь на компиляцию во время следующего фонового процесса оптимизации DEX.

В таких случаях, если вы хотите убедиться, что ваши базовые профили используются, вам может потребоваться принудительно скомпилировать их . ProfileVerifier позволяет запросить статус установки и компиляции профилей, как показано в следующем примере:

Котлин

private const val TAG = "MainActivity"

class MainActivity : ComponentActivity() {
  ...
  override fun onResume() {
    super.onResume()
    lifecycleScope.launch {
      logCompilationStatus()
    }
  }

  private suspend fun logCompilationStatus() {
     withContext(Dispatchers.IO) {
        val status = ProfileVerifier.getCompilationStatusAsync().await()
        when (status.profileInstallResultCode) {
            RESULT_CODE_NO_PROFILE ->
                Log.d(TAG, "ProfileInstaller: Baseline Profile not found")
            RESULT_CODE_COMPILED_WITH_PROFILE ->
                Log.d(TAG, "ProfileInstaller: Compiled with profile")
            RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION ->
                Log.d(TAG, "ProfileInstaller: Enqueued for compilation")
            RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING ->
                Log.d(TAG, "ProfileInstaller: App was installed through Play store")
            RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST ->
                Log.d(TAG, "ProfileInstaller: PackageName not found")
            RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ ->
                Log.d(TAG, "ProfileInstaller: Cache file exists but cannot be read")
            RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE ->
                Log.d(TAG, "ProfileInstaller: Can't write cache file")
            RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION ->
                Log.d(TAG, "ProfileInstaller: Enqueued for compilation")
            else ->
                Log.d(TAG, "ProfileInstaller: Profile not compiled or enqueued")
        }
    }
}

Java

public class MainActivity extends ComponentActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onResume() {
        super.onResume();

        logCompilationStatus();
    }

    private void logCompilationStatus() {
         ListeningExecutorService service = MoreExecutors.listeningDecorator(
                Executors.newSingleThreadExecutor());
        ListenableFuture<ProfileVerifier.CompilationStatus> future =
                ProfileVerifier.getCompilationStatusAsync();
        Futures.addCallback(future, new FutureCallback<>() {
            @Override
            public void onSuccess(CompilationStatus result) {
                int resultCode = result.getProfileInstallResultCode();
                if (resultCode == RESULT_CODE_NO_PROFILE) {
                    Log.d(TAG, "ProfileInstaller: Baseline Profile not found");
                } else if (resultCode == RESULT_CODE_COMPILED_WITH_PROFILE) {
                    Log.d(TAG, "ProfileInstaller: Compiled with profile");
                } else if (resultCode == RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION) {
                    Log.d(TAG, "ProfileInstaller: Enqueued for compilation");
                } else if (resultCode == RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING) {
                    Log.d(TAG, "ProfileInstaller: App was installed through Play store");
                } else if (resultCode == RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST) {
                    Log.d(TAG, "ProfileInstaller: PackageName not found");
                } else if (resultCode == RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ) {
                    Log.d(TAG, "ProfileInstaller: Cache file exists but cannot be read");
                } else if (resultCode
                        == RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE) {
                    Log.d(TAG, "ProfileInstaller: Can't write cache file");
                } else if (resultCode == RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION) {
                    Log.d(TAG, "ProfileInstaller: Enqueued for compilation");
                } else {
                    Log.d(TAG, "ProfileInstaller: Profile not compiled or enqueued");
                }
            }

            @Override
            public void onFailure(Throwable t) {
                Log.d(TAG,
                        "ProfileInstaller: Error getting installation status: " + t.getMessage());
            }
        }, service);
    }
}

Приведенные ниже коды результатов содержат подсказки о причинах некоторых проблем:

RESULT_CODE_COMPILED_WITH_PROFILE
Профиль установлен, скомпилирован и используется при каждом запуске приложения. Это тот результат, который вы хотите увидеть.
RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED
В запускаемом APK-файле профиль не найден. Если вы видите эту ошибку, убедитесь, что используете вариант сборки, включающий базовые профили, и что APK-файл содержит профиль.
RESULT_CODE_NO_PROFILE
При установке этого приложения через App Store или менеджер пакетов профиль для него не был установлен. Основная причина этой ошибки заключается в том, что установщик профилей не запустился из-за отключения ProfileInstallerInitializer . Обратите внимание, что при возникновении этой ошибки в APK-файле приложения всё ещё был обнаружен встроенный профиль. Если встроенный профиль не найден, возвращается код ошибки RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED .
RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION
Профиль находится в APK или AAB и ставится в очередь на компиляцию. Когда профиль устанавливается с помощью ProfileInstaller , он ставится в очередь на компиляцию при следующем запуске фоновой оптимизации DEX системой. Профиль не активен до завершения компиляции. Не пытайтесь тестировать производительность базовых профилей до завершения компиляции. Возможно, вам потребуется принудительно скомпилировать базовые профили . Эта ошибка не возникнет при установке приложения из Play Store или менеджера пакетов на устройствах под управлением Android 9 (API 28) и выше, поскольку компиляция выполняется во время установки.
RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING
Установлен несоответствующий профиль, и приложение было скомпилировано с его использованием. Это результат установки через магазин Google Play или менеджер пакетов. Обратите внимание, что этот результат отличается от RESULT_CODE_COMPILED_WITH_PROFILE , поскольку несоответствующий профиль компилирует только те методы, которые по-прежнему являются общими для профиля и приложения. Фактически, размер профиля меньше, чем ожидалось, и будет скомпилировано меньше методов, чем было включено в базовый профиль.
RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE
ProfileVerifier не может записать файл кэша результатов проверки. Это может произойти из-за проблем с правами доступа к папке приложения или из-за недостатка свободного места на диске устройства.
RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION
ProfileVerifier is running on an unsupported API version of Android. ProfileVerifier поддерживает только Android 9 (уровень API 28) и выше.
RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST
При запросе PackageManager пакета приложения возникает исключение PackageManager.NameNotFoundException . Это должно происходить крайне редко. Попробуйте удалить приложение и переустановить все компоненты.
RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ
Существует файл кэша предыдущих результатов проверки, но его невозможно прочитать. Такое случается крайне редко. Попробуйте удалить приложение и переустановить все компоненты.

Используйте ProfileVerifier в рабочей среде.

В производственной среде ProfileVerifier можно использовать совместно с библиотеками для создания аналитических отчетов, такими как Google Analytics for Firebase , для генерации аналитических событий, указывающих на статус профиля. Например, это позволит быстро получить уведомление, если будет выпущена новая версия приложения, в которой отсутствуют базовые профили.

Принудительное составление базовых профилей

Если статус компиляции ваших базовых профилей равен RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION , вы можете принудительно выполнить немедленную компиляцию с помощью adb :

adb shell cmd package compile -r bg-dexopt PACKAGE_NAME

Проверка состояния компиляции базового профиля без ProfileVerifier

Если вы не используете ProfileVerifier , вы можете проверить состояние компиляции с помощью adb , хотя это не даёт такой глубокой информации, как ProfileVerifier :

adb shell dumpsys package dexopt | grep -A 2 PACKAGE_NAME

Использование adb выдаёт результат, похожий на следующий:

  [com.google.samples.apps.nowinandroid.demo]
    path: /data/app/~~dzJiGMKvp22vi2SsvfjkrQ==/com.google.samples.apps.nowinandroid.demo-7FR1sdJ8ZTy7eCLwAnn0Vg==/base.apk
      arm64: [status=speed-profile] [reason=bg-dexopt] [primary-abi]
        [location is /data/app/~~dzJiGMKvp22vi2SsvfjkrQ==/com.google.samples.apps.nowinandroid.demo-7FR1sdJ8ZTy7eCLwAnn0Vg==/oat/arm64/base.odex]

Значение статуса указывает на состояние компиляции профиля и принимает одно из следующих значений:

Статус компиляции Значение
speed‑profile Составленный профиль существует и используется.
verify Скомпилированный профиль отсутствует.

Статус verify не означает, что APK или AAB не содержат профиль, поскольку они могут быть поставлены в очередь на компиляцию следующей фоновой задачей оптимизации DEX.

Значение параметра reason указывает, что запускает компиляцию профиля, и принимает одно из следующих значений:

Причина Значение
install‑dm Базовый профиль был составлен вручную или с помощью Google Play при установке приложения.
bg‑dexopt Профиль был создан, когда ваше устройство находилось в режиме ожидания. Это может быть базовый профиль или профиль, собранный во время использования приложений.
cmdline Компиляция была запущена с помощью adb. Это может быть базовый профиль или профиль, собранный во время использования приложения.

Проверьте соответствие приложения Startup Profile данным DEX и r8.json

Правила профиля запуска используются R8 на этапе сборки для оптимизации структуры классов в ваших DEX-файлах. Эта оптимизация на этапе сборки отличается от использования базовых профилей ( baseline.prof ), поскольку они упакованы в APK или AAB для компиляции ART на устройстве. Поскольку правила профиля запуска применяются в процессе сборки, отдельного файла startup.prof в вашем APK или AAB для анализа не существует. Эффект профилей запуска виден в структуре DEX-файла.

Проверьте конфигурацию DEX с помощью r8.json (рекомендуется для AGP 8.8 или выше).

Для проектов, использующих Android Gradle Plugin (AGP) версии 8.8 или выше, вы можете проверить, был ли применен профиль запуска, просмотрев сгенерированный файл r8.json . Этот файл входит в состав вашего AAB.

  1. Откройте свой AAB-архив и найдите файл r8.json .
  2. Найдите в файле массив dexFiles , в котором перечислены сгенерированные DEX-файлы.
  3. Найдите объект dexFiles , содержащий пару ключ-значение "startup": true . Это явно указывает на то, что правила профиля запуска были применены для оптимизации структуры данного DEX-файла.

    "dexFiles": [
     {
       "checksum": "...",
       "startup": true // This flag confirms profile application to this DEX file
     },
     // ... other DEX files
    ]
    

Проверьте конфигурацию DEX для всех версий AGP.

Если вы используете версию AGP ниже 8.8, проверка файлов DEX — основной способ убедиться в правильности применения профиля запуска. Этот метод также можно использовать, если вы используете AGP 8.8 или выше и хотите вручную проверить структуру DEX. Например, если вы не видите ожидаемого улучшения производительности. Чтобы проверить структуру DEX, выполните следующие действия:

  1. Откройте файл AAB или APK, используя команду Build > Analyze APK в Android Studio.
  2. Перейдите к первому DEX-файлу. Например, classes.dex .
  3. Проверьте содержимое этого DEX-файла. Вы должны убедиться, что критически важные классы и методы, определенные в файле профиля запуска ( startup-prof.txt ), присутствуют в этом основном DEX-файле. Успешное приложение означает, что эти критически важные для запуска компоненты имеют приоритет для более быстрой загрузки.

Проблемы с производительностью

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

Правильно оценивайте показатели стартапа.

Ваши базовые профили будут более эффективны, если ваши стартовые показатели будут четко определены. Двумя ключевыми показателями являются время до первого отображения (TTID) и время до полного отображения (TTFD) .

TTID — это момент, когда приложение отрисовывает свой первый кадр. Важно, чтобы этот момент был как можно короче, поскольку отображение чего-либо показывает пользователю, что приложение запущено. Можно даже отобразить неопределенный индикатор прогресса, чтобы показать, что приложение реагирует на действия пользователя.

TTFD — это момент, когда с приложением действительно можно взаимодействовать. Важно, чтобы этот момент был как можно короче, чтобы избежать разочарования пользователей. Если вы правильно укажете TTFD, вы сообщите системе, что код, выполняемый на пути к TTFD, является частью процесса запуска приложения. В результате система с большей вероятностью поместит этот код в профиль.

Чтобы ваше приложение работало быстро, старайтесь поддерживать значения TTID и TTFD на минимально возможном уровне.

Система способна определять TTID, отображать его в Logcat и сообщать о нем в рамках тестов производительности при запуске. Однако система не может определить TTFD, и ответственность за сообщение о достижении полностью отрисованного интерактивного состояния лежит на самом приложении. Это можно сделать, вызвав метод reportFullyDrawn() или ReportDrawn если вы используете Jetpack Compose. Если у вас есть несколько фоновых задач, которые должны завершиться до того, как приложение будет считаться полностью отрисованным, вы можете использовать FullyDrawnReporter , как описано в разделе «Повышение точности синхронизации при запуске» .

Профили библиотеки и пользовательские профили

При оценке влияния профилей производительности бывает сложно отделить преимущества профилей вашего приложения от профилей, предоставляемых библиотеками, такими как библиотеки Jetpack. При сборке APK-файла плагин Android Gradle добавляет все профили из зависимостей библиотек, а также ваш пользовательский профиль. Это хорошо для оптимизации общей производительности и рекомендуется для релизных сборок. Однако это затрудняет измерение того, насколько увеличивается производительность благодаря вашему пользовательскому профилю.

Быстрый способ вручную увидеть дополнительную оптимизацию, обеспечиваемую вашим пользовательским профилем, — это удалить его и запустить тесты производительности. Затем замените его и снова запустите тесты. Сравнение результатов покажет вам оптимизацию, обеспечиваемую только профилями из библиотеки, а также оптимизацию, обеспечиваемую профилями из библиотеки в сочетании с вашим пользовательским профилем.

Автоматизированный способ сравнения профилей — создание нового варианта сборки, содержащего только библиотечные профили, а не ваш пользовательский профиль. Сравните результаты тестов этого варианта с результатами релизного варианта, содержащего как библиотечные профили, так и ваши пользовательские профили. В следующем примере показано, как настроить вариант, включающий только библиотечные профили. Добавьте новый вариант с именем releaseWithoutCustomProfile в ваш модуль потребителя профилей, который обычно является модулем вашего приложения:

Котлин

android {
  ...
  buildTypes {
    ...
    // Release build with only library profiles.
    create("releaseWithoutCustomProfile") {
      initWith(release)
    }
    ...
  }
  ...
}
...
dependencies {
  ...
  // Remove the baselineProfile dependency.
  // baselineProfile(project(":baselineprofile"))
}

baselineProfile {
  variants {
    create("release") {
      from(project(":baselineprofile"))
    }
  }
}

Классный

android {
  ...
  buildTypes {
    ...
    // Release build with only library profiles.
    releaseWithoutCustomProfile {
      initWith(release)
    }
    ...
  }
  ...
}
...
dependencies {
  ...
  // Remove the baselineProfile dependency.
  // baselineProfile ':baselineprofile"'
}

baselineProfile {
  variants {
    release {
      from(project(":baselineprofile"))
    }
  }
}

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

Также необходимо добавить новый вариант в модуль генератора профилей. В этом примере модуль производителя называется :baselineprofile .

Котлин

android {
  ...
    buildTypes {
      ...
      // Release build with only library profiles.
      create("releaseWithoutCustomProfile") {}
      ...
    }
  ...
}

Классный

android {
  ...
    buildTypes {
      ...
      // Release build with only library profiles.
      releaseWithoutCustomProfile {}
      ...
    }
  ...
}

При запуске теста производительности из Android Studio выберите вариант releaseWithoutCustomProfile , чтобы измерить производительность только с использованием профилей из библиотеки, или выберите вариант release , чтобы измерить производительность с использованием профилей из библиотеки и пользовательских профилей.

Избегайте запуска приложений, сильно зависящего от операций ввода-вывода.

Если ваше приложение выполняет много операций ввода-вывода или сетевых вызовов во время запуска, это может негативно повлиять как на время запуска приложения, так и на точность тестирования производительности при запуске. Эти ресурсоемкие вызовы могут занимать неопределенное количество времени, которое может меняться со временем и даже между итерациями одного и того же теста. Операции ввода-вывода, как правило, лучше, чем сетевые вызовы, поскольку на последние могут влиять факторы, внешние по отношению к устройству и к самому устройству. Избегайте сетевых вызовов во время запуска. Если использование того или иного из них неизбежно, используйте операции ввода-вывода.

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

Если ваше приложение использует Hilt, вы можете указать фиктивные реализации, ограничивающие ввод-вывод, при проведении бенчмаркинга в Microbenchmark и Hilt .

Охватите все важные сценарии взаимодействия пользователя с системой.

Важно точно охватить все важные сценарии взаимодействия пользователя с приложением при создании базового профиля. Любые сценарии, которые не будут охвачены, не будут улучшены с помощью базового профиля. Наиболее эффективные базовые профили включают все распространенные сценарии взаимодействия пользователя при запуске приложения, а также сценарии взаимодействия пользователя внутри приложения, чувствительные к производительности, такие как прокручиваемые списки.

A/B-тестирование изменений профиля компиляции на этапе компиляции

Поскольку профили запуска и базовые профили являются оптимизацией на этапе компиляции, прямое A/B-тестирование различных APK-файлов с использованием Google Play Store, как правило, не поддерживается для релизов в производственной среде. Для оценки влияния в среде, приближенной к производственной, рассмотрите следующие подходы:

  • Внеплановый релиз : Загрузите внеплановый релиз для небольшой части вашей пользовательской базы, который включает только изменение профиля. Это позволит вам собрать реальные показатели разницы в производительности.

  • Локальное тестирование производительности : проведите локальное тестирование производительности вашего приложения с применением профиля и без него. Однако имейте в виду, что локальное тестирование показывает наилучший сценарий для профилей, поскольку оно не учитывает эффекты облачных профилей из ART, присутствующих на рабочих устройствах.