O scan de 8 segundos escondido em cada refresh
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
O scan da biblioteca inteira que eu achava que já tinha corrigido
Um padrão não se corrige de uma vez só. Mais um scan de biblioteca no main actor escondido num caminho de escrita, e a API em lote que só tinha sido conectada numa direção.
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á.
Álbuns em destaque, dados reais do MusicKit, e um teste que mentiu sobre estar passando
Não existe uma flag isEssential - o Essentials que você vê é uma playlist de músicas. featuredAlbums é o sinal real, e uma mudança de fixture expôs um teste que ninguém tinha rodado de novo.
Você também pode achar útil
RackNerd VPS
Hospedagem VPS econômica para serviços leves que funcionam continuamente.
Como afiliado da RackNerd, ganho com compras qualificadas.
Saiba maisNordPass
Gerenciador de senhas da equipe por trás da NordVPN, com um plano gratuito.
Como afiliado da NordPass, ganho com compras qualificadas.
Saiba maisProton 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