Artigo Técnico

HotXLS Component: large workbook performance in Delphi

Quando uma exportação de 300.000 linhas esgota os limites de memória, a responsabilidade é geralmente atribuída ao volume de linhas. Contudo, as linhas costumam ser inocentes. As componentes dispendiosas num livro de cálculo de grandes dimensões são aquelas criadas de forma secundária: um conjunto de estilos (style pool) que cresce com uma nova entrada por cada célula porque a formatação foi adicionada dentro do ciclo iterativo, o XML da folha de cálculo montado como uma única string gigante no momento da gravação, ou um milhão de fórmulas idênticas armazenadas individualmente. O HotXLS, a biblioteca nativa Delphi da losLab para arquivos XLS e XLSX, disponibiliza um controle específico para cada um destes custos. Nenhum deles está ativado por padrão porque cada um altera o comportamento e os compromissos do sistema; como tal, identificar qual o controle indicado para cada sintoma é a verdadeira competência de otimização de desempenho

Onde um livro de cálculo de grandes dimensões consome memória

Existem dois regimes distintos de consumo de memória a considerar. Durante a geração, o modelo de células em memória cresce com cada célula processada: valores, formatos e fórmulas tornam-se objetos ou entradas em pools. Durante a gravação, o fluxo XLSX padrão processa adicionalmente o XML de cada folha numa string ampla antes de a comprimir no contentor zip; assim, o pico de consumo é equivalente ao modelo acrescido da forma serializada da maior folha de cálculo. Uma tarefa que conclui o ciclo de construção mas falha no método SaveAs enquadra-se no segundo regime e não no primeiro, e a solução para um nada resolve no outro

O tamanho do arquivo segue uma lógica idêntica: as células são apenas um dos fatores de peso, juntamente com estilos, strings partilhadas, fórmulas, imagens e comentários. Uma análise com ForEachCell e a contagem das coleções por folha revela qual o recurso que domina o arquivo antes de avançar para otimizações erradas. Uma subtileza na medição: a contagem Sheet.Cells.Count do lado XLSX indica o número de células instanciadas no repositório esparso, e não a área do intervalo utilizado. Uma folha cujos dados ocupam um retângulo de 1000 por 50, com metade das células vazias, conta cerca de 25.000 e não 50.000. Esta distinção é relevante ao comparar um arquivo de grandes dimensões recebido do cliente com os seus dados de teste, pois a área do intervalo em uso e a população real de células podem diferir significativamente em layouts financeiros esparsos

O StreamingWrite corrige o fluxo de gravação e não o de construção

Definir a propriedade TXLSXWorkbook.StreamingWrite := True altera o comportamento do método SaveAs para um serializador por streaming que escreve o XML da folha de cálculo diretamente no fluxo zip, eliminando a string intermédia por folha. Tem o valor padrão False por compatibilidade de comportamento, e a sua aceitação requer apenas uma linha:

Seja rigoroso sobre o benefício real: o modelo de células construído no ciclo consome exatamente a mesma memória que antes. O StreamingWrite elimina o pico de consumo no momento da gravação, o que dita a diferença entre uma tarefa concluída com sucesso e outra que falha aos 95% do processo. Se o ciclo de construção em si esgotar a memória, as soluções necessárias são as duas descritas a seguir

Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Bulk');
  for R := 1 to 100000 do
  begin
    Sheet.Cells[R, 1].Value := R;
    Sheet.Cells[R, 2].Value := 'Row ' + IntToStr(R);
    Sheet.Cells[R, 3].Value := R * 1.5;
  end;
  Book.StreamingWrite := True;   // sheet XML streams into the zip container
  Book.SaveAs('bulk.xlsx');
finally
  Book.Free;
end;

Pools de estilos: adicionar uma vez, reutilizar o índice

A formatação XLSX no HotXLS baseia-se em pools: os métodos Book.Fonts.Add(...), Fills.AddSolid(...) e Borders.Add(...) devolvem um índice de pool baseado em 0 que as células referenciam. Invocar Fonts.Add com parâmetros idênticos dentro de um ciclo é consolidado automaticamente, pelo que consome tempo e não espaço. Contudo, Alignments.Add tem comportamento distinto: devolve um novo objeto a cada chamada, pelo que a criação de alinhamentos por célula faz o pool crescer linearmente com o número de linhas. Uma boa prática resolve ambos os casos: obtenha cada índice de pool uma vez, fora do ciclo de iteração, e atribua os índices no interior deste

O + 1 não é erro tipográfico e a sua omissão constitui o bug clássico neste cenário: os pools devolvem índices baseados em 0, enquanto as propriedades ao nível da célula tratam o valor 0 como "padrão", pelo que cada índice obtido do pool tem de ser incrementado em uma unidade na atribuição. Se falhar esta operação, os seus cabeçalhos adotarão silenciosamente o tipo de letra padrão do livro, uma falha que apenas será detetada na revisão estética

// hoist pool lookups out of the hot loop
HeaderFont := Book.Fonts.Add('Calibri', 11, True, False);   // 0-based pool index
for C := 1 to 24 do
  Sheet.Cells[1, C].FontIndex := HeaderFont + 1;            // cells store 1-based; 0 = default

Substituir a transferência de Variant por célula por callbacks de linha

Cada atribuição a Sheet.Cells[R, C].Value := X implica a pesquisa ou criação da célula e uma atribuição de Variant. Com algumas centenas de milhares de células, este overhead por acesso torna-se mensurável no profiling. O HotXLS disponibiliza APIs de callback em lote em ambas as facetas (ForEachCell e ForEachRow para leitura, WriteCells e WriteRows para escrita) que realizam a iteração internamente no motor e fornecem linhas completas de cada vez ao seu código:

O marcador Skip do callback deixa uma linha inalterada sem interromper o processo, e Cancel termina a operação antecipadamente, o que é útil se a origem for um leitor (reader) cujo comprimento se descobre em execução. Combine o método WriteRows na construção com o StreamingWrite na gravação e o fluxo de geração não apresentará mais pontos lentos por célula

procedure TLedgerExport.FillRow(Sender: TObject;
  SheetIndex, Row, FirstCol, LastCol: Integer;
  var Values: Variant; var Skip: Boolean; var Cancel: Boolean);
begin
  if Row > FCount then
  begin
    Cancel := True;     // stop the whole write
    Exit;
  end;
  Values := VarArrayOf([FRows[Row - 1].Account,
                        FRows[Row - 1].PostedOn,
                        FRows[Row - 1].Amount]);
end;

// one engine call instead of hundreds of thousands of property hits
Sheet.WriteRows(1, 1, FCount, 3, FillRow);

Controlos de leitura na interface XLS

Os arquivos legados .xls de grandes dimensões contam com ferramentas específicas. Definir _DisableGraphics := True antes de Open ignora por completo a análise da camada gráfica (drawing layer), o que acelera o carregamento de livros com formas acumuladas e imagens incorporadas ao longo de anos. A restrição é severa: a camada gráfica deixa de existir no modelo, pelo que gravar o livro resultará num arquivo sem ilustrações. Reserve esta opção para tarefas de análise de apenas leitura. O método SetTempDir redireciona os arquivos temporários do gerador BIFF, relevante em servidores onde a pasta temporária padrão tenha limites de quota ou resida em armazenamento lento. E UseSharedFormulas agrupa corpos de fórmulas repetidos em registros partilhados, reduzindo arquivos onde uma fórmula se repete ao longo de sessenta mil linhas

Os ciclos de leitura em dados XLS contêm uma armadilha de indexação importante porque duplica o trabalho se tratada defensivamente e corrompe dados se omitida: as propriedades UsedRange (como FirstRow, LastRow, FirstCol e LastCol) baseiam-se em 0, enquanto Cells.Item[Row, Col] baseia-se em 1. Um varrimento que percorra o intervalo em uso deve adicionar uma unidade a cada coordenada no acesso à célula (como em Cells.Item[Row + 1, Col + 1]), sob pena de ler uma grelha deslocada diagonalmente em uma célula, omitindo silenciosamente a última linha e coluna e incluindo uma primeira linha fantasma. O callback ForEachCell contorna esta divergência, constituindo mais um motivo para o preferir em varrimentos completos de folhas

Inspecionar arquivos antes de os carregar

A operação mais económica com livros de grandes dimensões é aquela que se evita. O método GetSheetNames em ambas as interfaces lista as folhas de um arquivo sem carregar os dados das células. A implementação XLSX lê apenas o manifesto do livro no contentor zip e deixa a instância do livro vazia; a faceta XLS interrompe a leitura no limite da primeira subestrutura. Isto torna-a na validação correta para determinar a folha de destino antes da importação, e CanReadEncrypted esclarece se o contentor está cifrado antes de uma tentativa falhada de Open

Preste atenção à convenção do código de retorno: estas funções de inspeção sinalizam falha com valores iguais ou inferiores a zero e limpam a lista de saída; como tal, valide por <= 0 em vez de comparar com um código de sucesso específico

Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
  if Book.GetSheetNames('big-unknown.xlsx', Names) <= 0 then
    raise Exception.Create('cannot enumerate sheets');   // failure clears the list
  // pick the target sheet, then decide whether a full Open is worth it
finally
  Book.Free;
  Names.Free;
end;

Adequar a abordagem à tarefa

Os objetos de ambas as interfaces não são seguros para partilha entre threads (thread-safe), mas é perfeitamente viável ter um livro independente por thread de trabalho, paralelizando a conversão em lote. Quando a saída se destina a HTTP e não ao disco, as sobrecargas de gravação em TStream aliam-se a StreamingWrite para que uma resposta volumosa nunca crie arquivos temporários. Aplica-se uma nota operacional: a gravação em fluxo escreve a partir da posição atual sem reposicionar; configure Position := 0 antes de entregar o fluxo à infraestrutura de resposta. O artigo sobre escrita por streaming e processamento em lote detalha este padrão de servidor, e o artigo sobre exportação de base de dados mostra onde integrar estas opções num relatório alimentado por dados

Por fim, mantenha um caso de teste (fixture) representativo do pior cenário por família de relatórios e monitorize o seu tempo de execução no fluxo de integração contínua (CI). As perdas de desempenho na geração de documentos raramente são óbvias: um estilo adicionado dentro de um ciclo ou uma validação substituída por um Open completo não alteram o resultado funcional, e a tarefa noturna apenas demorará mais quarenta minutos. Um teste cronometrado com um caso representativo de meio milhão de células converte essa degradação numa compilação com erro (red build) e evita incidentes em produção

As compilações de avaliação, os projetos de demonstração com exemplos de geração em lote e a referência completa da API estão disponíveis na página do HotXLS Component page