1. Antes de começar
Neste codelab, você vai aprender a usar a biblioteca Macrobenchmark. Com ela, você vai medir o tempo de inicialização do app, que é uma métrica importante para o engajamento do usuário, e o tempo para a renderização de frames, que indica possíveis pontos de instabilidade no app.
O que é necessário
- Android Studio Dolphin (2021.3.1) ou mais recente
- Conhecimento sobre Kotlin
- Conhecimentos básicos sobre testes no Android
- Um dispositivo físico com Android 6 (nível 23 da API) ou mais recente
O que você vai fazer
- Adicionar um módulo de comparação a um app que já existe.
- Medir o tempo de inicialização do app e de renderização de frames.
O que você vai aprender
- Medir o desempenho do aplicativo de maneira confiável
2. Etapas da configuração
Para começar, clone o repositório do GitHub na linha de comando desta maneira:
$ git clone https://github.com/android/codelab-android-performance.git
Outra opção é fazer o download de dois arquivos ZIP:
Abrir o projeto no Android Studio
- Na janela "Welcome to Android Studio", selecione
Open an Existing Project.
- Selecione a pasta
[Download Location]/android-performance/benchmarking
. Dica: selecione o diretóriobenchmarking
que contémbuild.gradle
. - Depois que o Android Studio importar o projeto, confira se é possível executar o módulo
app
para criar o app de exemplo que vamos usar para executar as comparações.
3. Introdução à biblioteca Jetpack Macrobenchmark
A biblioteca Jetpack Macrobenchmark mede a performance de interações maiores do usuário final, como inicialização, interações com a interface e animações. Ela oferece controle direto sobre o ambiente de performance testado. Com essa biblioteca, você pode controlar a compilação, a inicialização e a interrupção do app para medir diretamente o tempo de inicialização do app, o tempo de renderização de frames e as seções rastreadas do código.
Com a biblioteca Jetpack Macrobenchmark, você pode:
- Avaliar o app várias vezes usando padrões determinísticos de inicialização e de velocidade de rolagem.
- Reduzir as variações de performance com base em uma média de resultados alcançada após executar vários testes.
- Controlar o estado de compilação do app, fator essencial para a estabilidade da performance.
- Verificar o desempenho real do app com a reprodução de otimizações feitas no momento de instalação pela Google Play Store.
Os testes de instrumentação dessa biblioteca não chamam o código do aplicativo diretamente. Em vez disso, eles navegam pelo app da maneira que um usuário faria, executando toques e cliques, deslizando a tela, entre outros. A avaliação é realizada durante essas interações. Para avaliar partes específicas do código do aplicativo, consulte a biblioteca Jetpack Microbenchmark.
O processo de criar uma comparação é parecido com o de criar um teste de instrumentação, com a diferença de que não é necessário verificar o estado do app. As comparações usam a sintaxe JUnit (@RunWith
, @Rule
, @Test
etc.), mas os testes são executados em um processo separado para permitir a reinicialização ou a pré-compilação do app. Isso permite executar o app sem interferir nos estados internos, como o usuário faria. Para fazer isso, usamos o UiAutomator
para interagir com o aplicativo de destino.
App de exemplo
Neste codelab, você trabalhará com o aplicativo de exemplo JetSnack (link em inglês). um app virtual de pedidos de comida que usa o Jetpack Compose. Para avaliar o desempenho, não é necessário saber detalhes sobre a arquitetura do app. Basta compreender como o app se comporta e a estrutura da interface, para que você consiga acessar os elementos dela nas comparações. Execute o app e navegue pelas telas básicas, simulando pedidos dos lanches que você quiser.
4. Adicionar a biblioteca Macrobenchmark
Para usar a Macrobenchmark, é necessário adicionar um novo módulo do Gradle ao projeto. A maneira mais fácil de fazer isso é usando o assistente de módulos do Android Studio.
Abra a caixa de diálogo do novo módulo clicando com o botão direito do mouse no projeto ou módulo no painel Project e selecionando New > Module.
Selecione Benchmark no painel Templates. Confira se Macrobenchmark está selecionado como o tipo de módulo de comparação e confirme se as informações estão preenchidas da maneira esperada:
- Target application: o app que vai ser comparado.
- Module name: nome do módulo de comparação do Gradle.
- Package name: nome do pacote para comparações.
- Minimum SDK: é necessário usar pelo menos o Android 6 (nível 23 da API) ou versões mais recentes.
Clique em Finish.
Mudanças implementadas pelo assistente de módulos
O assistente de módulos faz várias mudanças no projeto.
Ele adiciona um módulo do Gradle com o nome macrobenchmark
ou o selecionado no assistente. Esse módulo usa o plug-in com.android.test
, que instrui o Gradle a não incluí-lo no app. Por esse motivo, ele só pode conter código de teste ou comparações.
O assistente também faz mudanças no módulo do app de destino selecionado. Mais especificamente, ele adiciona um novo tipo de build benchmark
ao módulo :app
build.gradle
, conforme mostrado no snippet de código abaixo:
benchmark {
initWith buildTypes.release
signingConfig signingConfigs.debug
matchingFallbacks = ['release']
debuggable false
}
Esse buildType precisa emular o buildType da release
da maneira mais semelhante possível. A diferença dele para o buildType da release
é que signingConfig
é definida como debug
, o que é necessário para criar o app localmente sem um keystore de produção.
No entanto, como a sinalização debuggable
está desativada, o assistente adiciona a tag <profileable>
ao AndroidManifest.xml
, para permitir que as comparações criem perfis no app com a performance da versão de lançamento.
<application>
<profileable
android:shell="true"
tools:targetApi="q" />
</application>
Para mais informações sobre as funções de <profileable>
, consulte nossa documentação.
A última coisa que o assistente faz é criar um scaffold para comparar os tempos de inicialização. Vamos tratar desse assunto na próxima etapa.
Agora você já pode começar a criar as comparações.
5. Medir o tempo de inicialização do app
O tempo de inicialização do app, ou o tempo que os usuários precisam esperar para começar a usar o app, é uma métrica essencial para o engajamento do usuário. O assistente de módulo cria uma classe de teste ExampleStartupBenchmark
, que consegue medir o tempo de inicialização do app. Ela tem esta aparência:
@RunWith(AndroidJUnit4::class)
class ExampleStartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "com.example.macrobenchmark_codelab",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD,
){
pressHome()
startActivityAndWait()
}
}
O que significam todos os parâmetros?
Ao programar uma comparação, o ponto de entrada é a função measureRepeated
da MacrobenchmarkRule
. Essa função executa as comparações, mas é necessário especificar os parâmetros abaixo:
packageName
: as comparações são executadas em um processo separado do app em teste. Portanto, é necessário especificar o app que vai ser avaliado.metrics
: o tipo de informação que você quer avaliar durante a comparação. Nesse caso, estamos interessados no tempo de inicialização do app. Confira outros tipos de métricas na documentação.iterations
: quantas vezes a comparação vai ser repetida. Mais iterações geram resultados mais estáveis, mas exigem um tempo de execução mais longo. O número ideal de vezes depende do nível de ruído que a métrica sendo avaliada gera no app.startupMode
: permite definir a maneira como app vai ser iniciado quando a comparação começar. As opções disponíveis são:COLD
,WARM
eHOT
. Nesse caso, usamosCOLD
, porque esse tipo de inicialização representa a maior quantidade de trabalho que um app precisa realizar.measureBlock
(último parâmetro lambda): nessa função, você define as ações que vão ser avaliadas na comparação, como iniciar uma atividade, clicar nos elementos da interface, rolar ou deslizar a tela, entre outras. A biblioteca Macrobenchmak coleta asmetrics
definidas nesse bloco.
Como programar ações de comparação
A biblioteca Macrobenchmark reinstala e reinicia o app. É importante programar as interações de forma independente do estado do app. A biblioteca Macrobenchmark oferece diversas funções e parâmetros úteis para interagir com o app.
A função mais importante é startActivityAndWait()
. Ela inicia a atividade padrão e aguarda até que o app renderize o primeiro frame antes de continuar seguindo as instruções da comparação programada. Se você quiser iniciar uma atividade diferente ou ajustar a intent inicial, use os parâmetros opcionais intent
ou block
.
Outra função útil é pressHome()
, que permite redefinir a comparação como uma condição padrão em casos em que o app não é encerrado a cada iteração, por exemplo, ao usar StartupMode.HOT
.
Para qualquer outra interação, use o parâmetro device
, que permite encontrar elementos da interface, rolar a tela, aguardar um conteúdo específico, entre outras interações.
Agora que definimos a comparação de inicialização, você vai poder executá-la na próxima etapa.
6. Executar a comparação
Antes de executar o teste de comparação, verifique se você selecionou a variante de build certa no Android Studio:
- Selecione o painel Build Variants.
- Mude a Active Build Variant para benchmark.
- Aguarde a sincronização do Android Studio.
Se não fizéssemos isso, a comparação falharia durante a execução e geraria um erro informando que não é possível comparar um aplicativo debuggable
:
java.lang.AssertionError: ERRORS (not suppressed): DEBUGGABLE WARNINGS (suppressed): ERROR: Debuggable Benchmark Benchmark is running with debuggable=true, which drastically reduces runtime performance in order to support debugging features. Run benchmarks with debuggable=false. Debuggable affects execution speed in ways that mean benchmark improvements might not carry over to a real user's experience (or even regress release performance).
É possível eliminar esse erro temporariamente usando o argumento de instrumentação androidx.benchmark.suppressErrors = "DEBUGGABLE"
. Siga as mesmas etapas apresentadas na seção Como executar comparações no Android Emulator.
Agora é possível fazer as comparações da mesma maneira que você faria com os testes instrumentados. Você pode executar a função de teste ou toda a classe que tem o ícone de gutter ao lado.
Lembre-se de selecionar um dispositivo físico. No Android Emulator, as comparações geram uma falha durante a execução, mostrando um aviso que os resultados gerados vão ser incorretos. Tecnicamente, é possível executar uma comparação em um emulador, mas o que seria avaliado é o desempenho da máquina host. Se essa máquina estiver sobrecarregada, as comparações vão ter um desempenho mais lento e vice-versa.
Depois de executar a comparação, o app vai ser recriado e, em seguida, as comparações serão executadas novamente. As comparações vão iniciar, interromper e até mesmo reinstalar o app várias vezes, de acordo com as iterations
definidas.
7. Opcional: como executar comparações no Android Emulator
Se você não tem um dispositivo físico e ainda quer executar as comparações no emulador, é possível eliminar o erro durante a execução usando o argumento de instrumentação androidx.benchmark.suppressErrors = "EMULATOR"
Para eliminar o erro, edite a configuração de execução:
- Selecione "Edit Configurations…" no menu de execução:
- Na janela aberta, selecione o ícone de opções
ao lado de "Instrumentation arguments".
- Para adicionar o parâmetro de instrumentação extra, clique em ➕ e digite os detalhes.
- Clique em OK para confirmar. Você vai encontrar o argumento na linha "Instrumentation arguments".
- Clique em OK para confirmar a configuração de execução.
Como alternativa, se precisar manter esse comportamento permanentemente na base de código, faça isso no build.gradle
no módulo :macrobenchmark
:
defaultConfig {
// ...
testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = 'EMULATOR'
}
8. Entender os resultados da inicialização
Depois que a comparação terminar, os resultados serão gerados diretamente no Android Studio, como na captura de tela abaixo:
Observe que, no nosso caso, o tempo de inicialização no Google Pixel 7 apresenta o valor mínimo de 294,8 ms, médio de 301,5 ms e máximo de 314,8 ms. No seu dispositivo, os resultados dessas mesmas comparações podem ser diferentes. Diversos fatores podem afetar os resultados, como:
- A potência do dispositivo.
- A versão do sistema utilizada.
- Apps em execução em segundo plano.
Sendo assim, é importante comparar os resultados gerados em um mesmo dispositivo, preferencialmente em um mesmo estado. Caso contrário, as diferenças observadas podem ser significativas. Caso não seja possível garantir o mesmo estado, é recomendável aumentar o número de iterations
para processar corretamente os resultados outliers.
Para possibilitar a análise, a biblioteca Macrobenchmark registra os rastros do sistema durante a execução das comparações. Para facilitar, o Android Studio marca cada iteração e os tempos medidos como um link para o rastreamento do sistema, o que permite abri-los facilmente para investigar.
9. Exercício opcional: declarar quando o app está pronto para uso
A biblioteca Macrobenchmark pode medir automaticamente o tempo para a renderização do primeiro frame pelo app, com timeToInitialDisplay
. No entanto, é comum que o conteúdo do app não termine de carregar até depois que a renderização do primeiro frame seja concluída, e pode ser que você queira medir quanto tempo o usuário precisa esperar até que o app possa ser utilizado. Esse período é conhecido como tempo para exibição total, ou seja, o tempo que leva para o conteúdo ser carregado totalmente e o usuário poder interagir com o app. A biblioteca Macrobenchmark pode detectar esse tempo automaticamente, mas é necessário programar o app para informar quando isso acontece, usando a função Activity.reportFullyDrawn()
.
No app de exemplo, uma barra de progresso simples é mostrada até que os dados sejam carregados. Nesse caso, é necessário esperar até que os dados estejam prontos e a lista de lanches apareça. Vamos ajustar o app de exemplo e adicionar a chamada a reportFullyDrawn()
.
Abra o arquivo Feed.kt
no pacote .ui.home
no painel Project.
Nesse arquivo, localize a função SnackCollectionList
responsável por criar a lista de lanches.
Confira se os dados estão prontos. Até que o conteúdo esteja pronto, uma lista vazia do parâmetro snackCollections
vai ser exibida. Portanto, você pode usar o elemento combinável ReportDrawnWhen
, que vai cuidar da geração de relatórios assim que o predicado for verdadeiro.
ReportDrawnWhen { snackCollections.isNotEmpty() }
Box(modifier) {
LazyColumn {
// ...
}
Como alternativa, você também pode usar o elemento combinável ReportDrawnAfter{}
, que aceita a função suspend
e espera até que ela seja concluída. Dessa forma, é possível aguardar o carregamento assíncrono de alguns dados ou a conclusão de alguma animação.
Depois disso, é necessário ajustar a ExampleStartupBenchmark
para aguardar o conteúdo. Caso contrário, a comparação termina com o primeiro frame renderizado e pode ignorar a métrica definida.
A comparação de inicialização espera apenas pela renderização do primeiro frame. O tempo de espera é incluído na função startActivityAndWait()
.
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "com.example.macrobenchmark_codelab",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD,
) {
pressHome()
startActivityAndWait()
// TODO wait until content is ready
}
No caso em questão, é possível aguardar até que a lista de conteúdo tenha alguns filhos. Portanto, adicione wait()
ao código, como no snippet abaixo:
@Test
fun startup() = benchmarkRule.measureRepeated(
//...
) {
pressHome()
startActivityAndWait()
val contentList = device.findObject(By.res("snack_list"))
val searchCondition = Until.hasObject(By.res("snack_collection"))
// Wait until a snack collection item within the list is rendered
contentList.wait(searchCondition, 5_000)
}
Para explicar o que acontece nesse snippet:
- Encontramos a lista de lanches com
Modifier.testTag("snack_list")
. - Definimos a condição de pesquisa que estabelece
snack_collection
como o elemento a ser aguardado. - Usamos a função
UiObject2.wait
para aguardar a condição no objeto da interface com tempo limite de cinco segundos.
Agora, você pode executar a comparação de novo. A biblioteca vai medir automaticamente timeToInitialDisplay
e timeToFullDisplay
, como na captura de tela abaixo:
A diferença entre TTID e TTFD, no nosso caso, é de 413 ms. Isso significa que, embora os usuários vejam um primeiro frame renderizado em 319,4 ms, eles precisam esperar mais 413 ms para rolar a lista.
10. Comparação do tempo para a renderização de frames
Depois de acessar o app, a segunda métrica importante para os usuários é a consistência do app, ou seja, se ele pula algum frame. Para medir isso, vamos usar FrameTimingMetric
.
Vamos supor que você quer avaliar o comportamento de rolagem da lista de itens e não quer medir nenhuma outra métrica antes disso. Nesse caso, é necessário dividir a comparação entre interações avaliadas e não avaliadas. Para fazer isso, vamos usar o parâmetro lambda setupBlock
.
Nas interações não avaliadas, definidas em setupBlock
, vamos iniciar uma atividade padrão e, nas interações avaliadas, definidas em measureBlock
, vamos encontrar o elemento da lista de interface, rolar a lista e esperar até que o conteúdo seja renderizado na tela. Se as interações não forem divididas em duas partes, não vai ser possível diferenciar os frames produzidos durante a inicialização do app e os frames produzidos durante a rolagem da lista.
Criar uma comparação do tempo para a renderização de frames
Para alcançar o fluxo mencionado, vamos criar uma nova classe ScrollBenchmarks
com um teste scroll()
, que vai incluir a comparação de tempo para a renderização de frames durante a rolagem. Primeiro, crie a classe de teste com a regra de comparação e um método de teste vazio:
@RunWith(AndroidJUnit4::class)
class ScrollBenchmarks {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun scroll() {
// TODO implement scrolling benchmark
}
}
Em seguida, adicione a estrutura de comparação com os parâmetros obrigatórios.
@Test
fun scroll() {
benchmarkRule.measureRepeated(
packageName = "com.example.macrobenchmark_codelab",
iterations = 5,
metrics = listOf(FrameTimingMetric()),
startupMode = StartupMode.COLD,
setupBlock = {
// TODO Add not measured interactions.
}
) {
// TODO Add interactions to measure list scrolling.
}
}
Essa comparação usa os mesmos parâmetros da comparação de startup
, com a exceção do parâmetro metrics
e o setupBlock
. A FrameTimingMetric
coleta o tempo dos frames produzidos pelo app.
Agora, vamos preencher o setupBlock
. Como mencionado anteriormente, nessa lambda, as interações não são avaliadas pela comparação. Você pode usar esse bloco para abrir o app e aguardar a renderização do primeiro frame.
@Test
fun scroll() {
benchmarkRule.measureRepeated(
packageName = "com.example.macrobenchmark_codelab",
iterations = 5,
metrics = listOf(FrameTimingMetric()),
startupMode = StartupMode.COLD,
setupBlock = {
// Start the default activity, but don't measure the frames yet
pressHome()
startActivityAndWait()
}
) {
// TODO Add interactions to measure list scrolling.
}
}
Agora, vamos programar o measureBlock
, que é o último parâmetro lambda. Primeiramente, como enviar itens para a lista de lanches é uma operação assíncrona, é necessário aguardar até que o conteúdo esteja pronto.
benchmarkRule.measureRepeated(
// ...
) {
val contentList = device.findObject(By.res("snack_list"))
val searchCondition = Until.hasObject(By.res("snack_collection"))
// Wait until a snack collection item within the list is rendered
contentList.wait(searchCondition, 5_000)
// TODO Scroll the list
}
Se preferir, caso você não queira avaliar a configuração inicial do layout, aguarde até que o conteúdo esteja pronto no setupBlock
.
Em seguida, defina limites para os gestos na lista de lanches. Se isso não for feito, o app pode acionar a navegação do sistema e isso pode fechar o app em vez de rolar o conteúdo da lista.
benchmarkRule.measureRepeated(
// ...
) {
val contentList = device.findObject(By.res("snack_list"))
val searchCondition = Until.hasObject(By.res("snack_collection"))
// Wait until a snack collection item within the list is rendered
contentList.wait(searchCondition, 5_000)
// Set gesture margin to avoid triggering system gesture navigation
contentList.setGestureMargin(device.displayWidth / 5)
// TODO Scroll the list
}
Por fim, role a lista usando o gesto fling()
e aguarde até que a interface fique inativa. Também é possível usar scroll()
ou swipe()
, dependendo da velocidade da rolagem.
benchmarkRule.measureRepeated(
// ...
) {
val contentList = device.findObject(By.res("snack_list"))
val searchCondition = Until.hasObject(By.res("snack_collection"))
// Wait until a snack collection item within the list is rendered
contentList.wait(searchCondition, 5_000)
// Set gesture margin to avoid triggering gesture navigation
contentList.setGestureMargin(device.displayWidth / 5)
// Scroll down the list
contentList.fling(Direction.DOWN)
// Wait for the scroll to finish
device.waitForIdle()
}
A biblioteca vai medir o tempo dos frames produzidos pelo app ao realizar as ações definidas.
Agora a comparação está pronta para ser executada.
Executar a comparação
Você pode executar essa comparação da mesma forma que a comparação de inicialização. Clique no ícone de gutter ao lado do teste e selecione Run 'scroll()'.
Caso precise de mais informações sobre como executar a comparação, consulte a etapa Executar a comparação.
Entender os resultados
A FrameTimingMetric
gera resultados de duração de frames em milissegundos (frameDurationCpuMs
) no 50º, 90º, 95º e 99º percentil. No Android 12 (nível 31 da API) e versões mais recentes, ela também retorna um valor correspondente ao tempo excedido pelos frames (frameOverrunMs
). Esse valor pode ser negativo, o que significa que há tempo sobrando para produzir o frame.
De acordo com os resultados, o valor médio (P50) para criar um frame no Google Pixel 7 é 3,8 ms, 6,4 ms abaixo do limite de tempo para a renderização de frames. Além disso, alguns frames podem ter sido pulados no percentil acima de 99 (P99), já que levavam 35,7 ms para serem produzidos, 33,2 ms acima do limite.
Assim como nos resultados referentes à inicialização do app, você pode clicar em iteration
para abrir o rastreamento do sistema registrado durante a comparação e investigar o que contribuiu para os tempos resultantes.
11. Parabéns
Parabéns! Você concluiu este codelab sobre a avaliação de performance de apps usando a biblioteca Jetpack Macrobenchmark.
Qual é a próxima etapa?
Confira o codelab Melhorar a performance do app com perfis de referência. Consulte também nosso repositório de exemplos de performance no GitHub (em inglês), que inclui a biblioteca Macrobenchmark e outros exemplos.