HotXLS Delphi Component lagrar en ODS-rad som bär table:number-rows-repeated och en radhöjd som en enda TXLSXRowHeightRun-post — första raden, sista raden, en höjd — i stället för en höjduppgift per upprepad rad, och viker in den tomcellsstil som raderna ärver i en enda intervallstilöverlagring. Det är hela förklaringen till att HotXLS 2.382.2 öppnar ett kalkylblad vars svans upprepar 1 048 530 tomma rader på 0,02 sekunder där 2.382.1 gjorde timeout, och till att samma fil sparas tillbaka till ODS med upprepningsantalet intakt i stället för som en miljon bokstavliga rader
Filen i fråga är helt vanlig. LibreOffice Calc skriver ett ark med fjorton kolumner och 45 datarader, och beskriver sedan allt nedanför dem med ett enda element: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Stilen ro1 sätter style:row-height="0.452cm", och varje <table:table-column> bär ett table:default-cell-style-name som varje tom cell i serien ärver. Hela content.xml är 103 kB. Inget i filen säger "dyr"; kostnaden var helt och hållet vår
Varför gör en enda upprepad rad att en ODS-import får timeout?
För att importören brukade expandera den. I 2.382.1 loopade radavslutaren SetRowHeight(RowIndex + i, RowHeight) en gång per upprepad rad och skrev varje höjd i en Name=Value-stränglista nycklad på radnummer. Varje insättning i listan körde en IndexOfName-sökning över allt som redan låg i den, så en miljon höjder kostade en miljon linjära genomsökningar — den kvadratiska listsökningen som HXLS-005 anmäldes mot. Samtidigt materialiserade OdsCommitRow ett cellobjekt för varje kolumn som ärvde en stil, på var och en av de upprepade raderna, eftersom en stilmässig tom cell fortfarande räknades som en cell
Sparsidan hade sin egen version av problemet. LibreOffice-filen slutar med ytterligare en ro1-rad efter den stora upprepningen, så den högst belägna stilmässiga raden låg allra längst ned i arket, och OdsBuildTableXml gick igenom varje rad fram till den och gav <table:table-row>-element ett i taget. Även en arbetsbok som importerats billigt skulle ha skrivits dyrt. Att fixa importen utan att fixa exporten skulle ha flyttat timeouten, inte tagit bort den
Vad är ett radhöjdsintervall i HotXLS?
Ett intervall är det minsta som kan beskriva "rad 46 till 1 048 575 är alla 12,81 punkter höga" utan att säga det 1 048 530 gånger. TXLSXRowHeightRun är en post med FirstRow, LastRow och Height; TXLSXRowHeightRuns är en dynamisk array av dem, och varje TXLSXWorksheet håller en i FRowHeightRuns bredvid den befintliga radhöjdslistan. Vid ODS-import grenar radavslutaren nu på upprepningsantalet: antalet 1 anropar fortfarande SetRowHeight, allt större anropar XlsxAssignRowHeightRun en gång för hela spannet. Spannet klipps mot XlsxMaxRow, som är 1 048 576, så ett upprepningsantal som skjuter över arket trunkeras i stället för att avvisas
XlsxAssignRowHeightRun är den enda skrivaren till arrayen, och den håller intervallen åtskilda genom konstruktion. Givet ett nytt intervall kopierar den varje befintligt intervall som ligger helt utanför det, delar varje intervall som överlappar det i biten före och biten efter, och lägger sedan till det nya intervallet när Present är true — eller lägger till ingenting när Present är false, vilket är hur ClearRowHeight stansar ut ett enradshål. Två saker följer. Arrayen innehåller aldrig överlappande intervall, så en uppslagning kan stanna vid första träffen. Och arrayen muteras aldrig på plats; en färsk kopia byggs vid varje anrop, vilket inte kostar något i de storlekar det handlar om och tar bort en hel klass av aliasingbuggar
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Workbook := TXLSXWorkbook.Create;
try
// Ett ark vars svansrad upprepas 1 048 530 gånger under en radstil
Workbook.OpenODS('conditional-formatting.ods');
Sheet := Workbook.Sheets[1];
// Båda läsningarna löses genom samma intervall; inget expanderades
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// En överstyrning på en rad skuggar intervallet utan att dela det
Sheet.RowHeight[500000] := 36;
// Att rensa en rad inuti intervallet skär det i två bitar
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // False
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // fortfarande intervallets höjd
finally
Workbook.Free;
end;
end;
Uppslagningsordningen är den del som är värd att memorera. TXLSXWorksheet.GetRowHeight kontrollerar radlistan först och konsulterar intervallen bara när raden inte har någon explicit post, och HasRowHeight gör detsamma. Så Sheet.RowHeight[500000] := 36 rör inte intervallet alls — den lägger till en post i radlistan, och den posten vinner eftersom den slås upp först. ClearRowHeight är motsatsen: den tar bort varje radpost och anropar sedan XlsxAssignRowHeightRun med Present = False, för en rensad rad måste läsas som "ingen höjd" även om ett intervall täcker den. ClearRowHeights tömmer båda strukturerna på en gång
Vart tar de ärvda tomcellsstilarna vägen?
In i en intervallstilöverlagring per kolumn, inte i cellobjekt. OdsCommitRow avgör per kolumnvärde om det är en kompakt tomcell: raden upprepas mer än en gång, och cellen har inget värde, ingen formel och ingen formaterad text. För en kompakt tomcell skapar den en riktig cell bara på intervallets första rad, tillämpar den ärvda stilen på den och registrerar sedan samma sex stilindex — typsnitt, fyllning, ram, talformat, justering, skydd — som en StyleOverlays.Add täckande rad två till och med slutet av intervallet i den kolumnen. Rader efter den första hoppas över helt i materialiseringsloopen
Regressionstestet gör formen konkret. Efter att ha öppnat ett ark vars andra rad upprepas 1 048 575 gånger under en fet kolumnstandardstil kontrolleras att Sheet.Cells.Count är under 10, och Sheet.Cells[700000, 1].FontIndex löses fortfarande upp till det feta typsnittet — överlagringen levererar stilen i samma stund den koordinaten rörs. Det är samma mekanism som hindrar en formaterad men tom kolumn från att kosta en miljon celler på XLSX-sidan; noterna om cellagring i radblock och intervallstilöverlagringar tar upp hur överlagringar läggs i lager och löses upp. Det nya här är att ODS-importören skapar dem själv, ur upprepningsantalet, i stället för att vänta på att ett program formaterar ett område
Hur skriver SaveAsODS tillbaka upprepningsantalet?
Genom att dela upp arkets tomma svans bara där något faktiskt ändras. OdsBuildTableXml håller nu reda på två gränser: contentMaxRow, den sista raden som bär ett värde, en formel, en hyperlänk eller en manuell radbrytning, och maxRow, som dessutom sträcker sig genom enbart stilmässiga tomma celler, enradshöjder, varje intervalls LastRow och varje överlagrings underkant. En enbart stilmässig tom cell räknas inte längre som innehåll — TXLSXCells.IsStyleOnlyBlank är det som utesluter den — så den avslutande stilmässiga raden i LibreOffice-filen slutar dra innehållsgränsen till arkets botten
Ovanför contentMaxRow skrivs raderna en i taget precis som förut. Nedanför beräknar skrivaren nextRow som det minsta av: nästa intervalls FirstRow, det aktuella intervallets LastRow + 1, nästa enradshöjdspost, nästa överlagringskant och nästa materialiserade cell. Allt från den aktuella raden fram till nextRow - 1 emitteras sedan som en enda <table:table-row> med table:number-rows-repeated satt till differensen, och bär en <table:table-cell/> per kolumn med det överlagringsupplösta stilnamnet när en överlagring täcker den kolumnen. Själva radstilen kommer från TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), som nu viker in höjdtexten — 12.81pt, till exempel — i sin dedupliceringsnyckel tillsammans med flaggorna för dold rad och sidbrytning, så varje rad i intervallet delar en enda ro<N>-stil med en enda style:row-height-egenskap
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);
// Den tomma svansen skrivs som en handfull upprepade rader, inte en miljon
Workbook.SaveAsODS(Saved);
Writeln('ODS size: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.Open(Saved);
// Överstyrning, hål och intervall överlever alla rundresan
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); // intervallhöjden
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
Testet som låser fast detta kontrollerar att den sparade strömmen är under 64 kB för ett ark vars höjdintervall spänner över 1 048 575 rader med en överstyrning och ett hål stansat i mitten. Två ärliga gränser hör hemma intill den siffran. För det första sätter ett kalkylblad med datavalideringar contentMaxRow till maxRow, så valideringar stänger av svanskompakteringen på det arket och det skrivs rad för rad igen. För det andra har XLSX inget upprepningsattribut — en SpreadsheetML-<row> beskriver en rad — så export av ett intervallbackat ark till .xlsx räknar upp de rader intervallet täcker och skriver ett ht-attribut på var och en. Modellen förblir kompakt i minnet; filformatet avgör hur filen ser ut
Vad är varje radnumrerande redigering nu skyldig intervallen?
Underhåll. En ny representation av radmetadata är bara korrekt om varje operation som ändrar radnummer flyttar den tillsammans med de radlistor den sitter bredvid, och incheckningen rör var och en av de operationerna. InsertRows och DeleteRows går genom XlsxShiftRowHeightRuns, som bygger om arrayen genom att behålla den del av varje intervall som ligger före redigeringspunkten, släppa allt som hamnar inom ett borttagningsfönster och lägga tillbaka resten förskjuten med deltat — så ett intervall som klyvs av en insättning blir två intervall med ett gap, och ett intervall som klyvs av en borttagning krymper. TileRangeAxisMetadata rensar intervallen över hela det kaklade spannet och registrerar sedan om varje källintervall en gång per kopia vid sin förskjutning. TXLSXWorksheet.CopyFrom och TXLSXSheets.AddCopy tar en Copy() av arrayen i stället för att tilldela den, vilket är varför testet kan rensa alla höjder på en klon och fortfarande hitta originalarket intakt på rad 1 048 576
var
Sheet: TXLSXWorksheet;
begin
Sheet := Workbook.Sheets[1];
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// Sätt in två rader vid 500000: överstyrningen flyttar till 500002, hålet till 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // False
// Ta bort dem igen: allt skiftar tillbaka
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
// Kakla raderna 2..4 två gånger nedåt arket; intervallhöjderna följer varje kopia
Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
Writeln(Sheet.RowHeight[7]:0:2); // intervallhöjden
end;
Lässidans gränser har samma skyldighet. GetUsedRange knuffar sin underkant till varje intervalls FirstRow och LastRow, och BuildRowMajorCellOrder utökar sin metadatainkluderande maxrad genom varje intervall så att XLSX-skrivaren fortfarande besöker rader som bara har höjd. Om du någon gång lägger till en egen radnycklad struktur ovanpå HotXLS objektmodell är det här checklistan: infoga, ta bort, kakla, kopiera, använt område och varje serialiserare. Missar du en blir felet tyst — höjderna driver med antalet insatta rader, och inget kastas
Vad förblir per rad, och hur siffrorna ser ut nu
Dolda flaggor, dispositionnivåer och kollapsat tillstånd expanderar fortfarande. Radavslutaren loopar SetRowHidden och SetRowOutlineLevel en gång per upprepad rad, så ett ark som döljer en miljonradig svans, eller nästlar den i en table:table-row-group, betalar en radpost för vart och ett av de attributen. Ändringen i 2.382.2 är avgränsad till de två saker HXLS-005 faktiskt mätte — höjder och ärvda tomcellsstilar — och samma intervallteknik skulle gå att tillämpa på de andra om någon fil någonsin krävde det. ODS-läsaren agerar inte heller på style:use-optimal-row-height; en radstil som säger "optimal" och anger en höjd importeras med den höjden
Mot korpusen slutför conditional-formatting.ods nu cykeln öppna, kontrollera, spara, återöppna och kontrollera igen på 0,178 sekunder på Win32 och 0,158 sekunder på Win64, med själva öppningssteget på 0,020 sekunder, inom en budget på 60 sekunder som den tidigare sprängde. Arbetsboksgränssnitten som formatet flödar genom beskrivs i genomgången av att öppna och spara ODS-filer, och den bredare uppsättningen spakar för stora filer i prestanda för stora arbetsböcker; själva ODF-raderade elementet, med sina upprepnings- och stilattribut, specificeras i ODF 1.3 Part 3 §9.1.4
HotXLS läser och skriver XLS, XLSX och ODS från nativ Delphi- och C++Builder-kod utan Excel eller LibreOffice installerat, vilket är varför en miljonradig upprepning är något biblioteket måste modellera bra i stället för att lämna över till en extern process — sidan för HotXLS Delphi spreadsheet component listar de format och RAD Studio-versioner som stöds