Sunday, August 2, 2009

Faça sua aplicação ADO + DataSnap VOAR!

Nota: Estes testes são válidos somente para aplicações que utilizam o mecanismo de acesso a dados ADO (ou dbGo).

Tenho usado o DataSnap de forma bem sucedida há muitos anos. Ultimamente venho otimizando os meus sistemas que usam DataSnap para a melhor performance possível, especialmente quando estou lidando com DataSets com muitos registros.

Andreas Hausladen fez um trabalho excelente em seu Midas SpeedFix, mas tem mais.

Eu tenho uma tabela simples "RUAS" contendo alguns campos: ID (Integer), NOME (varchar[50]) e mais alguns poucos campos. Bem, estou usando ADO para o acesso aos dados. A tabela tem 50.000 registros. Eu tenho um ADOQuery com este comando SQL:
SELECT * FROM ruas

Conectado a este ADOQuery eu tenho um DataSetProvider e a ele um ClientDataSet. Quando eu abro o ADOQuery leva 1,5 segundos, mas quando eu abro o ClientDataSet leva exatamente 59 segundos!!! 59 segundos para abrir uma query num ambiente de produção está completamente fora de questão.

Alguns vão dizer: "O ADO não fez o fetch de todos os registros, daí a diferença". Isto não é verdade. Você pode abrir o ADOQuery e ir para o último registro (efetivamente fazendo um fetch all) e o tempo será o mesmo. 1,5 segundoso para recuperar 50.000 registros! Um bom número.

Outros irão dizer: "Você não pode ter um ClientDataSet com 50.000 registros!". Não posso? Porquê não? 15 anos atrás me disseram "traga apenas alguns registros, não milhares". Atualmente temos servidores de aplicação com 8 Gb de RAM pelo menos, internet banda larga com 10 Mbps e os mesmos limites de 15 anos atrás se aplicam?

O ClientDataSet pode ser usado como mecanismo de cache, liberando o servidor de banco de dados de retornar o mesmo resultset repetidamente, mas eu preciso colocar mais do que umas poucas centenas de registros nele! Além disso, existem aplicações que seguem o modelo "Briefcase". Como alguém pode desenvolver uma aplicação com este modelo se o framework DataSnap impõe um limite tão baixo?

Então eu decidi descobrir porquê o ADO leva 1,5 segundos e o DataSnap leva 40 vezes mais. Fiz um profile da unit Provider.pas e descobri que a maior parte do tempo é gasta dentro do método TDataSetProvider.InternalGetRecords. O método TDataSetProvider.InternalGetRecords chama o TBaseProvider.InternalGetRecords herdado, que por sua vez chama TDataSetProvider.CreateDataPacket. Existe uma longa cadeia de chamadas de métodos, mas no final eu descobri que o gargalo é o método TDataPacketWriter.WriteDataSet. Analisando este método talvez tenhamos uma dica:

while (not DataSet.EOF) and (Result < RecsOut) do
begin
FIDSWriter.PutField(fldIsChanged, 1, @B);
for i := 0 to High(Info) do
Info[i].PutProc(@Info[i]);
Inc(Result);
if Result < RecsOut then
DataSet.Next;
end;

Como podemos ver, existe um loop while percorrendo todos os registros do DataSet. Bem, nós sabemos que este tipo de cenário pode ser bem lento pois cada chamada a DataSet.Next dispara uma longa sequência de eventos se DataSet.ControlsDisabled = False, ou seja, se não chamarmos o método DataSet.DisableControls antes do loop.

Então, só para testar o efeito de DisableControls na velocidade da obtenção dos dados pelo DataSetProvider, eu fiz uma pequena alteração no código onde eu o ClientDataSet era aberto, da seguinte forma:

ADOQuery1.DisableControls;
try
ClientDataSet.Open;
finally
ADOQuery1.EnableControls;
end;

O resultado que eu obtive foi incrível: Dos 59 segundos originais, o tempo caiu para 3,3 segundos, uma melhora de quase 95% no tempo de abertura do ClientDataSet!
Eu pude verificar que a diferença de performance é tanto maior quanto mais registros existem no ClientDataSet.

Existem dois problemas de ordem prática nesta abordagem:
1) Usar DisableControls programaticamente em todos os pontos do sistema onde se tem um ClientDataSet ligado a um DataSetProvider é totalmente inviável;
2) DisableControls não pode ser utilizado quando a tabela é master em uma relação Master/Detail.

Outra coisa interessante que eu descobri é que o DBExpress não sofre do mesmo problema, isto é, DisbleControls faz pouca diferença. De qualquer forma, irei medir o desempenho do DBExpress, com e sem DisableControls para comparar os resultados.

A solução que eu criei terá abordada num próximo post.

3 comments:

fabricioaraujo_rj said...

Interessante!! Estou esperando essa solução... ;-)

Rodrigo Holl said...

Grande dica, resolveu meu problema.
Vlew!

Anonymous said...

Parabéns pelo Post;