Pular para o conteúdo
Development

O scan de 8 segundos escondido em cada refresh

Por Victor Da Luz
iosswiftperformancemusickitdev-logdeep-cut-atlas

O Deep Cut Atlas tem duas abas principais. As duas às vezes demoravam uma eternidade para carregar, tempo suficiente para eu trocar de app enquanto esperava. Essa semana rodei uma revisão de performance para descobrir o motivo.

O problema

O app rastreia quais álbuns você já tem para mostrar só os lançamentos novos. Para isso ele escaneia sua biblioteca inteira do Apple Music e monta um conjunto de chaves de álbum normalizadas. Eu sabia que o scan não era de graça. O que eu não sabia era quanto ele custava de verdade numa biblioteca real, ou com que frequência ele rodava.

Então eu medi. Coloquei um diagnóstico temporário no app atrás de um argumento de launch, compilei para o meu celular, e iniciei pelo terminal capturando o console:

xcrun devicectl device process launch --console --terminate-existing \
  <device-id> com.example.app --timing-diag

Os números na minha biblioteca (1.610 artistas, 5.054 álbuns):

fetchLibraryArtists    cold  533ms
fetchLibraryAlbumKeys  cold 8215ms
fetchPlaylistContents         73ms

Oito segundos. E a revisão de código encontrou a aba Discover rodando esse scan em cada lote: cada cold start, cada pull-to-refresh, cada retorno do background com dados obsoletos. A aba Playlist tinha aprendido essa lição meses atrás e cacheava o scan por sessão. A aba Discover nunca recebeu o recado.

A correção

Mover o cache para dentro do serviço compartilhado, para que toda aba se beneficie e o scan rode uma vez por sessão:

private var cachedLibraryAlbumKeys: Set<String>?
private var libraryAlbumKeysScan: Task<Set<String>, Error>?

func fetchLibraryAlbumKeys() async throws -> Set<String> {
    if let cachedLibraryAlbumKeys { return cachedLibraryAlbumKeys }
    let scan = libraryAlbumKeysScan ?? Task.detached(priority: .utility) { /* scan */ }
    libraryAlbumKeysScan = scan
    do {
        let keys = try await scan.value
        cachedLibraryAlbumKeys = keys
        libraryAlbumKeysScan = nil
        return keys
    } catch {
        if libraryAlbumKeysScan == scan { libraryAlbumKeysScan = nil }
        throw error
    }
}

A Task armazenada importa tanto quanto o valor armazenado. No launch do app, duas abas pedem o scan com milissegundos de diferença uma da outra. Só com um cache de valor, as duas dariam miss e as duas escaneariam. Com a task cacheada, o segundo chamador espera o scan do primeiro chamador. E em caso de falha a task é limpa para que a próxima chamada tente de novo, em vez de repetir um erro cacheado para sempre. Task é Equatable no Swift, o que permite que o caminho de falha limpe só a própria task em vez de atropelar um retry que outra chamada já tinha começado.

Mais dois ganhos saíram da mesma revisão. A busca de catálogo por artista estava fazendo duas requests por artista: uma para resolver o artista de novo por id, outra para carregar seus álbuns. Os objetos de artista do scan da sessão podem ser reaproveitados diretamente, então agora é uma request só. E a aba playlist estava buscando o conteúdo de novo a cada troca de aba. Agora ela checa um contador de versão de escrita no serviço, então só busca de novo quando algo realmente mudou. Uma janela de 5 minutos de obsolescência pega edições feitas no próprio app Music.

A armadilha de medição

Minha primeira comparação de tempo para a busca de catálogo disse que o caminho antigo era 16x mais rápido que o meu caminho novo. Isso teria sido um dia ruim, só que era bobagem: as duas passadas buscaram os mesmos cinco artistas, e a segunda passada foi servida pelo cache HTTP do MusicKit. Dez requests de rede em 61ms não é algo que o meu wifi consegue fazer.

Rodando de novo com amostras de artistas disjuntas, e medindo o caminho antigo primeiro para enviesar contra a minha mudança, deu o resultado honesto: 231ms por artista antes, 153ms depois.

Resultados

O cold start do Discover foi de aproximadamente 18 segundos de trabalho para cerca de 6, e um pull-to-refresh morno não paga mais o scan de 8 segundos. As trocas de aba do Playlist são instantâneas a menos que algo realmente tenha mudado. Tudo confirmado no dispositivo, já que o MusicKit não retorna nada no simulador.

A lição que eu continuo reaprendendo: meça no dispositivo com dados reais antes e depois. O scan da biblioteca “parecia lento” por semanas, mas bastou um número impresso para ver que eram 8 segundos e rodava com muito mais frequência do que precisava.

Leitura relacionada

Development

A playlist que já tinha o nome certo

Um artefato de renomeação, uma API sem campo de autor pra atualizar, e um teste em dispositivo real provando que o bug já tinha se corrigido sozinho, fechado como aceitar como está.

Ler

Você também pode achar útil

RackNerd

RackNerd VPS

Hospedagem VPS econômica para serviços leves que funcionam continuamente.

Como afiliado da RackNerd, ganho com compras qualificadas.

Saiba mais
NordPass

NordPass

Gerenciador de senhas da equipe por trás da NordVPN, com um plano gratuito.

Como afiliado da NordPass, ganho com compras qualificadas.

Saiba mais
Proton

Proton Pass

Gerenciador de senhas focado em privacidade, da equipe por trás do Proton Mail.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais