O HotXLS Delphi Component guarda uma linha ODS que carrega table:number-rows-repeated e uma altura de linha como um único registro TXLSXRowHeightRun — primeira linha, última linha, uma altura — em vez de uma entrada de altura por linha repetida, e dobra o estilo de célula vazia que essas linhas herdam num único overlay de estilo de intervalo. É essa a razão de o HotXLS 2.382.2 abrir uma planilha cuja cauda repete 1.048.530 linhas vazias em 0,02 segundos onde a 2.382.1 estourava o tempo, e de o mesmo arquivo voltar para ODS com a contagem de repetição intacta em vez de virar um milhão de linhas literais
O arquivo em questão é comum. O LibreOffice Calc escreve uma planilha de catorze colunas com 45 linhas de dados e depois descreve tudo abaixo delas com um único elemento: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. O estilo ro1 define style:row-height="0.452cm", e cada <table:table-column> carrega um table:default-cell-style-name que toda célula vazia do run herda. Todo o content.xml tem 103 KB. Nada no arquivo diz caro; o custo era inteiramente nosso
Por que uma linha repetida estoura o tempo de uma importação ODS?
Porque o importador costumava expandi-la. Na 2.382.1 o finalizador de linha fazia um laço de SetRowHeight(RowIndex + i, RowHeight) uma vez por linha repetida, gravando cada altura numa lista de strings Name=Value indexada pelo número da linha. Toda inserção nessa lista rodava um IndexOfName por tudo o que já estava nela, então um milhão de alturas custava um milhão de varreduras lineares — a busca quadrática em lista contra a qual o HXLS-005 foi aberto. Ao mesmo tempo OdsCommitRow materializava um objeto de célula para cada coluna que herdava um estilo, em cada uma das linhas repetidas, porque uma célula vazia com estilo ainda contava como célula
O lado do save tinha sua própria versão do problema. O arquivo do LibreOffice termina com mais uma linha ro1 depois da grande repetição, então a linha com estilo mais alta ficava no fundo da planilha, e OdsBuildTableXml percorria toda linha até ela emitindo elementos <table:table-row> um a um. Mesmo uma pasta de trabalho cuja importação tivesse saído barata teria uma gravação cara. Corrigir a importação sem corrigir a exportação teria movido o estouro de tempo, não o removido
O que é um run de altura de linha no HotXLS?
Um run é a menor coisa capaz de descrever as linhas 46 a 1.048.575 com 12,81 pontos de altura sem dizê-lo 1.048.530 vezes. TXLSXRowHeightRun é um registro de FirstRow, LastRow e Height; TXLSXRowHeightRuns é um array dinâmico deles, e cada TXLSXWorksheet guarda um em FRowHeightRuns ao lado da lista de alturas por linha já existente. Na importação ODS o finalizador de linha agora decide pela contagem de repetição: contagem 1 ainda chama SetRowHeight, qualquer coisa maior chama XlsxAssignRowHeightRun uma vez para todo o trecho. O trecho é limitado a XlsxMaxRow, que é 1.048.576, então uma contagem de repetição que ultrapassa a planilha é truncada em vez de rejeitada
XlsxAssignRowHeightRun é o único writer do array, e ela mantém os runs disjuntos por construção. Dado um intervalo novo, ela copia todo run existente que fica inteiramente fora dele, divide qualquer run que o sobreponha na parte anterior e na parte posterior, e então anexa o intervalo novo quando Present é true — ou não anexa nada quando Present é false, que é como ClearRowHeight abre um buraco de uma linha. Duas coisas decorrem disso. O array nunca contém intervalos sobrepostos, então uma consulta pode parar no primeiro acerto. E o array nunca é alterado no lugar; uma cópia nova é construída a cada chamada, o que não custa nada nos tamanhos em questão e elimina uma classe inteira de bugs de alias
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Workbook := TXLSXWorkbook.Create;
try
// Uma planilha cuja linha da cauda se repete 1.048.530 vezes sob um estilo de linha
Workbook.OpenODS('conditional-formatting.ods');
Sheet := Workbook.Sheets[1];
// As duas leituras resolvem pelo mesmo run; nada foi expandido
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// Um override de uma linha sombreia o run sem dividi-lo
Sheet.RowHeight[500000] := 36;
// Limpar uma linha dentro do run corta o run em dois pedaços
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // False
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // ainda a altura do run
finally
Workbook.Free;
end;
end;
A ordem de consulta é a parte que vale memorizar. TXLSXWorksheet.GetRowHeight confere primeiro a lista por linha e só consulta os runs quando a linha não tem entrada explícita, e HasRowHeight faz o mesmo. Então Sheet.RowHeight[500000] := 36 não toca no run em nada — ela adiciona uma entrada à lista por linha, e essa entrada vence porque é consultada primeiro. ClearRowHeight é o oposto: remove qualquer entrada por linha e então chama XlsxAssignRowHeightRun com Present = False, porque uma linha limpa precisa ler como sem altura mesmo se um run a cobrir. ClearRowHeights esvazia as duas estruturas de uma vez
Para onde vão os estilos herdados das células vazias?
Para um overlay de estilo de intervalo por coluna, e não para objetos de célula. OdsCommitRow decide, coluna a coluna, se o valor é um vazio compacto: a linha se repete mais de uma vez, a célula não tem valor, nem fórmula, nem rich text. Para um vazio compacto ela cria uma célula de verdade só na primeira linha do run, aplica o estilo herdado a ela, e então registra os mesmos seis índices de estilo — fonte, preenchimento, borda, formato numérico, alinhamento, proteção — como um StyleOverlays.Add cobrindo da segunda linha até o fim do run naquela coluna. As linhas depois da primeira são puladas por completo no laço de materialização
O teste de regressão torna a forma concreta. Depois de abrir uma planilha cuja segunda linha se repete 1.048.575 vezes sob um estilo padrão de coluna em negrito, afirma-se que Sheet.Cells.Count fica abaixo de 10, e Sheet.Cells[700000, 1].FontIndex ainda resolve para a fonte em negrito — o overlay fornece o estilo no momento em que aquela coordenada é tocada. É o mesmo mecanismo que impede uma coluna formatada mas vazia de custar um milhão de células no lado XLSX; as notas sobre armazenamento de células em blocos de linha e overlays de estilo por intervalo cobrem como os overlays se empilham e resolvem. O que é novo aqui é que o importador ODS os cria por conta própria, a partir da contagem de repetição, em vez de esperar que uma aplicação formate um intervalo
Como o SaveAsODS grava a contagem de repetição de volta?
Dividindo a cauda vazia da planilha só onde algo realmente muda. OdsBuildTableXml agora acompanha dois limites: contentMaxRow, a última linha que carrega valor, fórmula, hyperlink ou quebra de linha manual, e maxRow, que além disso se estende por células vazias só com estilo, alturas de uma linha, o LastRow de cada run e a borda inferior de cada overlay. Uma célula vazia só com estilo deixou de contar como conteúdo — é o TXLSXCells.IsStyleOnlyBlank que a exclui — então a linha final com estilo do arquivo do LibreOffice para de arrastar o limite de conteúdo até o fundo da planilha
Acima de contentMaxRow as linhas são escritas uma a uma exatamente como antes. Abaixo dele o writer calcula nextRow como o menor entre: o FirstRow do próximo run, o LastRow + 1 do run atual, a próxima entrada de altura de uma linha, a próxima borda de overlay e a próxima célula materializada. Tudo da linha atual até nextRow - 1 é então emitido como um <table:table-row> com table:number-rows-repeated igual à diferença, carregando um <table:table-cell/> por coluna com o nome de estilo resolvido pelo overlay quando um overlay cobre aquela coluna. O estilo da linha em si vem de TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), que agora dobra o texto da altura — 12.81pt, por exemplo — na própria chave de desduplicação junto com as flags de linha oculta e de quebra de página, então toda linha do run compartilha um estilo ro<N> com uma única propriedade style:row-height
var
Workbook, Reopened: TXLSXWorkbook;
Saved: TMemoryStream;
begin
Workbook := TXLSXWorkbook.Create;
Reopened := TXLSXWorkbook.Create;
Saved := TMemoryStream.Create;
try
Workbook.OpenODS('conditional-formatting.ods');
Workbook.Sheets[1].RowHeight[500000] := 36;
Workbook.Sheets[1].ClearRowHeight(500001);
// A cauda vazia é escrita como um punhado de linhas repetidas, não um milhão
Workbook.SaveAsODS(Saved);
Writeln('ODS size: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.Open(Saved);
// Override, buraco e run sobrevivem todos ao round trip
Writeln(Reopened.Sheets[1].RowHeight[500000]:0:2); // 36.00
Writeln(Reopened.Sheets[1].HasRowHeight(500001)); // False
Writeln(Reopened.Sheets[1].RowHeight[500002]:0:2); // altura do run
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
O teste que fixa isso afirma que o stream salvo fica abaixo de 64 KB para uma planilha cujo run de altura cobre 1.048.575 linhas com um override e um buraco aberto no meio. Dois limites honestos cabem ao lado desse número. Primeiro, uma worksheet com qualquer validação de dados define contentMaxRow igual a maxRow, então validações desligam a compactação da cauda naquela planilha e ela volta a ser escrita linha a linha. Segundo, o XLSX não tem atributo de repetição — um <row> do SpreadsheetML descreve uma linha — então exportar uma planilha apoiada em runs para .xlsx enumera as linhas que o run cobre e grava um atributo ht em cada uma. O modelo continua compacto em memória; o formato do arquivo decide com que cara o arquivo fica
O que toda edição que renumera linhas deve agora aos runs?
Manutenção. Uma representação nova de metadados de linha só está correta se toda operação que muda números de linha a movimentar junto com as listas por linha ao lado das quais ela fica, e o commit toca cada uma dessas operações. InsertRows e DeleteRows passam por XlsxShiftRowHeightRuns, que reconstrói o array mantendo a parte de cada run que fica antes do ponto de edição, descartando o que cai dentro de uma janela de exclusão e readicionando o restante deslocado pelo delta — então um run que atravessa uma inserção vira dois runs com um vão, e um run que atravessa uma exclusão encolhe. TileRangeAxisMetadata limpa os runs por todo o trecho repetido e depois registra de novo cada run de origem uma vez por cópia, no deslocamento dela. TXLSXWorksheet.CopyFrom e TXLSXSheets.AddCopy pegam um Copy() do array em vez de atribuí-lo, e é por isso que o teste consegue limpar todas as alturas num clone e ainda encontrar a planilha original intacta na linha 1.048.576
var
Sheet: TXLSXWorksheet;
begin
Sheet := Workbook.Sheets[1];
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// Insere duas linhas em 500000: o override vai para 500002, o buraco para 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // False
// Exclui elas de novo: tudo volta ao lugar
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
// Repete as linhas 2..4 duas vezes planilha abaixo; as alturas seguem cada cópia
Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
Writeln(Sheet.RowHeight[7]:0:2); // a altura do run
end;
Os limites do lado da leitura têm a mesma obrigação. GetUsedRange sobe sua borda inferior até o FirstRow e o LastRow de cada run, e BuildRowMajorCellOrder estende sua linha máxima, incluindo metadados, por todo run, para que o writer de XLSX ainda visite linhas que só têm altura. Se você algum dia adicionar uma estrutura indexada por linha por cima do modelo de objetos do HotXLS, esta é a lista: inserir, excluir, repetir, copiar, intervalo usado e todo serializador. Erre um e a falha é silenciosa — as alturas deslocam pelo número de inserções, e nada levanta exceção
O que continua por linha, e como os números estão agora
Flags de linha oculta, níveis de estrutura de tópicos e estado recolhido ainda são expandidos. O finalizador de linha faz laços de SetRowHidden e SetRowOutlineLevel uma vez por linha repetida, então uma planilha que oculta uma cauda de um milhão de linhas, ou a aninha dentro de um table:table-row-group, paga uma entrada por linha para cada um desses atributos. A mudança da 2.382.2 tem escopo nas duas coisas que o HXLS-005 realmente mediu — alturas e estilos herdados de células vazias — e a mesma técnica de run se aplicaria às outras se algum arquivo exigisse. O reader de ODS também não age sobre style:use-optimal-row-height; um estilo de linha que diz ideal e dá uma altura é importado com aquela altura
Contra o corpus, conditional-formatting.ods agora completa o ciclo de abrir, afirmar, salvar, reabrir e reafirmar em 0,178 segundos no Win32 e 0,158 segundos no Win64, com o estágio de abertura em si em 0,020 segundos, dentro de um orçamento de 60 segundos que ele antes esgotava. As interfaces no nível da pasta de trabalho por onde o formato passa estão descritas no passo a passo de abrir e salvar arquivos ODS, e o conjunto mais amplo de alavancas para arquivos grandes em desempenho com pastas de trabalho grandes; o próprio elemento de linha do ODF, com seus atributos de repetição e estilo, está especificado na ODF 1.3 Part 3 §9.1.4
O HotXLS lê e grava XLS, XLSX e ODS a partir de código Delphi e C++Builder nativo, sem Excel ou LibreOffice instalados, e é por isso que uma repetição de um milhão de linhas é algo que a biblioteca precisa modelar bem em vez de entregar a um processo externo — a página do componente HotXLS Delphi Spreadsheet lista os formatos suportados e as versões do RAD Studio