HotXLS Delphi Component ukladá ODS riadok, ktorý nesie table:number-rows-repeated a výšku riadku, ako jediný záznam TXLSXRowHeightRun — prvý riadok, posledný riadok, jedna výška — namiesto jedného záznamu výšky na každý opakovaný riadok, a štýl prázdnych buniek, ktorý tie riadky dedia, zbalí do jednej intervalovej style overlay. To je celý dôvod, prečo HotXLS 2.382.2 otvorí tabuľku, ktorej koniec opakuje 1 048 530 prázdnych riadkov, za 0,02 sekundy tam, kde 2.382.1 vypršal timeout, a prečo sa ten istý súbor uloží späť do ODS s neporušeným počtom opakovaní a nie ako milión doslovných riadkov
Spomínaný súbor je obyčajný. LibreOffice Calc zapíše štrnásťstĺpcový hárok so 45 riadkami dát a potom všetko pod nimi opíše jedným elementom: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Štýl ro1 nastavuje style:row-height="0.452cm" a každý <table:table-column> nesie table:default-cell-style-name, ktorý dedia všetky prázdne bunky v tom rune. Celý content.xml má 103 KB. Nič na tom súbore nehovorí „drahý“; tá cena bola celá naša
Prečo jeden opakovaný riadok vyprší timeout pri ODS importe?
Pretože importer ho predtým rozbaľoval. Vo 2.382.1 row finisher točil SetRowHeight(RowIndex + i, RowHeight) raz na každý opakovaný riadok a každú výšku zapisoval do string listu Name=Value kľúčovaného číslom riadku. Každý insert do toho listu spustil IndexOfName lookup cez všetko, čo v ňom už bolo, takže milión výšok stál milión lineárnych prechodov — tá kvadratická list search, na ktorú bol podaný HXLS-005. V tom istom čase OdsCommitRow materializoval cell objekt pre každý stĺpec, ktorý dedil štýl, na každom z tých opakovaných riadkov, pretože štylovaná prázdna bunka sa stále počítala ako bunka
Ukladacia strana mala vlastnú verziu toho problému. Súbor z LibreOffice končí za veľkým opakovaním ešte jedným riadkom ro1, takže najvyšší štylovaný riadok sedel úplne na dne hárku a OdsBuildTableXml prešiel každý riadok až poň a emitoval elementy <table:table-row> jeden po druhom. Aj workbook, ktorý sa importoval lacno, by sa zapisoval draho. Opraviť import bez opravy exportu by timeout len presunulo, nie odstránilo
Čo je row-height run v HotXLS?
Run je to najmenšie, čím sa dá opísať „riadky 46 až 1 048 575 majú všetky výšku 12,81 bodu“ bez toho, aby sa to povedalo 1 048 530-krát. TXLSXRowHeightRun je záznam s FirstRow, LastRow a Height; TXLSXRowHeightRuns je dynamické pole takých záznamov a každý TXLSXWorksheet drží jedno v FRowHeightRuns vedľa existujúceho per-row zoznamu výšok. Pri ODS importe row finisher teraz vetví podľa počtu opakovaní: počet 1 stále volá SetRowHeight, čokoľvek väčšie volá XlsxAssignRowHeightRun raz pre celý rozsah. Rozsah sa oreže na XlsxMaxRow, čo je 1 048 576, takže počet opakovaní, ktorý hárok prekročí, sa skráti a nie odmietne
XlsxAssignRowHeightRun je jediný zapisovateľ toho poľa a drží runy bez prekrytia už z princípu konštrukcie. Keď dostane nový interval, skopíruje každý existujúci run, ktorý leží celý mimo neho, každý run, ktorý sa s ním prekrýva, rozdelí na časť pred a časť za a potom pridá nový interval, keď je Present true — alebo nepridá nič, keď je Present false, čím ClearRowHeight vysekne dieru na jeden riadok. Z toho plynú dve veci. Pole nikdy neobsahuje prekrývajúce sa intervaly, takže lookup sa môže zastaviť na prvom zásahu. A pole sa nikdy nemutuje na mieste; pri každom volaní sa postaví čerstvá kópia, čo pri týchto veľkostiach nič nestojí a odstraňuje celú triedu aliasing bugov
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Workbook := TXLSXWorkbook.Create;
try
// Hárok, ktorého koncový riadok sa opakuje 1 048 530-krát pod jedným row štýlom
Workbook.OpenODS('conditional-formatting.ods');
Sheet := Workbook.Sheets[1];
// Obe čítania sa rozlíšia cez ten istý run; nič sa nerozbaľovalo
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// Override na jeden riadok zatieni run bez toho, aby ho rozdelil
Sheet.RowHeight[500000] := 36;
// Vyčistenie jedného riadka vnútri runu rozreže run na dve časti
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // False
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // stále výška runu
finally
Workbook.Free;
end;
end;
Poradie vyhľadávania je tá časť, ktorá sa oplatí zapamätať. TXLSXWorksheet.GetRowHeight najprv skontroluje per-row zoznam a runy sa opýta len vtedy, keď riadok nemá explicitný záznam, a HasRowHeight robí to isté. Takže Sheet.RowHeight[500000] := 36 sa runu vôbec nedotkne — pridá jeden záznam do per-row zoznamu a ten záznam vyhrá, pretože sa hľadá prvý. ClearRowHeight je opak: odstráni akýkoľvek per-row záznam a potom zavolá XlsxAssignRowHeightRun s Present = False, pretože vyčistený riadok sa musí čítať ako „žiadna výška“, aj keď ho run pokrýva. ClearRowHeights vyprázdni obe štruktúry naraz
Kam idú zdedené štýly prázdnych buniek?
Do jednej intervalovej style overlay na stĺpec, nie do cell objektov. OdsCommitRow rozhoduje pri každej hodnote stĺpca, či ide o kompaktnú prázdnu bunku: riadok sa opakuje viac než raz a bunka nemá hodnotu, formulu ani rich text. Pre kompaktnú prázdnu bunku vytvorí skutočnú bunku len na prvom riadku runu, aplikuje na ňu zdedený štýl a potom zaregistruje tých istých šesť style indexov — font, fill, border, number format, alignment, protection — ako StyleOverlays.Add pokrývajúci v tom stĺpci riadky dva až po koniec runu. Riadky za prvým sa v materializačnej slučke preskočia úplne
Regresný test dáva tomu konkrétny tvar. Po otvorení hárku, ktorého druhý riadok sa opakuje 1 048 575-krát pod bold default štýlom stĺpca, sa assertuje, že Sheet.Cells.Count je pod 10, a Sheet.Cells[700000, 1].FontIndex sa stále rozlíši na bold font — overlay dodá štýl v momente, keď sa na tú súradnicu siahne. Je to ten istý mechanizmus, ktorý na strane XLSX bráni tomu, aby formátovaný ale prázdny stĺpec stál milión buniek; poznámky o row-block úložisku buniek a intervalových style overlayoch pokrývajú, ako sa overlaye vrstvia a riešia. Nové je tu to, že ich ODS importer vytvára sám, z počtu opakovaní, namiesto toho, aby čakal, kým aplikácia naformátuje rozsah
Ako SaveAsODS zapíše počet opakovaní späť?
Tým, že prázdny koniec hárku delí len tam, kde sa naozaj niečo mení. OdsBuildTableXml teraz sleduje dve hranice: contentMaxRow, teda posledný riadok, ktorý drží hodnotu, formulu, hyperlink alebo manuálny row break, a maxRow, ktorá sa navyše naťahuje cez style-only prázdne bunky, jednoriadkové výšky, LastRow každého runu a spodný okraj každého overlaya. Style-only prázdna bunka sa už nepočíta ako obsah — vylučuje ju TXLSXCells.IsStyleOnlyBlank — takže ten koncový štylovaný riadok v súbore z LibreOffice prestal ťahať hranicu obsahu na dno hárku
Nad contentMaxRow sa riadky zapisujú jeden po druhom presne ako predtým. Pod ňou writer spočíta nextRow ako najmenšiu z hodnôt: FirstRow nasledujúceho runu, LastRow + 1 aktuálneho runu, nasledujúci jednoriadkový záznam výšky, nasledujúci okraj overlaya a nasledujúca materializovaná bunka. Všetko od aktuálneho riadka po nextRow - 1 sa potom emituje ako jeden <table:table-row> s table:number-rows-repeated nastaveným na ten rozdiel, nesúci jeden <table:table-cell/> na stĺpec s menom štýlu rozlíšeným cez overlay, keď ten stĺpec overlay pokrýva. Samotný row štýl pochádza z TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), ktorý teraz skladá text výšky — povedzme 12.81pt — do svojho deduplikačného kľúča vedľa flagov hidden a page-break, takže každý riadok v rune zdieľa jeden štýl ro<N> s jedinou vlastnosťou 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);
// Prázdny koniec sa zapíše ako hŕstka opakovaných riadkov, nie milión
Workbook.SaveAsODS(Saved);
Writeln('ODS size: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.Open(Saved);
// Override, diera aj run prežijú 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); // výška runu
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
Test, ktorý to pripína, assertuje, že uložený stream je pod 64 KB pre hárok, ktorého výškový run pokrýva 1 048 575 riadkov s overridom a dierou vyseknutou v strede. K tomu číslu patria dve poctivé hranice. Po prvé, worksheet s akýmikoľvek data validations nastaví contentMaxRow na maxRow, takže validácie vypnú kompakciu konca na tom hárku a ten sa znovu zapisuje riadok po riadku. Po druhé, XLSX nemá repeat atribút — SpreadsheetML <row> opisuje jeden riadok — takže export runmi podopretého hárku do .xlsx vymenuje riadky, ktoré run pokrýva, a na každý zapíše atribút ht. Model zostáva kompaktný v pamäti; o tom, ako vyzerá súbor, rozhoduje formát
Čo teraz každá úprava prečíslúvajúca riadky dlhuje runom?
Údržbu. Nová reprezentácia row metadát je správna len vtedy, keď ju každá operácia, ktorá mení čísla riadkov, posúva spolu s per-row zoznamami, vedľa ktorých sedí, a tento commit sa dotýka každej z tých operácií. InsertRows a DeleteRows idú cez XlsxShiftRowHeightRuns, ktorý pole prestavia tak, že podrží časť každého runu ležiacu pred bodom úpravy, zahodí všetko, čo padne do vymazávacieho okna, a zvyšok pridá posunutý o deltu — takže run, ktorý pretína vloženie, sa stane dvoma runmi s medzerou, a run, ktorý pretína vymazanie, sa zmenší. TileRangeAxisMetadata vyčistí runy cez celý dlaždicovaný rozsah a potom znova zaregistruje každý zdrojový run raz na kópiu v jeho offsete. TXLSXWorksheet.CopyFrom a TXLSXSheets.AddCopy berú Copy() toho poľa namiesto priradenia, a preto test môže vyčistiť všetky výšky na klone a pôvodný hárok stále nájsť nedotknutý na riadku 1 048 576
var
Sheet: TXLSXWorksheet;
begin
Sheet := Workbook.Sheets[1];
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// Vlož dva riadky na 500000: override sa posunie na 500002, diera na 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // False
// Zmaž ich znova: všetko sa posunie späť
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
// Dlaždicuj riadky 2..4 dvakrát dole hárkom; výšky runov idú s každou kópiou
Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
Writeln(Sheet.RowHeight[7]:0:2); // výška runu
end;
Hranice na strane čítania majú tú istú povinnosť. GetUsedRange posúva svoj spodný okraj na FirstRow a LastRow každého runu a BuildRowMajorCellOrder naťahuje svoj metadata-inclusive maximálny riadok cez každý run, aby XLSX writer stále navštívil riadky, ktoré majú len výšku. Ak niekedy pridáte vlastnú štruktúru kľúčovanú riadkami na vrch object modelu HotXLS, toto je checklist: insert, delete, tile, copy, used range a každý serializér. Vynechajte jeden a zlyhanie je tiché — výšky sa posunú o počet vložení a nič nevyhodí výnimku
Čo zostáva per-row a ako teraz vyzerajú čísla
Hidden flagy, outline úrovne a collapsed stav sa stále rozbaľujú. Row finisher točí SetRowHidden a SetRowOutlineLevel raz na každý opakovaný riadok, takže hárok, ktorý skryje miliónový koniec alebo ho vnorí do table:table-row-group, platí per-row záznam za každý z tých atribútov. Zmena vo 2.382.2 je ohraničená na dve veci, ktoré HXLS-005 naozaj meral — výšky a zdedené štýly prázdnych buniek — a tá istá run technika by sa dala použiť aj na ostatné, keby to nejaký súbor vyžadoval. ODS reader tiež nekoná podľa style:use-optimal-row-height; row štýl, ktorý hovorí „optimal“ a dáva výšku, sa importuje s tou výškou
Proti corpusu teraz conditional-formatting.ods dokončí cyklus open, assert, save, reopen a re-assert za 0,178 sekundy na Win32 a 0,158 sekundy na Win64, pričom samotná open fáza má 0,020 sekundy, a to v rámci 60-sekundového rozpočtu, ktorý predtým vyčerpal. Workbook-level rozhrania, ktorými formát preteká, opisuje walkthrough otvárania a ukladania ODS súborov, a širšiu sadu pák pre veľké súbory výkon veľkých workbookov; samotný ODF row element s jeho repeat a style atribútmi špecifikuje ODF 1.3 Part 3 §9.1.4
HotXLS číta a zapisuje XLS, XLSX a ODS z natívneho Delphi a C++Builder kódu bez nainštalovaného Excelu či LibreOffice, a preto je miliónové opakovanie riadkov niečo, čo musí knižnica dobre namodelovať, a nie odovzdať externému procesu — stránka HotXLS Delphi spreadsheet component uvádza podporované formáty a verzie RAD Studia