Как отлаживать профили базовой конфигурации

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

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

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

./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 для создания профиля Baseline Profile. Это ожидаемо, поскольку обычно не следует запускать тесты производительности на эмуляторе. Однако, поскольку при создании профилей базовой конфигурации вы не собираете показатели эффективности, для удобства вы можете запустить сбор профилей базовой конфигурации в эмуляторах. Чтобы использовать профили базовой конфигурации в эмуляторе, выполните сборку и установку из командной строки и задайте аргумент, чтобы включить правила профилей базовой конфигурации:

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

Вы также можете создать собственную конфигурацию запуска в Android Studio, чтобы включить профили Baseline на эмуляторах. Для этого выберите Run (Запуск) > Edit Configurations (Изменить конфигурации).

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

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

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

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

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

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

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

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

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

Kotlin

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-файле не найден профиль. Если вы видите эту ошибку, убедитесь, что используете вариант сборки, включающий профили Baseline, и что APK содержит профиль.
RESULT_CODE_NO_PROFILE
При установке приложения через магазин приложений или менеджер пакетов не был установлен профиль. Основная причина этого кода ошибки заключается в том, что установщик профиля не был запущен, так как ProfileInstallerInitializer отключен. Обратите внимание, что при возникновении этой ошибки встроенный профиль все равно будет найден в APK-файле приложения. Если встроенный профиль не найден, возвращается код ошибки RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED.
RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION
Профиль найден в APK-файле или наборе App Bundle и поставлен в очередь на компиляцию. Когда профиль устанавливается с помощью ProfileInstaller, он ставится в очередь на компиляцию при следующей фоновой оптимизации DEX-файлов. Профиль не будет активен, пока компиляция не завершится. Не пытайтесь тестировать профили Baseline до завершения компиляции. Возможно, вам потребуется принудительно скомпилировать профили Baseline Profile. Эта ошибка не возникает, если приложение установлено из Google Play или с помощью диспетчера пакетов на устройстве с 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
ProfileVerifieris 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 Аналитика для 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. Это может быть базовый профиль или профиль, собранный во время использования приложения.

Проверьте, что профиль запуска применяется к DEX и r8.json

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

Проверьте расположение DEX-файлов с помощью r8.json (рекомендуется для AGP 8.8 или более поздней версии)

Если в проекте используется плагин Android Gradle (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 (Анализ APK) в Android Studio.
  2. Перейдите к первому DEX-файлу. например classes.dex.
  3. Проверьте содержимое этого DEX-файла. Убедитесь, что критически важные классы и методы, определенные в файле профиля запуска (startup-prof.txt), присутствуют в основном файле DEX. Если все сделано правильно, то эти критически важные для запуска компоненты будут загружаться быстрее.

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

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

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

Профили Baseline будут эффективнее, если вы четко определите показатели запуска. Два ключевых показателя – время до начальной отрисовки (TTID) и время до полной отрисовки (TTFD).

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

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

Чтобы приложение работало быстро, оба показателя должны быть как можно ниже.

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

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

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

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

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

Kotlin

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.

Kotlin

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

Яркий

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

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

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

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

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

Если в вашем приложении используется Hilt, вы можете предоставить фиктивные реализации, связанные с I/O, при тестировании производительности в Microbenchmark и Hilt.

Охватите все важные пути пользователей

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

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

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

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

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