Comparar casos de uso com a Jetpack Macrobenchmark

A biblioteca Macrobenchmark possibilita que você crie testes de desempenho de inicialização e execução diretamente no app em dispositivos com o Android M (API 23) ou versões mais recentes.

É recomendável usar a Macrobenchmark com a versão mais nova do Android Studio (2021.1.1 ou mais recente). Há novos recursos nessa versão do ambiente de desenvolvimento integrado à Macrobenchmark. Os usuários de versões anteriores do Android Studio podem usar a instrução extra mostrada mais adiante nesta página para trabalhar com arquivos de rastreamento.

O teste de comparação é fornecido pela API de regra MacrobenchmarkRule do JUnit4 na biblioteca Macrobenchmark:

Kotlin

    @get:Rule
    val benchmarkRule = MacrobenchmarkRule()

    @Test
    fun startup() = benchmarkRule.measureRepeated(
        packageName = "mypackage.myapp",
        metrics = listOf(StartupTimingMetric()),
        iterations = 5,
        startupMode = StartupMode.COLD
    ) { // this = MacrobenchmarkScope
        pressHome()
        val intent = Intent()
        intent.setPackage("mypackage.myapp")
        intent.setAction("mypackage.myapp.myaction")
        startActivityAndWait(intent)
    }
  

Java

    @Rule
    MacrobenchmarkRule benchmarkRule = MacrobenchmarkRule()

    @Test
    void startup() = benchmarkRule.measureRepeated(
        "mypackage.myapp", // packageName
        listOf(StartupTimingMetric()), // metrics
        5, // iterations
        StartupMode.COLD // startupMode
    ) { scope ->
        scope.pressHome()
        Intent intent = Intent()
        intent.setPackage("mypackage.myapp")
        intent.setAction("mypackage.myapp.myaction")
        scope.startActivityAndWait(intent)
    }
  

As métricas são exibidas diretamente no Android Studio e também são geradas para o uso de CI em um arquivo JSON.

Exemplos de resultados do Studio

Configuração do módulo

A biblioteca Macrobenchmark exige um com.android.test módulo separado do código do app, responsável por executar os que medem o desempenho do app.

Bumblebee

No Android Studio Bumblebee, há um modelo disponível para simplificar a configuração do módulo da Macrobenchmark.

Adicionar um novo módulo

O modelo de módulo Benchmark cria automaticamente um módulo no projeto para medir o aplicativo criado por um módulo, incluindo um exemplo de inicialização da Benchmark.

Para usar o modelo do módulo a fim de criar um novo módulo, siga as etapas abaixo:

  1. Clique com o botão direito do mouse no seu projeto ou módulo no painel Project do Android Studio e clique em New > Module.

  2. Selecione Benchmark Module.

    Modelo de módulo da Benchmark

  3. É possível personalizar o aplicativo de destino (o aplicativo a ser comparado), bem como o nome do pacote e do novo módulo Macrobenchmark.

  4. Clique em Finish.

Arctic Fox

No Arctic Fox, você criará um módulo de biblioteca e o converterá em um módulo de teste.

Adicionar um novo módulo

Adicione um novo módulo ao projeto. Esse módulo contém testes da Macrobenchmark.

  1. Clique com o botão direito do mouse no seu projeto ou módulo no painel Project do Android Studio e clique em New > Module.
  2. Selecione Android Library no painel Templates.
  3. Digite macrobenchmark como o nome do módulo.
  4. Defina o Minimum SDK como API 23: Android M.
  5. Clique em Finish.

Configurar um novo módulo de biblioteca

Modificar o arquivo do Gradle

Personalize o build.gradle do módulo Macrobenchmark da seguinte maneira:

  1. Mude o plug-in de com.android.library para com.android.test.
  2. Adicione outras propriedades obrigatórias do módulo de teste no bloco android {}:
  3.    targetProjectPath = ":app" // Note that your module name may be different
    
       // Enable the benchmark to run separately from the app process
       experimentalProperties["android.experimental.self-instrumenting"] = true
       buildTypes {
           // Declare a build type (release) to match the target app's build type
           release {
               debuggable = true
           }
       }
  4. Mude todas as dependências chamadas testImplementation ou androidTestImplementation para implementation.
  5. Adicione uma dependência à biblioteca Macrobenchmark:
    • implementation 'androidx.benchmark:benchmark-macro-junit4:1.1.0-alpha13'
  6. Após o bloco android {}, mas antes do dependencies {}, adicione:
  7.    androidComponents {
          beforeVariants(selector().all()) {
              // Enable only the benchmark buildType, since we only want to measure
              // release-like build performance (should match app buildType)
              enabled = buildType == 'benchmark'
          }
       }

Simplificar a estrutura de diretórios

Em um módulo com.android.test, há apenas um diretório de origem para todos os testes. Exclua os outros diretórios de origem, incluindo src/test e src/androidTest, já que eles não são usados.

Consulte o módulo de exemplo da Macrobenchmark (link em inglês) para referência.

Criar uma Macrobenchmark

Defina uma nova classe de teste nesse módulo, preenchendo o nome do pacote do app:

@RunWith(AndroidJUnit4::class)
class SampleStartupBenchmark {
    @get:Rule
    val benchmarkRule = MacrobenchmarkRule()

    @Test
    fun startup() = benchmarkRule.measureRepeated(
        packageName = "mypackage.myapp",
        metrics = listOf(StartupTimingMetric()),
        iterations = 5,
        startupMode = StartupMode.COLD
    ) { // this = MacrobenchmarkScope
        pressHome()
        val intent = Intent()
        intent.setPackage("mypackage.myapp")
        intent.setAction("mypackage.myapp.myaction")
        startActivityAndWait(intent)
    }
}
   

Configurar o app

Para avaliar um app, chamado de destino da Macrobenchmark, ele precisa ser capaz de criar um perfil para ler informações detalhadas sobre o trace. É possível ativar isso na tag <application> do AndroidManifest.xml do app:

<application ... >
    <!-- Profileable to enable Macrobenchmark profiling -->
    <!-- Suppress AndroidElementNotAllowed -->
    <profileable android:shell="true"/>
    ...
</application>

Configure o app comparado para que fique o mais próximo possível da experiência do usuário. Defina-o como não depurável e, de preferência, com a minificação ativada, que melhora o desempenho. Normalmente, isso é feito criando uma cópia de benchmark da variante release, que terá o mesmo desempenho, mas será assinada localmente com chaves debug:

buildTypes {
    benchmark {
        // duplicate any release build type settings for measurement accuracy,
        // such as "minifyEnabled" and "proguardFiles" in this block

        debuggable false
        signingConfig signingConfigs.debug
    }
}

Sincronize com o Gradle, abra o painel Build Variants à esquerda e selecione a variante benchmark do app e do módulo Macrobenchmark. Isso vai garantir que a comparação crie e teste a variante correta do app:

Selecionar variante da Benchmark

A execução da Macrobenchmark em atividades internas requer mais uma etapa. Para comparar uma atividade interna exported=false, transmita um setupBlock para MacrobenchmarkRule.measureRepeated() para ir até o código que será comparado. Use o measureBlock para invocar a ação de inicialização ou rolagem para fazer a medição.

Personalizar a Macrobenchmark

CompilationMode

As Macrobenchmarks podem especificar uma classe CompilationMode, que define quanto do app precisa ser pré-compilado do bytecode DEX (o formato de bytecode dentro de um APK) para código de máquina (semelhante à linguagem C++ pré-compilada).

Por padrão, as Macrobenchmarks são executadas com CompilationMode.DEFAULT, que no Android Nougat (API de nível 24) e versões mais recentes, instala um perfil de referência (se disponível). Já no Android Marshmallow (API de nível 23) e versões anteriores, ele compila completamente o APK, que é o comportamento padrão do sistema.

Você poderá instalar um perfil de referência se o aplicativo de destino tiver um perfil de referência e a biblioteca ProfileInstaller.

No Android Nougat (API de nível 24) ou versões mais recentes, é possível personalizar a classe CompilationMode para afetar a quantidade de pré-compilação no dispositivo para imitar diferentes níveis de compilação antecipada (AOT, na sigla em inglês) ou armazenamento em cache JIT. Consulte CompilationMode.Full, CompilationMode.Partial e CompilationMode.None.

Essa funcionalidade é criada com base em comandos de compilação do ART. Cada comparação limpa os dados do perfil antes de começar para garantir que não haja interferência entre comparações.

Inicialização

Para executar uma atividade, você pode transmitir um modo de inicialização predefinido (COLD, WARM ou HOT) para a função measureRepeated(). Esse parâmetro muda a forma como a atividade é iniciada e o estado do processo no início do teste.

Para saber mais sobre os tipos de inicialização, consulte a documentação sobre inicialização do Android vitals.

Rolagem e animação

Diferentemente da maioria dos testes de IU do Android, os testes da Macrobenchmark acontecem em um processo separado do próprio app. Isso é necessário para ativar ações como eliminar o processo do app e compilá-lo usando comandos do shell.

Você pode direcionar o app usando a biblioteca UI Automator ou outro mecanismo que controle o app de destino no processo de teste. Abordagens como Espresso ou ActivityScenario não funcionarão porque geralmente são executadas em um processo compartilhado com o app.

O exemplo a seguir encontra uma RecyclerView usando o próprio ID de recurso e rola para baixo várias vezes:

@Test
fun measureScroll() {
    benchmarkRule.measureRepeated(
        packageName = "mypackage.myapp",
        metrics = listOf(FrameTimingMetric()),
        iterations = 5,
        setupBlock = {
            // before starting to measure, navigate to the UI to be measured
            val intent = Intent()
            intent.action = ACTION
            startActivityAndWait(intent)
        }
    ) {
        val recycler = device.findObject(By.res("mypackage.myapp", "recycler_id"))
        // Set gesture margin to avoid triggering gesture nav
        // with input events from automation.
        recycler.setGestureMargin(device.displayWidth / 5)

        // Scroll down several times
        for (i in 1..10) {
            recycler.scroll(Direction.DOWN, 2f)
            device.waitForIdle()
        }
    }
}

Conforme o teste especifica uma FrameTimingMetric, o tempo dos frames é gravado e relatado como um resumo de alto nível da distribuição de tempo para a renderização do frame: 50º, 90º, 95º e 99º percentis.

Seu comparativo não precisa rolar a IU. Em vez disso, ele pode, por exemplo, exibir uma animação. Ele também não precisa usar o IU Automator especificamente. Desde que os frames estejam sendo produzidos pelo sistema de visualização, o que inclui frames produzidos pelo Compose, as métricas de desempenho serão coletadas. Os mecanismos do processo, como o Espresso, não funcionarão porque o app precisa ser gerado pelo processo do app de teste.

Executar a Macrobenchmark

Faça o teste no Android Studio para medir o desempenho do app no dispositivo. É necessário realizar o teste em um dispositivo físico, e não em um emulador, porque ele não produz números de desempenho do produto que representem a experiência do usuário final.

Consulte a seção Comparações na CI para ver informações sobre como fazer e monitorar comparações na integração contínua.

Também é possível fazer todas as comparações na linha de comando usando o comando connectedCheck:

$ ./gradlew :macrobenchmark:connectedCheck

Erros de configuração

Se o app estiver configurado incorretamente (por exemplo, se for depurável ou não estiver relacionado à criação de perfil), a MacroBenchmark gerará um erro, em vez de relatar uma medição incorreta ou incompleta. É possível suprimir esses erros com o argumento androidx.benchmark.suppressErrors.

Erros também são gerados ao tentar medir um emulador ou em um dispositivo com bateria fraca, porque isso pode comprometer a disponibilidade e a velocidade do relógio.

Inspecionar um trace

Cada iteração medida captura um rastreamento do sistema separado. É possível abrir os resultados de traces clicando em um dos links no painel Test Results, como mostrado na imagem da seção Jetpack Macrobenchmark deste tópico. Quando o trace é carregado, o Android Studio pede que você selecione o processo a ser analisado. A seleção é pré-preenchida com o processo do app de destino:

Seleção de processos de trace do Studio

Após o arquivo de trace ser carregado, o Studio mostra os resultados na ferramenta CPU Profiler:

Trace do Studio

Acessar arquivos de trace manualmente

Se você estiver usando uma versão mais antiga do Android Studio (anterior a 2020.3.1) ou a ferramenta Perfetto para analisar um arquivo de trace, há outras etapas envolvidas.

Primeiro, extraia o arquivo de trace do dispositivo:

# The following command pulls all files ending in .perfetto-trace from the directory
# hierarchy starting at the root /storage/emulated/0/Android.
$ adb shell find /storage/emulated/0/Android/ -name "*.perfetto-trace" \
    | tr -d '\r' | xargs -n1 adb pull

O caminho do arquivo de saída poderá ser diferente se você personalizá-lo com o argumento additionalTestOutputDir. Você pode procurar registros de caminho de trace no logcat para ver onde eles estão gravados. Exemplo:

I PerfettoCapture: Writing to /storage/emulated/0/Android/data/androidx.benchmark.integration.macrobenchmark.test/cache/TrivialStartupBenchmark_startup[mode=COLD]_iter002.perfetto-trace.

Se você invocar os testes usando a linha de comando do Gradle, como ./gradlew macrobenchmark:connectedCheck, os arquivos de resultados de teste poderão ser copiados em um diretório de saída de teste no sistema host. Para fazer isso, adicione esta linha ao arquivo gradle.properties do projeto:

android.enableAdditionalTestOutput=true

Os arquivos de resultado das execuções de teste são exibidos no diretório do build do seu projeto da seguinte forma:

build/outputs/connected_android_test_additional_output/debugAndroidTest/connected/<device-name>/TrivialStartupBenchmark_startup[mode=COLD]_iter002.perfetto-trace

Quando tiver o arquivo de trace no sistema host, você poderá abri-lo no Android Studio em File > Open no menu. Isso mostra a visualização da ferramenta do criador de perfil exibida na seção anterior.

Outra alternativa é usar a ferramenta Perfetto. O Perfetto possibilita inspecionar todos os processos que ocorrem em todo o dispositivo durante o trace, enquanto o CPU Profiler do Android Studio limita a inspeção a um único processo.

Melhorar os dados de trace com eventos personalizados

Pode ser útil instrumentar seu aplicativo com eventos de trace personalizados, que são vistos com o restante do relatório de trace e podem ajudar a apontar problemas específicos do app. Para saber mais sobre como criar eventos de trace personalizados, consulte o guia Definir eventos personalizados.

Como fazer comparações na CI

É comum fazer testes na CI sem o Gradle ou localmente se você estiver usando um sistema de compilação diferente. Nesta seção, explicamos como configurar a Macrobenchmark para uso na CI no momento da execução.

Arquivos de resultados: JSON e traces

A Macrobenchmark gera um arquivo JSON e vários arquivos de trace: um por iteração medida de cada loop MacrobenchmarkRule.measureRepeated.

Você pode definir onde esses arquivos serão gravados, transmitindo o seguinte argumento de instrumentação no momento da execução:

-e additionalTestOutputDir "device_path_you_can_write_to"

Para simplificar, você pode especificar um caminho em /sdcard/, mas é necessário desativar o armazenamento com escopo definindo requestLegacyExternalStorage como true no seu módulo Macrobenchmark:

<manifest ... >
  <application android:requestLegacyExternalStorage="true" ... >
    ...
  </application>
</manifest>

Ou transmita um argumento de instrumentação para ignorar o armazenamento com escopo durante o teste:

-e no-isolated-storage 1

Exemplo de JSON

Veja a seguir um exemplo de saída JSON para uma única comparação de inicialização:

{
    "context": {
        "build": {
            "device": "walleye",
            "fingerprint": "google/walleye/walleye:10/QQ3A.200805.001/6578210:userdebug/dev-keys",
            "model": "Pixel 2",
            "version": {
                "sdk": 29
            }
        },
        "cpuCoreCount": 8,
        "cpuLocked": false,
        "cpuMaxFreqHz": 2457600000,
        "memTotalBytes": 3834605568,
        "sustainedPerformanceModeEnabled": false
    },
    "benchmarks": [
        {
            "name": "startup",
            "params": {},
            "className": "androidx.benchmark.integration.macrobenchmark.SampleStartupBenchmark",
            "totalRunTimeNs": 77969052767,
            "metrics": {
                "startupMs": {
                    "minimum": 228,
                    "maximum": 283,
                    "median": 242,
                    "runs": [
                        238,
                        283,
                        256,
                        228,
                        242
                    ]
                }
            },
            "warmupIterations": 3,
            "repeatIterations": 5,
            "thermalThrottleSleepSeconds": 0
        }
    ]
}

Outros recursos

Um projeto de exemplo (link em inglês) está disponível como parte do repositório Android/performance-samples no GitHub.

Para ver orientações sobre como detectar regressões de desempenho, consulte Como combater regressões com os comparativos de mercado na CI (link em inglês).

Feedback

Para comunicar problemas ou enviar solicitações de recursos para a MacroBenchmark Jetpack, consulte o Issue Tracker público.