De HotXLS Delphi Component bewaart een ODS-rij met table:number-rows-repeated en een rijhoogte als één record TXLSXRowHeightRun — eerste rij, laatste rij, één hoogte — in plaats van één hoogte-item per herhaalde rij, en vouwt de stijl van lege cellen die die rijen erven samen in één interval-stijloverlay. Dat is precies waarom HotXLS 2.382.2 een spreadsheet waarvan de staart 1,048,530 lege rijen herhaalt in 0.02 seconden opent waar 2.382.1 een timeout kreeg, en waarom hetzelfde bestand weer naar ODS opslaat met de herhalingsteller intact in plaats van als een miljoen letterlijke rijen
Het bestand in kwestie is heel gewoon. LibreOffice Calc schrijft een blad met veertien kolommen en 45 rijen data, en beschrijft daarna alles daaronder met één element: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Stijl ro1 zet style:row-height="0.452cm", en elke <table:table-column> draagt een table:default-cell-style-name die elke lege cel in de reeks erft. De hele content.xml is 103 KB. Niets aan het bestand zegt "duur"; de kosten waren volledig de onze
Waarom geeft één herhaalde rij een timeout bij een ODS-import?
Omdat de importer hem vroeger uitklapte. In 2.382.1 liet de rij-afronder SetRowHeight(RowIndex + i, RowHeight) één keer per herhaalde rij lopen, en schreef elke hoogte in een Name=Value-stringlijst met het rijnummer als sleutel. Elke invoeging in die lijst deed een IndexOfName-opzoeking over alles wat er al in stond, dus een miljoen hoogtes kostten een miljoen lineaire scans — het kwadratische lijstzoeken waar HXLS-005 tegen was geregistreerd. Tegelijk materialiseerde OdsCommitRow een celobject voor elke kolom die een stijl erfde, op elke herhaalde rij, omdat een lege cel met stijl nog steeds als cel telde
De opslagkant had zijn eigen versie van het probleem. Het LibreOffice-bestand eindigt na de grote herhaling met nog één ro1-rij, dus de hoogst gestijlde rij zat helemaal onderaan het blad, en OdsBuildTableXml liep elke rij tot daar langs om <table:table-row>-elementen één voor één uit te schrijven. Zelfs een workbook die goedkoop was geïmporteerd zou duur zijn weggeschreven. De import fixen zonder de export te fixen zou de timeout hebben verplaatst, niet weggenomen
Wat is een rijhoogte-run in HotXLS?
Een run is het kleinste ding dat "rijen 46 tot en met 1,048,575 zijn allemaal 12.81 punten hoog" kan beschrijven zonder dat 1,048,530 keer te zeggen. TXLSXRowHeightRun is een record met FirstRow, LastRow en Height; TXLSXRowHeightRuns is een dynamische array daarvan, en elke TXLSXWorksheet houdt er één bij in FRowHeightRuns, naast de bestaande hoogtelijst per rij. Bij ODS-import vertakt de rij-afronder nu op de herhalingsteller: een teller van 1 roept nog steeds SetRowHeight aan, alles groter roept één keer XlsxAssignRowHeightRun aan voor de hele spanne. De spanne wordt afgekapt op XlsxMaxRow, dat 1,048,576 is, dus een herhalingsteller die over het blad heen schiet wordt afgekapt in plaats van geweigerd
XlsxAssignRowHeightRun is de enige schrijver van de array, en hij houdt de runs door zijn constructie disjunct. Krijgt hij een nieuw interval, dan kopieert hij elke bestaande run die er volledig buiten ligt, splitst hij elke run die eroverheen valt in het stuk ervoor en het stuk erna, en voegt hij daarna het nieuwe interval toe wanneer Present true is — of voegt hij niets toe wanneer Present false is, en zo slaat ClearRowHeight een gat van één rij. Twee dingen volgen daaruit. De array bevat nooit overlappende intervallen, dus een opzoeking kan bij de eerste treffer stoppen. En de array wordt nooit ter plekke gemuteerd; bij elke aanroep wordt een verse kopie gebouwd, wat op deze groottes niets kost en een hele klasse aliasing-bugs wegneemt
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Workbook := TXLSXWorkbook.Create;
try
// Een blad waarvan de staartrij 1,048,530 keer herhaalt onder één rijstijl
Workbook.OpenODS('conditional-formatting.ods');
Sheet := Workbook.Sheets[1];
// Beide uitlezingen lossen op via dezelfde run; er is niets uitgeklapt
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// Een override van één rij overschaduwt de run zonder hem te splitsen
Sheet.RowHeight[500000] := 36;
// Eén rij binnen de run wissen knipt de run in twee stukken
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // False
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // nog steeds de runhoogte
finally
Workbook.Free;
end;
end;
De opzoekvolgorde is het deel dat het onthouden waard is. TXLSXWorksheet.GetRowHeight kijkt eerst in de lijst per rij en raadpleegt de runs alleen wanneer de rij geen expliciet item heeft, en HasRowHeight doet hetzelfde. Dus Sheet.RowHeight[500000] := 36 raakt de run helemaal niet — het voegt één item toe aan de lijst per rij, en dat item wint omdat het eerst wordt opgezocht. ClearRowHeight is het omgekeerde: het verwijdert een eventueel item per rij en roept dan XlsxAssignRowHeightRun aan met Present = False, want een gewiste rij moet als "geen hoogte" lezen, ook als een run hem dekt. ClearRowHeights maakt beide structuren in één keer leeg
Waar blijven de geërfde stijlen van lege cellen?
In één interval-stijloverlay per kolom, niet in celobjecten. OdsCommitRow beslist per celwaarde of het een compacte lege cel is: de rij herhaalt meer dan één keer, en de cel heeft geen waarde, geen formule en geen rich text. Voor een compacte leegte maakt hij alleen op de eerste rij van de run een echte cel, past daarop de geërfde stijl toe, en registreert daarna dezelfde zes stijlindexen — font, vulling, rand, getalnotatie, uitlijning, beveiliging — als een StyleOverlays.Add die in die kolom de rijen twee tot het eind van de run dekt. Rijen na de eerste worden in de materialisatielus volledig overgeslagen
De regressietest maakt de vorm concreet. Na het openen van een blad waarvan de tweede rij 1,048,575 keer herhaalt onder een vette kolomstandaardstijl, wordt gecontroleerd dat Sheet.Cells.Count onder 10 blijft, en dat Sheet.Cells[700000, 1].FontIndex nog steeds naar het vette font verwijst — de overlay levert de stijl op het moment dat die coördinaat wordt aangeraakt. Dat is hetzelfde mechanisme dat een opgemaakte maar lege kolom op de XLSX-kant geen miljoen cellen laat kosten; de notities over rijblokcelopslag en interval-stijloverlays behandelen hoe overlays stapelen en oplossen. Wat hier nieuw is, is dat de ODS-importer ze zelf aanmaakt, vanuit de herhalingsteller, in plaats van te wachten tot een applicatie een bereik opmaakt
Hoe schrijft SaveAsODS de herhalingsteller terug?
Door de lege staart van het blad alleen te splitsen waar echt iets verandert. OdsBuildTableXml houdt nu twee grenzen bij: contentMaxRow, de laatste rij met een waarde, formule, hyperlink of handmatige rij-einde, en maxRow, die ook doorloopt over alleen-gestijlde lege cellen, hoogtes van één rij, de LastRow van elke run en de onderrand van elke overlay. Een alleen-gestijlde lege cel telt niet langer als inhoud — TXLSXCells.IsStyleOnlyBlank sluit hem uit — dus de gestijlde staartrij in het LibreOffice-bestand sleept de inhoudgrens niet meer naar de onderkant van het blad
Boven contentMaxRow worden rijen één voor één geschreven, precies zoals voorheen. Daaronder berekent de schrijver nextRow als de kleinste van: de FirstRow van de volgende run, de LastRow + 1 van de huidige run, het volgende hoogte-item van één rij, de volgende overlayrand, en de volgende gematerialiseerde cel. Alles van de huidige rij tot nextRow - 1 wordt dan uitgeschreven als één <table:table-row> met table:number-rows-repeated op het verschil, met één <table:table-cell/> per kolom met de via de overlay opgeloste stijlnaam wanneer een overlay die kolom dekt. De rijstijl zelf komt van TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), die de hoogtetekst — 12.81pt, zeg maar — nu in zijn deduplicatiesleutel vouwt naast de flags voor verbergen en pagina-einde, zodat elke rij in de run één ro<N>-stijl deelt met één eigenschap 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);
// De lege staart wordt als een handvol herhaalde rijen geschreven, niet als een miljoen
Workbook.SaveAsODS(Saved);
Writeln('ODS size: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.Open(Saved);
// Override, gat en run overleven alle drie de 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); // runhoogte
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
De test die dit vastpint, controleert dat de opgeslagen stream onder 64 KB blijft voor een blad waarvan de hoogte-run 1,048,575 rijen omspant met een override en een gat erin geslagen. Naast dat getal horen twee eerlijke grenzen. Ten eerste zet een werkblad met welke data-validatie dan ook contentMaxRow op maxRow, dus validaties schakelen de staartcompressie op dat blad uit en het wordt weer rij voor rij geschreven. Ten tweede heeft XLSX geen herhalingsattribuut — een SpreadsheetML-<row> beschrijft één rij — dus een blad met runs naar .xlsx exporteren somt de rijen op die de run dekt en schrijft op elk een ht-attribuut. Het model blijft compact in het geheugen; het bestandsformaat bepaalt hoe het bestand eruitziet
Wat is elke bewerking die rijnummers verschuift nu verplicht aan de runs?
Onderhoud. Een nieuwe representatie van rijmetadata is alleen correct als elke bewerking die rijnummers verandert hem meebeweegt met de lijsten per rij waar hij naast staat, en de commit raakt elk van die bewerkingen aan. InsertRows en DeleteRows lopen via XlsxShiftRowHeightRuns, die de array opnieuw opbouwt door van elke run het deel te houden dat vóór het bewerkingspunt ligt, weg te laten wat binnen een verwijderingsvenster valt, en de rest verschoven met het delta opnieuw toe te voegen — dus een run die een invoeging overspant wordt twee runs met een gat, en een run die een verwijdering overspant krimpt. TileRangeAxisMetadata wist de runs over de hele getegelde spanne en registreert daarna elke bronrun één keer per kopie op zijn offset. TXLSXWorksheet.CopyFrom en TXLSXSheets.AddCopy nemen een Copy() van de array in plaats van hem toe te wijzen, en daarom kan de test alle hoogtes op een kloon wissen en het originele blad op rij 1,048,576 nog intact vinden
var
Sheet: TXLSXWorksheet;
begin
Sheet := Workbook.Sheets[1];
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// Voeg twee rijen in op 500000: de override schuift naar 500002, het gat naar 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // False
// Verwijder ze weer: alles schuift terug
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
// Tegel rijen 2..4 twee keer naar beneden; runhoogtes volgen elke kopie
Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
Writeln(Sheet.RowHeight[7]:0:2); // de runhoogte
end;
De grenzen aan de leeskant hebben dezelfde verplichting. GetUsedRange verhoogt zijn onderrand naar de FirstRow en LastRow van elke run, en BuildRowMajorCellOrder rekt zijn metadata-inclusieve maximumrij op over elke run, zodat de XLSX-writer ook rijen bezoekt die alleen hoogte hebben. Als je ooit je eigen op rijen gebaseerde structuur bovenop het HotXLS-objectmodel zet, is dit de checklist: invoegen, verwijderen, tegelen, kopiëren, gebruikt bereik, en elke serializer. Mis je er één, dan is de fout stil — hoogtes schuiven mee met het aantal invoegingen, en er wordt niets gegooid
Wat blijft per rij, en hoe zien de cijfers er nu uit
Verborgen flags, overzichtsniveaus en samengevouwen toestand klappen nog steeds uit. De rij-afronder loopt SetRowHidden en SetRowOutlineLevel één keer per herhaalde rij langs, dus een blad dat een staart van een miljoen rijen verbergt, of die in een table:table-row-group nestelt, betaalt een item per rij voor elk van die attributen. De wijziging in 2.382.2 is beperkt tot de twee dingen die HXLS-005 werkelijk heeft gemeten — hoogtes en geërfde lege stijlen — en dezelfde runtechniek zou op de andere van toepassing zijn als een bestand dat ooit zou eisen. De ODS-reader doet ook niets met style:use-optimal-row-height; een rijstijl die "optimaal" zegt en een hoogte geeft, wordt met die hoogte geïmporteerd
Tegen het corpus rondt conditional-formatting.ods de cyclus openen, controleren, opslaan, heropenen en opnieuw controleren nu af in 0.178 seconden op Win32 en 0.158 seconden op Win64, waarbij de openingsfase zelf 0.020 seconden kost, binnen een budget van 60 seconden dat eerder werd uitgeput. De interfaces op workbookniveau waar het formaat doorheen loopt, staan in de walkthrough van ODS-bestanden openen en opslaan, en de bredere set knoppen voor grote bestanden in prestaties van grote workbooks; het ODF-rij-element zelf, met zijn herhalings- en stijlattributen, staat gespecificeerd in ODF 1.3 Part 3 §9.1.4
HotXLS leest en schrijft XLS, XLSX en ODS vanuit native Delphi- en C++Builder-code zonder geïnstalleerde Excel of LibreOffice, en daarom is een herhaling van een miljoen rijen iets wat de library goed moet modelleren in plaats van uit te besteden aan een extern proces — de HotXLS Delphi spreadsheet component-pagina somt de ondersteunde formaten en RAD Studio-versies op