Como Aprimoramos a Incialização Dos Nossos Apps Android Em Até 24%

A Engenharia de performance pode ser tão complexa quanto cozinhar. A diferença é que cozinhar quase sempre tem uma receita a seguir.

Victor Oliveira
Mercado Libre Tech

--

Você tambêm pode ler este artigo em inglês aqui!

Introdução

Você já tentou cozinhar? A Le Cordon Bleu como uma escola de artes culinárias foi fundada em Paris (1895) pela jornalista e editora da revista La Cuisinière Cordon Bleu, Marthe Distel. Esta escola de culinária é considerada uma das melhores do mundo. Mesmo que muitos queiram se formar lá para dominar técnicas extremamente difíceis, se tornarem grandes Chefs e fazer conquistas extraordinárias, a maioria deles provavelmente começou de forma pequena. Por exemplo, diariamente em casa, eles podem ter cozinhado pelo menos uma vez sem seguir as receitas. Ao desenvolverem o gosto pela prática culinária, percebem que há muitos pratos complexos que não podem ser preparados sem o compromisso de seguir etapas rigorosas, desde a temperatura do forno até os ingredientes que são pesados em uma balança de alta precisão. Sendo assim, cozinhar pode ser tão difícil quanto engenharia de performance. Isso significa que podemos nos ater a algumas diretrizes mais genéricas, mas não há receitas específicas a seguir.

Contexto

Aqui no Mercado Livre, performance é algo muito importante, por isso estamos sempre tentando melhorá-la. Nos últimos meses, parte do nosso time de Performance tem focado esforços em reduzir o tempo de inicialização dos nossos aplicativos Fintech (também conhecido como Mercado Pago) e Marketplace (também conhecido como Mercado Livre). Durante essa jornada, enfrentamos muitos desafios e é isso que gostaria de compartilhar com vocês hoje.

Quando falamos de questões de desempenho, primeiro precisamos ter métricas para entender qual é a situação atual. Infelizmente, quando o artigo foi escrito, não estávamos suportando o Android Gradle Plugin 7, então não havia condições de utilizar o Macrobenchmark do Google para triagem de problemas de desempenho. Ainda assim, nossa demanda inicial não podia esperar e tivemos que improvisar. O Macrobenchmark foi apenas uma entre outras ferramentas que tivemos que avaliar para entender o que era viável para atingirmos nosso objetivo. O Baseline Profiles, por exemplo, que afirma melhorar o tempo de inicialização em até 40%, também não era uma opção, pois o Macrobenchmark é necessário para sua implementação. Para nós, analisar o desempenho tinha que significar compreender totalmente o que nossa inicialização estava fazendo nos bastidores. O Android Studio Profiler e Perfetto nos ajudaram com essa avaliação.

Problemas

o Mercado Livre é conhecido por democratizar o comércio eletrônico na América Latina. De acordo com o Google Play Console, no momento em que este artigo foi escrito, cerca de 19% de nossos usuários não tinham dispositivos Android de última geração, mas também esperavam desfrutar de uma inicialização mais rápida. Portanto, nosso principal critério ao longo dessa jornada foi fazer o benchmark da API do Android mais baixa possível. O objetivo principal era reduzir a inicialização a frio nesses dispositivos, assumindo que nossa refatoração de algoritmo impactaria positivamente os usuários empoderando seus dispositivos.

Do ponto de vista da implementação, analisamos o que poderia fazer sentido desenvolver tentando equalizar o maior custo-benefício entre aumento de desempenho e Time To Market (TTM), então agimos sobre cinco tópicos:

  1. Iterações excessivas sobre listas
  2. Configurações não críticas na inicialização
  3. Muitos lançamentos de escopo de corrotinas
  4. Pouca utilização de threads em cache
  5. Rastreamento de inicialização antecipada

Iterações excessivas sobre listas

A notação Big O é bastante conhecida por avaliar o desempenho de algoritmos. Essa notação nos permite afirmar que, de maneira simples, temos uma performance de inicialização linear ou, em outras palavras, um algoritmo O(n). O tempo de inicialização de nossos aplicativos é diretamente proporcional ao número de configurações que precisamos fazer antes de permitir a entrada dos usuários.

A principal diferença com nosso algoritmo anterior é que não acumulamos mais muitas tarefas antes de executá-las. Isso costumava ser necessário para que elas pudessem ser classificadas a fim de evitar erros na inicialização do aplicativo, pois algumas dessas tarefas dependiam do estado de outras para execução adequada.

/**
*
* Old algorithm example of a high-order function
* being stored for later execution.
*
**/
class ConfiguratorManager {

internal val configurables = mutableListOf<ConfiguratorData<*>>()

infix fun <T : Configurable> ConfiguratorManager.configure(
configurable: () -> T
) = ConfiguratorDefaultData(configurable) .also {
configurables += it
}
}

Configurações não críticas na inicialização

Normalmente não há necessidade de criar uma subclasse da Application. Quando necessário, a implementação deve ser o mais rápida possível. Ainda assim, com uma base de código grande e inúmeras equipes criando experiências para os usuários no mesmo aplicativo, é muito comum perder essa essência. Como nossa tela inicial não está otimizada no momento, somos forçados a bloquear a execução da thread principal para garantir que qualquer configuração essencial seja feita antes de iniciar o aplicativo.

Está em nosso backlog aproveitar ao máximo a tela inicial dos apps. Com isso em mente, para ajudar em uma migração futura e aumentar o desempenho do aplicativo imediatamente, removemos várias configurações de recursos não críticos de serem executadas na thread principal. Conseguimos até ajustar alguns deles para que pudessem ser entregues dinamicamente.

Muitos lançamentos de escopo de corrotinas

Corrotinas é um poderoso padrão de design de simultaneidade usado para simplificar o código executado de forma assíncrona. Cada tarefa em segundo plano processada costumava ser feita criando um novo escopo corrotina e iniciando-o imediatamente. Embora a execução paralela de tarefas de inicialização seja geralmente considerada uma boa prática, percebemos que em nosso cenário era melhor diminuir o uso dessa prática.

A criação de corrotinas aqui significa declará-la e inicializá-la. Durante nossa Prova de Conceito, o custo dessa operação foi praticamente irrelevante. Por outro lado, iniciar escopos de corrotina pode ser uma tarefa muito cara para a inicialização de um aplicativo. Em nosso cenário, observamos que a criação e o lançamento de um único escopo de Coroutine — evitando assim o paralelismo e aplicando apenas uma execução assíncrona — poderia tornar a execução desse código até 9% mais rápida.

/**
*
* Old algorithm responsible for creating
* a parallel execution.
*
**/
internal class ConfiguratorEnqueueImpl(
private val coroutineScope: ConfiguratorCoroutineScope
) : ConfiguratorEnqueue {

private val scope: CoroutineScope by lazy {
coroutineScope.newScope()
}

private val jobs = mutableListOf<Job>()

override fun launch(
dispatcher: CoroutineDispatcher,
block: suspend CoroutineScope.() -> Unit
) {
scope.launch(dispatcher, block = block).also { jobs += it }
}

override suspend fun enqueue() { jobs.joinAll() }

}

Pouca utilização de threads em cache

Perfetto UI é uma excelente ferramenta de desempenho frequentemente apresentada pelos engenheiros do Google para realizar benchmark de aplicativos Android. Também pode ser usado através de uma linha de comando. Uma amostra de rastreamento no Perfetto exibe informações sobre kernel, uso de memória e distintos serviços executando em um dispositivo. Ao fazer o benchmarking de inicialização a frio, começamos a nos perguntar como poderíamos evitar que outros aplicativos em segundo plano fossem alocados em um núcleo de processamento durante a configuração inicial dos nossos aplicativos.

O contexto de corrotinas inclui um CoroutineDispatcher para especificar qual(is) thread(s) a corrotina correspondente usará para sua execução. Ao processar uma configuração assíncrona, o Mercado Pago e o Mercado Livre agora fazem uso de uma implementação personalizada que, por meio de um ExecutorService, armazena threads em cache dentro de um pool. Esses threads são definidas como prioridade máxima de forma que o dispositivo possa realizar qualquer coisa relacionada ao nosso processo de inicialização o mais rápido possível.

Rastreamento de inicialização antecipada

Como afirmado anteriormente, nossa inicialização tem mais tarefas do que de fato gostaríamos. Algumas delas são executadas em segundo plano. Mesmo sendo processada de forma assíncrona, uma tarefa precisa de um núcleo para ser alocada.

Esse contexto inicial é importante para entender por que adiamos a exportação do rastreamento de inicialização. Costumávamos processar o rastreamento durante a execução de tarefas; agora enfileiramos eventos em um buffer para exportá-los posteriormente, garantindo que não haja desperdício de recursos. Nada além de tarefas críticas de inicialização são processadas, seja na thread principal ou não. Cinco segundos é o período que escolhemos para adiar a exportação, pois representa a grande maioria (p90) do tempo de inicialização de nossos usuários de acordo com o Google Play Console.

Resultados

Os resultados coletados por Mercado Pago e Mercado Livre foram um pouco semelhantes, embora tenham escalado de maneira diferente. O primeiro aplicativo teve um aumento de desempenho melhor do que o segundo. Isso pode ser atribuído à diferença de tarefas que um tem em relação ao outro, não apenas de forma quantitativa — é importante ter em mente que suas implementações podem divergir totalmente. Para melhor ilustrar aqui o que isso significa: Mercado Pago e Mercado Livre contam com a configuração do módulo de Notificações como uma tarefa vital durante a inicialização. Ainda assim, o algoritmo do Mercado Livre não é igual ao do Mercado Pago e carrega mais rápido.

Na tentativa de igualar nossa base de instalação, os benchmarks aqui apresentados foram feitos com dois chipsets completamente diferentes rodando Android APIs 24 e 29. A partir das melhorias anteriores apresentadas em relação ao nosso novo algoritmo — que representa uma fração de toda a duração da inicialização — Mercado Pago apresentou uma faixa de aumento de desempenho de 23% a 58%, enquanto o Mercado Livre permaneceu em 17% em ambas as APIs. O benchmark geral do tempo de inicialização a frio no Mercado Livre apresentou uma melhora de até 5%, embora o Mercado Pago tenha melhorado cerca de 24%.

Comparação dos dispositivos e sua melhoria máxima de desempenho durante o benchmark em relação ao tempo geral de inicialização

As amostras de produção seguiram o que havíamos comprovado internamente. Em comparação com três meses antes, o tempo de inicialização a frio do p90 do Mercado Pago melhorou em até 24%, de acordo com o Firebase. Nesse mesmo intervalo, o Mercado Livre foi aprimorado em até 16%.

Tempo de inicialização do Mercado Livre e Mercado Pago apresentado pelo Firebase em um comparativo com os últimos 3 meses

Sessões de inicialização a frio não conformes no Mercado Pago foram reduzidas pela metade na comparação de um período de sete dias. A métrica de 30 dias diminuiu 29%. O Mercado Livre também minimizou o tempo de inicialização em aproximadamente 7% e 6%, respectivamente.

Percentual de sessões em dissonância com o tempo de incialização ideal de acordo com a Play Store

Convertendo essas métricas em quantidades, o Mercado Pago agora tem mais de 1,5 milhão de usuários por dia inicializando nossos aplicativos em menos de 5 segundos. O mesmo se aplica ao Mercado Livre, de fato atingindo 500 mil inicializações dentro desse tempo ideal.

Melhoria de performance alcançada no Mercado Livre e Mercado Pago

Conclusão

Sabemos que ainda há muito a ser feito pela performance dos nossos aplicativos, especialmente em termos de inicialização. Desde a remoção de implementações desnecessárias até a otimização da tela inicial, ainda temos margem para melhorias. Elas certamente são mais difíceis de alcançar, pois exigem especulação sobre o que exatamente atrapalha o aplicativo, compreensão total da execução dos algoritmos e criatividade para contornar esses problemas.

Também devemos dizer que estamos muito empolgados em ver como o Baseline Profiles pode melhorar ainda mais nosso desempenho de inicialização porque, seja aproveitando as bibliotecas do Google ou seguindo em frente com nossas próprias ideias, o mais importante para nós é oferecer a melhor experiência de usuário para nossos clientes.

E você? Tem alguma experiência pessoal ou profissional sobre performance que gostaria de compartilhar? Conte-nos mais sobre nos comentários abaixo e fique atento ao nosso blog para saber como como continuamos melhorando a performance de nossos aplicativos móveis!

Referências Medium

  1. Le Cordon Bleu Story
  2. Top 5 Culinary Schools In The World
  3. Android Vitals — Profiling App Startup
  4. App Startup Time
  5. Time To Market
  6. Ace Your Coding Interview By Understanding Big O Notation

--

--