Otimizar downloads para um acesso de rede eficiente

Usar o rádio sem fio para transferir dados é, possivelmente, uma das fontes mais significativas de consumo de bateria do seu app. Para minimizar o consumo de bateria associado à atividade de rede, é essencial entender como seu modelo de conectividade afetará o hardware de rádio subjacente.

Esta lição apresenta a máquina de estado de rádio sem fio e explica como o modelo de conectividade do seu app interage com ela. Em seguida, propõe maneiras de minimizar suas conexões de dados, usar a pré-busca e agrupar suas transferências para minimizar o consumo de bateria associado às suas transferências de dados.

A máquina de estado de rádio

Um rádio sem fio totalmente ativo consome uma quantidade significativa de energia, então ele transita entre diferentes estados de energia para economizá-la quando não está em uso, ao mesmo tempo em que tenta minimizar a latência associada ao carregamento do rádio quando é necessário.

A máquina de estado para um rádio de rede 3G geralmente consiste em três estados de energia:

  1. Potência total: usada quando uma conexão está ativa, permitindo que o dispositivo transfira dados na taxa mais alta possível.
  2. Baixa potência: um estado intermediário que usa cerca de 50% da bateria no estado completo.
  3. Em espera: o estado de energia mínima durante o qual nenhuma conexão de rede fica ativa ou é necessária.

Ao mesmo tempo em que os estados de baixa potência e em espera consomem significativamente menos bateria, eles também introduzem uma latência considerável nas solicitações de rede. Retornar à potência total a partir do estado baixo leva cerca de 1,5 segundo, enquanto a mudança do modo de espera para a potência total pode levar mais de 2 segundos.

Para minimizar a latência, a máquina de estado usa um atraso para adiar a transição para estados de energia mais baixos. A Figura 1 usa os horários da AT&T para um rádio 3G típico.

Figura 1. Máquina de estado de rádio sem fio 3G típica.

A máquina de estado de rádio em cada dispositivo, em particular o atraso de transição associado ("tempo de cauda") e a latência de inicialização, varia com base na tecnologia de rádio sem fio empregada (2G, 3G, LTE etc.) e é definida e configurada pela operadora de rede com a qual o dispositivo opera.

Esta lição descreve uma máquina de estado representativa para um rádio sem fio 3G típico, com base nos dados fornecidos pela AT&T. No entanto, os princípios gerais e as práticas recomendadas resultantes são aplicáveis a todas as implementações de rádio sem fio.

Essa abordagem é particularmente eficaz para a navegação na Web comum, porque impede a latência indesejada enquanto os usuários navegam. O tempo de cauda relativamente baixo também garante que, quando uma sessão de navegação é concluída, o rádio passe para um estado de energia mais baixo.

Infelizmente, essa abordagem pode levar a apps ineficientes em sistemas operacionais modernos de smartphones, como o Android, em que os apps são executados em primeiro plano (em que a latência é importante) e em segundo plano (em que a duração da bateria deve ser priorizada).

Como os apps afetam a máquina de estado de rádio

Cada vez que você cria uma nova conexão de rede, o rádio transita para o estado de potência total. No caso da máquina de estado de rádio 3G típica descrita acima, ela permanecerá em potência total durante a transferência, mais 5 segundos adicionais de tempo de cauda, seguidos por 12 segundos no estado de baixa energia. Assim, para um dispositivo 3G típico, toda sessão de transferência de dados fará com que o rádio consuma energia por quase 20 segundos.

Na prática, isso significa que um app que transfere dados não agrupados por 1 segundo a cada 18 segundos manterá o rádio sem fio sempre ativo, movendo-o de volta para a alta potência exatamente quando estava prestes a entrar no modo de espera. Como resultado, cada minuto consumirá bateria no estado de alta potência por 18 segundos e no estado de baixa potência nos 42 segundos restantes.

Em comparação, o mesmo app que agrupa transferências de 3 segundos a cada minuto manterá o rádio no estado de alta potência por apenas 8 segundos e no estado de baixa potência por apenas mais 12 segundos.

O segundo exemplo permite que o rádio fique em espera por mais 40 segundos a cada minuto, resultando em uma redução massiva no consumo de bateria.

Figura 2. Uso relativo de energia em rádio sem fio para transferências agrupadas versus desagrupadas.

Fazer pré-busca de dados

A pré-busca de dados é uma maneira eficaz de reduzir o número de sessões independentes de transferência de dados. A pré-busca permite que você faça o download de todos os dados que provavelmente serão necessários para determinado período em uma única sequência, em uma única conexão, com capacidade total.

Ao carregar suas transferências, você reduz o número de ativações de rádio necessárias para fazer o download de dados. Como resultado, você não apenas conserva a duração da bateria, como também melhora a latência, diminui a largura de banda necessária e reduz os tempos de download.

A pré-busca também oferece uma experiência do usuário aprimorada, minimizando a latência no app causada pela espera pela conclusão dos downloads antes de executar uma ação ou ver os dados.

No entanto, se usada de forma muito agressiva, a pré-busca apresenta o risco de aumentar o consumo de bateria e o uso de largura de banda, bem como da cota de download, ao fazer o download de dados que não são usados. Também é importante garantir que a pré-busca não atrase a inicialização do app enquanto ele aguarda o término da pré-busca. Em termos práticos, isso pode significar o processamento progressivo de dados ou a priorização de transferências consecutivas, de modo que os dados necessários para a inicialização do app sejam transferidos e processados primeiro.

O nível de agressividade para fazer a pré-busca depende do tamanho dos dados a serem transferidos e da probabilidade de eles serem usados. Como orientação geral, baseada na máquina de estado descrita acima, para dados que têm 50% de chance de serem usados na sessão atual do usuário, você normalmente realiza a pré-busca por cerca de 6 segundos (aproximadamente 1 a 2 Mb) antes que o custo potencial de fazer o download de dados não utilizados corresponda à possível economia de não fazer.

De modo geral, uma prática recomendada é pré-buscar dados de maneira que você só precise iniciar outro download a cada 2 a 5 minutos e na ordem de 1 a 5 megabytes.

Seguindo esse princípio, os downloads grandes (como de arquivos de vídeo) precisam ser feitos em blocos em intervalos regulares (a cada 2 a 5 minutos), realizando a pré-busca apenas dos dados de vídeo que provavelmente serão visualizados nos próximos minutos.

Observe que outros downloads precisam ser agrupados, conforme descrito na próxima seção, Transferências e conexões em lote, e que essas aproximações variam de acordo com o tipo e a velocidade da conexão, conforme discutido em Modificar padrões de download com base no tipo de conectividade.

Vejamos alguns exemplos práticos:

Um player de música

Você pode pré-buscar um álbum inteiro, mas se o usuário parar de ouvi-lo após a primeira música, você terá desperdiçado uma quantidade significativa de largura de banda e duração da bateria.

Uma abordagem melhor seria manter um buffer de uma música além daquela que está sendo tocada. Para fazer streaming de música, em vez de manter um stream contínuo que deixe o rádio ativo o tempo todo, recomendamos o uso do HTTP Live Streaming para transmitir o stream de áudio em sequências, simulando a abordagem de pré-busca descrita acima.

Um leitor de notícias

Muitos apps de notícias tentam reduzir a largura de banda fazendo o download de manchetes somente depois que uma categoria é selecionada, de artigos completos somente quando o usuário quer lê-los e de miniaturas assim que elas são exibidas na tela.

Usando essa abordagem, o rádio será forçado a permanecer ativo para a maioria das sessões de leitura de notícias dos usuários enquanto eles rolarem pelas manchetes, trocarem de categoria e lerem artigos. Além disso, a troca constante de estados de energia resultará em latência significativa ao trocar de categoria ou ler artigos.

Uma abordagem melhor seria fazer a pré-busca de uma quantidade razoável de dados na inicialização, começando pelo primeiro conjunto de manchetes e miniaturas, o que garante um tempo de inicialização de baixa latência, e continuando com as manchetes e miniaturas restantes, bem como o texto de cada artigo disponível pelo menos a partir da lista principal de manchetes.

Outra alternativa é fazer a pré-busca de todas as manchetes, miniaturas, textos de artigos e possivelmente até fotos de artigos completos, normalmente em segundo plano em um horário predeterminado. Essa abordagem corre o risco de consumir uma quantidade significativa de largura de banda e duração da bateria para fazer o download de conteúdo que nunca será usado, então implemente-a com cuidado.

Uma solução é programar que o download completo ocorra somente quando o dispositivo estiver conectado ao Wi-Fi e, possivelmente, somente quando estiver carregando. Isso é investigado com mais detalhes em Modificar padrões de download com base no tipo de conectividade.

Transferências e conexões em lote

Toda vez que você inicia uma conexão, independentemente do tamanho da transferência de dados associada, você pode fazer com que o rádio consuma energia por quase 20 segundos ao usar um rádio sem fio 3G típico.

Um app que dá um ping no servidor a cada 20 segundos, apenas para confirmar que o app está em execução e visível para o usuário, mantém o rádio ligado indefinidamente, resultando em um custo de bateria significativo para quase nenhuma transferência de dados real.

Com isso em mente, é importante agrupar suas transferências de dados e criar uma fila de transferências pendentes. Se feito corretamente, você pode fazer transferências com comutação de fase que estão prestes a ocorrer dentro de uma janela de tempo similar, para que todas elas aconteçam simultaneamente, garantindo que o rádio consuma energia pelo tempo mais curto possível.

A filosofia por trás dessa abordagem é transferir o máximo de dados possível durante cada sessão de transferência, em um esforço para limitar o número de sessões necessárias.

Isso significa que você precisa agrupar suas transferências enfileirando transferências tolerantes a atrasos e antecipando atualizações e pré-buscas programadas, para que sejam todas executadas quando forem necessárias transferências sensíveis ao tempo. Da mesma forma, as atualizações programadas e a pré-busca regular precisam iniciar a execução da fila de transferências pendentes.

Para ver um exemplo prático, vamos retornar aos exemplos anteriores de Fazer pré-busca de dados.

Considere um app de notícias que usa a rotina de pré-busca descrita acima. O leitor de notícias coleta informações analíticas para entender os padrões de leitura dos usuários e classificar as matérias mais acessadas. Para manter as notícias atualizadas, ele verifica a cada hora se há atualizações. Para conservar a largura de banda, em vez de fazer o download de fotos completas para cada artigo, ele pré-busca apenas miniaturas e faz o download das fotos completas quando elas são selecionadas.

Neste exemplo, todas as informações de análise coletadas no app precisam ser agrupadas e enfileiradas para download, em vez de serem transmitidas conforme são coletadas. O pacote resultante precisa ser transferido quando uma foto em tamanho original estiver sendo baixada ou quando uma atualização por hora estiver sendo realizada.

Qualquer transferência sensível ao tempo ou sob demanda, como o download de uma imagem em tamanho original, precisa antecipar as atualizações agendadas regularmente. É necessário que a atualização planejada seja executada ao mesmo tempo que a transferência sob demanda, com a próxima atualização agendada para ocorrer após o intervalo definido. Essa abordagem reduz o custo de realizar uma atualização regular ao se apoiar no download de fotos necessário e sensível ao tempo.

Reduzir conexões

Geralmente, é mais eficiente reutilizar as conexões de rede existentes do que iniciar novas. A reutilização de conexões também permite que a rede reaja de forma mais inteligente ao congestionamento e a problemas de dados de rede relacionados.

Em vez de criar várias conexões simultâneas para fazer o download de dados ou encadear várias solicitações GET consecutivas, sempre que possível você deve agrupar essas solicitações em um único GET.

Por exemplo, seria mais eficiente fazer uma única solicitação para cada artigo de notícia a ser retornado em uma única solicitação/resposta do que fazer várias consultas para várias categorias de notícias. O rádio sem fio precisa ficar ativo para transmitir os pacotes de término/confirmação de término associados ao tempo limite do servidor e do cliente, portanto, também é recomendável fechar suas conexões quando elas não estiverem em uso, em vez de esperar por esses tempos limite.

Dito isso, fechar uma conexão muito cedo pode impedir que ela seja reutilizada, o que exige uma sobrecarga adicional para estabelecer uma nova conexão. Um comprometimento útil é não fechar a conexão imediatamente, mas antes que o tempo limite inerente expire.

Identificar problemas com o Network Profiler

Use o Network Profiler para acompanhar quando seu app faz solicitações de rede. Você pode monitorar como e quando seu app transfere dados e otimiza o código subjacente adequadamente.

A Figura 3 mostra um padrão de transferência de pequenas quantidades de dados com aproximadamente 15 segundos de intervalo, sugerindo que a eficiência pode ser drasticamente aprimorada ao pré-buscar cada solicitação ou agrupar os uploads.

Figura 3. Acompanhamento do uso da rede.

Ao monitorar a frequência das suas transferências de dados e a quantidade de dados transferidos durante cada conexão, você pode identificar áreas do app que podem ser mais eficientes em termos de bateria. Geralmente, você precisa procurar picos curtos que possam ser atrasados ou que façam com que uma transferência posterior seja antecipada.

Para identificar melhor a causa dos picos de transferência, a API Traffic Stats permite marcar as transferências de dados que ocorrem em uma linha de execução usando o método TrafficStats.setThreadStatsTag(), seguido pela marcação e desmarcação manual de soquetes individuais usando TrafficStats.tagSocket() e TrafficStats.untagSocket(). Exemplo:

Kotlin

    TrafficStats.setThreadStatsTag(0xF00D)
    TrafficStats.tagSocket(outputSocket)
    // Transfer data using socket
    TrafficStats.untagSocket(outputSocket)
    

Java

    TrafficStats.setThreadStatsTag(0xF00D);
    TrafficStats.tagSocket(outputSocket);
    // Transfer data using socket
    TrafficStats.untagSocket(outputSocket);
    

A biblioteca HttpURLConnection marca automaticamente os soquetes com base no valor atual de TrafficStats.getThreadStatsTag(). A biblioteca também marca e desmarca os soquetes quando é reciclada por meio de pools com sinal de atividade.

Kotlin

    private class IdentifyTransferSpikeTask : AsyncTask<String, Nothing, String>() {

        override fun onPreExecute() = TrafficStats.setThreadStatsTag(0xF00D)

        override fun doInBackground(vararg urls: String): String {
            try {
                // Make network request using HttpURLConnection.connect()
            }
        }

        override fun onPostExecute(result: String) = TrafficStats.clearThreadStatsTag()
    }
    

Java

    private class IdentifyTransferSpikeTask extends AsyncTask<String, Void, String> {
        @Override
        protected void onPreExecute() {
          TrafficStats.setThreadStatsTag(0xF00D);
        }

        @Override
        protected String doInBackground(String... urls) {
            try {
                // Make network request using HttpURLConnection.connect()
            }
        }

        @Override
        protected void onPostExecute(String result) {
            TrafficStats.clearThreadStatsTag();
       }
    }
    

A marcação de soquete é compatível com o Android 4.0, mas as estatísticas em tempo real só são exibidas em dispositivos com o Android 4.0.3 ou posterior.