Depurar perfis de referência

Este documento mostra as práticas recomendadas para ajudar a diagnosticar problemas e garantir que seus perfis de referência funcionem corretamente, para oferecer o máximo de benefícios.

Problemas de build

Se você copiou o exemplo de perfis de referência no app de exemplo Now in Android (link em inglês), é possível que encontre falhas no teste durante a tarefa do perfil de referência, afirmando que os testes não podem ser executados em um emulador:

./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):
        ...

As falhas ocorrem porque o Now in Android usa um dispositivo gerenciado pelo Gradle para gerar perfis de referência. As falhas são esperadas, porque geralmente não é recomendável executar comparações de desempenho em um emulador. No entanto, como você não está coletando métricas de desempenho ao gerar perfis de referência, é possível coletar esses perfis em emuladores por conveniência. Para usar perfis de referência com um emulador, execute o build e a instalação na linha de comando e defina um argumento para ativar as regras de perfis de referência:

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

Como alternativa, você pode criar uma configuração de execução personalizada no Android Studio para ativar perfis de referência em emuladores, selecionando Run > Edit Configurations:

Adicionar uma configuração de execução personalizada para criar perfis de referência no Now in Android
Figura 1. Adicione uma configuração de execução personalizada para criar perfis de referência no Now in Android.

Problemas de instalação

Confira se o APK ou AAB que você está criando é de uma variante de build que inclui perfis de referência. A maneira mais fácil de verificar isso é abrindo o APK no Android Studio selecionando Build > Analyze APK, abrindo seu APK e procurando o perfil no arquivo /assets/dexopt/baseline.prof:

Verificar se há um perfil de referência usando o Visualizador de APK no Android Studio
Figura 2. Verifique se há um perfil de referência usando o Visualizador de APK no Android Studio.

Os perfis de referência precisam ser compilados no dispositivo que executa o app. Para instalações da app store e apps instalados usando PackageInstaller, a compilação no dispositivo acontece como parte do processo de instalação do app. No entanto, quando o app é transferido por sideload do Android Studio ou usando ferramentas de linha de comando, a biblioteca ProfileInstaller do Jetpack é responsável por enfileirar os perfis para compilação durante o processo seguinte de otimização de DEX em segundo plano. Nesses casos, se você quiser garantir que os perfis de referência estejam sendo usados, talvez seja necessário forçar a compilação de perfis de referência. O ProfileVerifier permite consultar o status da instalação e compilação do perfil, conforme mostrado no exemplo abaixo:

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);
    }
}

Os códigos de resultado abaixo dão dicas sobre a causa de alguns problemas:

RESULT_CODE_COMPILED_WITH_PROFILE
O perfil foi instalado e compilado, e será usado sempre que o app for executado. Esse é o resultado que você quer encontrar.
RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED
Nenhum perfil foi encontrado no APK ou AAB em execução. Verifique se você está usando uma variante de build que inclua perfis de referência caso esse erro apareça e se o APK contém um perfil.
RESULT_CODE_NO_PROFILE
Nenhum perfil foi instalado para este app durante a instalação pela app store ou pelo gerenciador de pacotes. O principal motivo para o código do erro é que o instalador do perfil não foi executado, porque o ProfileInstallerInitializer estava desativado. Quando esse erro é informado, um perfil incorporado ainda é encontrado no APK do aplicativo. Quando um perfil incorporado não é encontrado, o código do erro retornado é RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED.
RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION
Um perfil foi encontrado no APK ou AAB e foi colocado na fila para compilação. Quando um perfil é instalado pelo ProfileInstaller, ele é colocado na fila para compilação na próxima vez que a otimização DEX em segundo plano for executada pelo sistema. O perfil não fica ativo até que a compilação seja concluída. Não tente comparar seus perfis de referência até que a compilação seja concluída. Talvez seja necessário forçar a compilação de perfis de referência. Esse erro não vai ocorrer quando o app for instalado pela app store ou pelo gerenciador de pacotes em dispositivos com o Android 9 (API 28) ou versões mais recentes, porque a compilação é realizada durante a instalação.
RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING
Um perfil não correspondente foi instalado e o app foi compilado com ele. Isso acontece porque a instalação foi feita pela Google Play Store ou pelo gerenciador de pacotes. Esse resultado é diferente de RESULT_CODE_COMPILED_WITH_PROFILE, porque o perfil não correspondente compila apenas os métodos que ainda estão compartilhados entre o perfil e o app. O perfil é efetivamente menor do que o esperado e menos métodos serão compilados do que os incluídos no perfil de referência.
RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE
ProfileVerifier não pode gravar o arquivo de cache de resultados da verificação. Isso pode acontecer porque há algo errado com as permissões da pasta do app ou porque não há espaço livre em disco suficiente no dispositivo.
RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION
O ProfileVerifieris running on an unsupported API version of Android. ProfileVerifier oferece suporte apenas ao Android 9 (nível 28 da API) e versões mais recentes.
RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST
Uma PackageManager.NameNotFoundException é gerada ao consultar o PackageManager do pacote do app. Isso raramente deveria acontecer. Tente desinstalar o app e reinstalar tudo.
RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ
Há um arquivo de cache de resultados da verificação anterior, mas ele não pode ser lido. Isso raramente deveria acontecer. Tente desinstalar o app e reinstalar tudo.

Usar o ProfileVerifier na produção

Na produção, você pode usar o ProfileVerifier com bibliotecas de relatórios de análise, como o Google Analytics para Firebase, para gerar eventos de análise que indicam o status do perfil. Por exemplo, isso vai alertar você rapidamente se for lançada uma nova versão do app que não contenha perfis de referência.

Forçar a compilação de perfis de referência

Se o status de compilação dos perfis de referência for RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION, você poderá forçar a compilação imediata usando adb:

adb shell cmd package compile -r bg-dexopt PACKAGE_NAME

Verificar o estado da compilação sem o ProfileVerifier

Caso não esteja usando o ProfileVerifier, é possível verificar o estado de compilação usando o adb, embora ele não forneça insights tão profundos quanto o ProfileVerifier:

adb shell dumpsys package dexopt | grep -A 2 PACKAGE_NAME

O uso do adb produz algo semelhante a:

  [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]

O valor indica o status de compilação do perfil e é um dos valores abaixo:

Status da compilação Significado
speed‑profile Um perfil compilado existe e está sendo usado.
verify Não existe um perfil compilado.

Um status verify não significa que o APK ou o AAB não contém um perfil, porque ele pode ser colocado na fila para compilação pela próxima tarefa de otimização de DEX em segundo plano.

O motivo do valor indica o que aciona a compilação do perfil e é um destes valores:

Motivo Significado
install‑dm Um perfil de referência foi compilado manualmente ou pelo Google Play quando o app foi instalado.
bg‑dexopt Um perfil foi compilado enquanto seu dispositivo estava inativo. Pode ser um perfil de referência ou um perfil coletado durante o uso do app.
cmdline A compilação foi acionada usando o adb. Pode ser um perfil de referência ou um perfil coletado durante o uso do app.

Problemas de desempenho

Esta seção mostra algumas práticas recomendadas para definir e comparar corretamente seus perfis de referência e aproveitar ao máximo os benefícios deles.

Comparar corretamente as métricas de inicialização

Os perfis de referência serão mais eficazes se as métricas de inicialização forem bem definidas. As duas métricas principais são tempo para exibição inicial (TTID, na sigla em inglês) e tempo para exibição total (TTFD, na sigla em inglês).

O TTID representa quando o app renderiza o primeiro frame. É importante que ele seja o mais curto possível, porque quando algo aparece na tela, o usuário sabe que o app está em execução. Você pode até mesmo mostrar um indicador de progresso indeterminado para mostrar que o app é responsivo.

O TTFD ocorre quando é possível interagir com o app. É importante que ele seja o mais breve possível para evitar a frustração do usuário. Se você sinalizar corretamente o TTFD, vai informar ao sistema que o código executado no caminho para o TTFD faz parte da inicialização do app. É mais provável que o sistema coloque esse código no perfil como um resultado.

Mantenha o TTID e o TTFD o mais curtos possíveis para tornar o app responsivo.

O sistema consegue detectar o TTID, mostrá-lo no Logcat e informar como parte das comparações de inicialização. No entanto, o sistema não consegue determinar o TTFD, e é responsabilidade do app informar quando atinge um estado interativo totalmente renderizado. Para fazer isso, chame reportFullyDrawn() ou ReportDrawn, se você estiver usando o Jetpack Compose. Se você tiver várias tarefas em segundo plano que precisam ser concluídas antes que o app seja considerado totalmente renderizado, use FullyDrawnReporter, conforme descrito em Melhorar a precisão da marcação do tempo de inicialização.

Perfis de biblioteca e perfis personalizados

Ao comparar o impacto dos perfis, pode ser difícil separar os benefícios dos perfis do app daqueles fornecidos por bibliotecas, como as bibliotecas do Jetpack. Quando você cria o APK, o Plug-in do Android para Gradle adiciona todos os perfis nas dependências de biblioteca e ao seu perfil personalizado. Isso é bom para otimizar o desempenho geral e é recomendado para builds de lançamento. No entanto, fica difícil medir o ganho de desempenho extra proveniente do perfil personalizado.

Uma maneira rápida de conferir manualmente a otimização extra fornecida pelo seu perfil personalizado é removê-la e executar as comparações. Em seguida, substitua a API e execute as comparações novamente. A comparação dos dois vai mostrar as otimizações oferecidas apenas pelos perfis da biblioteca, além do seu personalizado.

Uma maneira automatizável de comparar perfis é criando uma nova variante de build que contenha apenas os perfis da biblioteca, e não o personalizado. Compare as comparações dessa variante com a de lançamento que contém os perfis de biblioteca e personalizados. O exemplo abaixo mostra como configurar a variante que inclui apenas perfis da biblioteca. Adicione uma nova variante chamada releaseWithoutCustomProfile ao módulo de consumidor do perfil, que normalmente é o módulo do app:

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"))
    }
  }
}

Groovy

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

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

O exemplo de código anterior remove a dependência baselineProfile de todas as variantes e a aplica seletivamente apenas à variante release. Pode parecer não intuitivo que os perfis da biblioteca ainda sejam adicionados quando a dependência no módulo do produtor de perfil é removida. No entanto, esse módulo é responsável apenas por gerar seu perfil personalizado. O Plug-in do Android para Gradle ainda está em execução para todas as variantes e é responsável por incluir perfis de biblioteca.

Você também precisa adicionar a nova variante ao módulo do gerador de perfis. Neste exemplo, o módulo do produtor é chamado de :baselineprofile.

Kotlin

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

Groovy

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

Para comparar apenas com os perfis da biblioteca, execute o seguinte:

./gradlew :baselineprofile:connectedBenchmarkReleaseWithoutCustomProfileAndroidTest

Para comparar com os perfis de biblioteca e com o perfil personalizado, execute o seguinte:

./gradlew :baselineprofile:connectedBenchmarkReleaseAndroidTest

Executar o código anterior no app de exemplo da Macrobenchmark mostra que há uma diferença de desempenho entre as duas variantes. Com apenas os perfis da biblioteca, a comparação quente startupCompose mostra os seguintes resultados:

SmallListStartupBenchmark_startupCompose[mode=COLD]
timeToInitialDisplayMs   min  70.8,   median  79.1,   max 126.0
Traces: Iteration 0 1 2 3 4 5 6 7 8 9

Há perfis em muitas bibliotecas do Jetpack Compose, então há algumas otimizações usando apenas o plug-in do perfil de referência para Gradle. No entanto, há outras otimizações ao usar o perfil personalizado:

SmallListStartupBenchmark_startupCompose[mode=COLD]
timeToInitialDisplayMs   min 57.9,   median 73.5,   max 92.3
Traces: Iteration 0 1 2 3 4 5 6 7 8 9

Evitar a inicialização do app vinculada à E/S

Se o app estiver realizando muitas chamadas de E/S ou de redes durante a inicialização, isso poderá afetar negativamente o tempo de inicialização do app e a precisão da comparação de inicialização. Essas chamadas pesadas podem levar um tempo indeterminado, que pode variar até mesmo entre iterações da mesma comparação. As chamadas de E/S geralmente são melhores que as chamadas de rede, porque as últimas podem ser afetadas por fatores externos ao dispositivo e no próprio dispositivo. Evite chamadas de rede durante a inicialização. Quando o uso de um ou outro tipo de chamada for inevitável, use as chamadas de E/S.

Recomendamos que a arquitetura do app ofereça suporte à inicialização do app sem chamadas de rede ou de E/S, mesmo que seja apenas para uso durante a comparação da inicialização. Isso ajuda a garantir a menor variabilidade possível entre diferentes iterações das comparações.

Se o app usa o Hilt, você pode fornecer implementações falsas vinculadas de E/S ao fazer comparações na Microbenchmark e no Hilt.

Abordar todas as jornadas importantes do usuário

É importante abordar com cuidado todas as jornadas importantes do usuário na geração de perfis de referência. As jornadas do usuário que não forem cobertas não serão melhoradas pelos perfis de referência. Os perfis de referência mais eficazes incluem todas as jornadas comuns do usuário na inicialização, além das jornadas do usuário no app que exigem maior desempenho, como listas de rolagem.