18 Set, 2009 11:46

Performance do SQLite Persistent Object

Minha primeira idéia foi aproveitar a chegada do iPhone OS 3.0 e migrar do SPO para Core Data imaginando que, por ser uma solução desenvolvida especificamente para a plataforma, seria bem mais eficiente. Contudo, após estudo da API, vi que a mudança seria um pouco mais complicada, pois ela usa um paradigma diferente da anterior e eu teria que mexer muito além das classes de modelo, possivelmente mexendo em todas as outras classes, controladores principalmente. Embora tenha coberto as classes de modelo com testes unitários, achei que seria arriscado demais fazer a mudança estando tão próximo da entrada do sistema em produção. Decidi deixar essa idéia na gaveta e parti para tentar melhorar o que já estava implementado e em funcionamento.

Avaliando o tempo de cada atividade

Não faz sentido buscar melhorias na performance se você não tiver como saber se as modificações estão realmente influenciando no tempo de cada atividade. Para avaliar isso, existem algumas ferramentas disponíveis no Mac OS, como o Instruments e o Shark. Utilizando a segunda, consegui ter uma idéia clara de quais métodos estavam consumindo mais tempo da CPU através da análise de Time Profile.

Mas para medir o tempo de uma atividade completa - que atravessa vários métodos, threads, callbacks - eu desenvolvi uma classe simples, porém eficaz, para calcular esses tempos. Uma classe que batizei de Benchmark e que pode ser usada da seguinte maneira:

- baixarLista {
    [Benchmark start:@"baixarLista"];
    ....
}

- exibirLista {
    .....
    [Benchmark finish:@"baixarLista"];
}

A saída seria algo parecido com:

2009-09-17 13:52:49.729 MyAPP[876:20b] BENCHMARK: baixarLista started
2009-09-17 13:52:56.507 MyAPP[876:6b07] BENCHMARK: baixarLista: 6.777050s

As vantagens desta abordagem são:

  1. o código é pouco invasivo, pois você pode dar o import do Benchmark no .pch e usar em todas as classes;
  2. permite o uso de labels para identificar o benchmark, o que permite realizar vários benchmarks ao mesmo tempo sem se confundir;
  3. por utilizar somente métodos estáticos, permite que um benchmark comece num método de uma classe e termine no de outra classe sem que seja necessário o compartilhamento de dados entre as classes.

O código completo da classe você confere aqui.

Identificando os gargalos

Depois de levantar o tempo gasto e os métodos mais custosos durante a execução de cada atividade, pude me concentrar em encontrar os pontos considerados gargalos e que eu poderia fazer alguma mudança.

Datas

O SPO armazena os campos de data como strings no banco e todas as operações de recuperação ou gravação passam por métodos de conversão definidos em NSDate-SQLitePersistence.m. Analisando a saída do Shark, observei que estas conversões estavam tomando muito tempo de processamento e, para minha surpresa, a maior parte do tempo não era gasta com a conversão em si, mas com a instanciação da classe NSDateFormatter. Para solucionar o problema, bastou declarar o formatador como estático e instanciá-lo somente uma vez, criando uma espécie de cache. Esta modificação já surtiu um grande efeito, pois a atividade que estava sendo trabalhada envolvia, de uma só vez, o download, gravação e exibição de uma lista de entidades que possuíam 3 campos de data cada uma.

Lista dinâmica de propriedades

Dentre vários métodos do SPO, são feitas chamadas ao método propertiesWithEncodedTypes das entidades para obter um dicionário com todas suas propriedades e seus respectivos tipos. Esse método é relativamente custoso, pois esse dicionário é montado toda vez que o método é solicitado e, a cada busca por uma lista de entidades, podem ser feitas dezenas - ou centenas - de chamadas a este método.

Mais uma vez, a solução foi simples: cache. Depois da primeira chamada do método, o dicionário fica gravado numa variável estática que é consultada nas chamadas subsequentes. Resultado: mais um salto na performance.

Strings

As propriedades das entidades em Objective C são declaradas em camelCase, enquanto as respectivas colunas do banco seguem uma notação_com_underscores e para fazer essa conversão, o SPO define dois métodos em NSString-SQLiteColumnName.m. Como nos casos anteriores, esses métodos são muito solicitados durante as atividades, mas eu consegui fazer uma estatística que mostrava em em mais de 80% dos casos a conversão era desnecessária, pois a propriedade tinha a mesma notação tanto no código quanto na base. A mudança neste caso foi simplesmente verificar antes se o nome já estava no formato certo e retorná-lo imediatamente. Outro salto na performance.

Conclusão

Com as mudanças acima e utilizando a classe de benchmark, conseguir registrar melhoras de mais de 60% em diversas atividades do aplicativo. Registrei quedas, por exemplo, de 11 para 4 segundos, outras de 9 para 3.

Fiquei muito satisfeito com o resultado, principalmente por não ter que encarar uma mudança profunda no mecanismo de armazenamento do aplicativo tão próximo da entrada em produção.

Se você também utiliza o SQLite Persistent Objects na sua app, pode baixar o patch aqui.

Ao navegar neste site, você consente o uso de cookies nossos e de terceiros, que coletam informações anônimas e são essenciais para melhorar sua experiência em nosso site.