HotXLS Delphi Component съхранява ODS ред, който носи table:number-rows-repeated и височина на ред, като един-единствен запис TXLSXRowHeightRun — първи ред, последен ред, една височина — вместо по един height запис на повторен ред, и сгъва стила на празните клетки, който тези редове наследяват, в един-единствен interval style overlay. Това е цялата причина HotXLS 2.382.2 да отваря таблица, чиен опашен участък повтаря 1,048,530 празни реда за 0.02 секунди, където 2.382.1 изтече, и защо същият файл се записва обратно в ODS с repeat count-а запазен, а не като милион буквални редове
Файлът става дума за обикновен. LibreOffice Calc записва лист с четиринадесет колони и 45 реда данни, после описва всичко под тях с един елемент: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Стилът ro1 задава style:row-height="0.452cm", а всяко <table:table-column> носи table:default-cell-style-name, който всяка празна клетка в run-а наследява. Целият content.xml е 103 KB. Нищо във файла не казва „скъпо“; скъпо беше изцяло наше
Защо един повторен ред кара ODS импорт да изтече?
Защото importer-ът го разъвижеше. В 2.382.1 завършващият редове цикъл въртеше SetRowHeight(RowIndex + i, RowHeight) по веднъж на повторен ред, записвайки всяка височина в списък от string-ове Name=Value, ключован по номер на ред. Всяко вмъкване в този списък пускаше IndexOfName търсене върху всичко, вече вътре в него, така че милион височини струваха милион линейни сканирания — квадратичното списъчно търсене, срещу което беше подаден HXLS-005. По същото време OdsCommitRow материализираше клетъчен обект за всяка колона, наследила стил, на всеки един от повторените редове, защото стилизирана празна клетка все пак броеше за клетка
Страната на записа имаше собствена версия на проблема. LibreOffice файлът свършва с още един ro1 ред след големия repeat, така че най-високият стилизиран ред седеше най-долу на листа, а OdsBuildTableXml обхождаше всеки ред до него, излъчвайки елементи <table:table-row> по един. Дори работна книга, която беше импортирана евтино, щеше да се запише скъпо. Поправката на импорта без поправката на export-а щеше да премести timeout-а, не да го премахне
Какво е row-height run в HotXLS?
Run е най-малкото нещо, което може да опише „редове 46 до 1,048,575 са всички високи 12.81 пункта“, без да го казва 1,048,530 пъти. TXLSXRowHeightRun е запис от FirstRow, LastRow и Height; TXLSXRowHeightRuns е динамичен масив от тях, а всеки TXLSXWorksheet държи един в FRowHeightRuns до съществуващия per-row списък с височини. При ODS импорт завършващият редове код вече разклонява по repeat count-а: бройка 1 все още вика SetRowHeight, всяка по-голяма вика XlsxAssignRowHeightRun веднъж за целия участък. Участъкът се ограничава до XlsxMaxRow, който е 1,048,576, така че repeat count, който прелита листа, се съкращава, а не се отхвърля
XlsxAssignRowHeightRun е единственият писач на масива, и той държи run-овете несъприкосновени по конструкция. При нов интервал той копира всеки съществуващ run, лежащ изцяло извън него, разрязва всеки run, който го застъпва, на парчето преди и парчето след, после добавя новия интервал, когато Present е true — или не добавя нищо, когато Present е false, което е начинът, по който ClearRowHeight пробива едноредова дупка. От това следват две неща. Масивът никога не съдържа застъпващи се интервали, така че търсене може да спре на първото попадение. И масивът никога не се мутира на място; свежо копие се изгражда при всяко извикване, което струва нищо при замесените размери и премахва една цяла класа aliasing бъгове
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Workbook := TXLSXWorkbook.Create;
try
// Лист, чиен опашен ред се повтаря 1,048,530 пъти под един row стил
Workbook.OpenODS('conditional-formatting.ods');
Sheet := Workbook.Sheets[1];
// И двете четения се разрешават през същия run; нищо не е разъвито
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// Едноредов override закрива run-а, без да го разделя
Sheet.RowHeight[500000] := 36;
// Изчистването на един ред вътре в run-а го разрязва на две части
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // False
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // все още run височината
finally
Workbook.Free;
end;
end;
Редът на търсене е частта, която си заслужава да се запамети. TXLSXWorksheet.GetRowHeight проверява първо per-row списъка и пита run-овете само когато редът няма изричен запис, а HasRowHeight прави същото. Така Sheet.RowHeight[500000] := 36 изобщо не пипа run-а — той добавя един запис към per-row списъка, а този запис печели, защото се търси пръв. ClearRowHeight е обратното: той премахва всеки per-row запис и после вика XlsxAssignRowHeightRun с Present = False, защото изчистен ред трябва да се чете като „няма височина“, дори някой run да го покрива. ClearRowHeights изпразва и двете структури наведнъж
Накъде отиват наследените стилове на празните клетки?
В по един interval style overlay на колона, не в клетъчни обекти. OdsCommitRow решава на колона-стойност дали тя е компактна празна: редът се повтаря повече от веднъж, клетката няма стойност, няма формула и няма rich text. За компактна празна той създава истинска клетка само на първия ред на run-а, прилага наследения стил върху нея, после регистрира същите шест стилови индекса — шрифт, запълване, рамка, числов формат, подравняване, защита — като StyleOverlays.Add, покриващ редове два до края на run-а в тази колона. Редовете след първия се прескачат изцяло в цикъла на материализация
Регресионният тест прави формата конкретна. След отваряне на лист, чиен втори ред се повтаря 1,048,575 пъти под bold column стил по подразбиране, Sheet.Cells.Count се assert-ва под 10, а Sheet.Cells[700000, 1].FontIndex все още се разрешава до bold шрифта — overlay-ът доставя стила в момента, в който координатата се докосне. Това е същият механизъм, който не позволява на форматирана-но-празна колона да струва милион клетки на XLSX страната; бележките за row-block клетъчно съхранение и interval style overlays обхващат как overlay-ите се наслояват и разрешават. Новото тук е, че ODS importer-ът ги създава сам, от repeat count-а, вместо да чака приложение да форматира диапазон
Как SaveAsODS записва repeat count-а обратно?
Чрез разрязване на празната опашка на листа само там, където нещо действително се сменя. OdsBuildTableXml вече следи две граници: contentMaxRow — последният ред, който държи стойност, формула, хипервръзка или ръчен break на ред — и maxRow, който допълнително се простира през стил-само празни клетки, едноредови височини, LastRow на всеки run и долния ръб на всеки overlay. Стил-само празна клетка вече не брои за съдържание — TXLSXCells.IsStyleOnlyBlank е това, което я изключва — така че завършващият стилизиран ред в LibreOffice файла спира да дърпа границата на съдържанието към дъното на листа
Над contentMaxRow редовете се записват по един точно както преди. Под него writer-ът изчислява nextRow като най-малкото от: FirstRow на следващия run, LastRow + 1 на текущия run, следващия едноредов height запис, следващия overlay ръб и следващата материализирана клетка. Всичко от текущия ред до nextRow - 1 се излъчва после като едно <table:table-row> с table:number-rows-repeated, зададен на разликата, носещо по едно <table:table-cell/> на колона с overlay-разрешеното име на стил, когато overlay покрива тази колона. Стилът на реда самият идва от TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), който вече сгъва текста на височината — да речем 12.81pt — в ключа си за дедупликация до скрития и page-break флаговете, така че всеки ред в run-а споделя един ro<N> стил с единствено свойство 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);
// Празната опашка се записва като шепа повторени редове, не милион
Workbook.SaveAsODS(Saved);
Writeln('ODS size: ', Saved.Size, ' bytes');
Saved.Position := 0;
Reopened.Open(Saved);
// Override, дупката и run-ът всички оцеляват през 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); // run височина
finally
Saved.Free;
Reopened.Free;
Workbook.Free;
end;
end;
Тестът, който закова това, assert-ва, че записаният поток е под 64 KB за лист, чиен height run обхваща 1,048,575 реда с override и дупка, пробита в средата. Две честни граници седят до това число. Първо, worksheet с каквито и да е data validations задава contentMaxRow на maxRow, така че validations изключват tail компресията на този лист и той отново се записва ред по ред. Второ, XLSX няма repeat атрибут — SpreadsheetML <row> описва един ред — така че export на лист, опрян на run, към .xlsx изброява редовете, които run-ът покрива, и записва атрибут ht на всеки. Моделът остава компактен в паметта; файловият формат решава как изглежда файлът
Какво дължат на run-овете всички редакции с преименуване на редове?
Поддръжка. Ново представяне на метаданни за редове е коректно само ако всяка операция, която сменя номерата на редовете, ги мести заедно с per-row списъците, до които седят, и commit-ът докосва всяка от тези операции. InsertRows и DeleteRows минават през XlsxShiftRowHeightRuns, който възстановява масива като пази частта на всеки run, лежаща пред точката на редакция, изпуска каквото падне вътре в изтривален прозорец и пресъздава остатъка, изместен с делтата — така run, който обхваща мястото на вмъкване, става два run-а с пролука, а run, който обхваща мястото на изтриване, се свива. TileRangeAxisMetadata изчиства run-овете из целия тилиран участък, после пре-регистрира всеки source run по веднъж на копие на неговото отместване. TXLSXWorksheet.CopyFrom и TXLSXSheets.AddCopy вземат Copy() на масива, вместо да го присвояват, което е причината тестът да може да изчисти всички височини на клонинг и все пак да намери оригиналния лист непокътнат на ред 1,048,576
var
Sheet: TXLSXWorksheet;
begin
Sheet := Workbook.Sheets[1];
Sheet.RowHeight[500000] := 36;
Sheet.ClearRowHeight(500001);
// Вмъкни два реда на 500000: override-ът отива на 500002, дупката на 500003
Sheet.InsertRows(500000, 2);
Writeln(Sheet.RowHeight[500002]:0:2); // 36.00
Writeln(Sheet.HasRowHeight(500003)); // False
// Изтрий ги пак: всичко се измества обратно
Sheet.DeleteRows(500000, 2);
Writeln(Sheet.RowHeight[500000]:0:2); // 36.00
// Размножи редове 2..4 два пъти надолу по листа; run височините следват всяко копие
Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
Writeln(Sheet.RowHeight[7]:0:2); // run височината
end;
Границите от страна на четене имат същото задължение. GetUsedRange качва долния си ръб до FirstRow и LastRow на всеки run, а BuildRowMajorCellOrder разпростира своя максимум-ред-включващ-метаданни през всеки run, така че XLSX writer-ът все още посещава редове само-с-височина. Ако някога добавите собствена ред-ключована структура върху обектния модел на HotXLS, това е чеклистът: вмъкване, изтриване, tile, копиране, used range и всеки сериализатор. Пропуснете едното и провалът е тих — височините се отместват с броя вмъквания, и нищо не хвърля
Какво остава per-row и как изглеждат числата сега
Скритите флагове, outline нивата и свитото състояние все още се разъвиват. Завършващият редове цикъл върти SetRowHidden и SetRowOutlineLevel по веднъж на повторен ред, така че лист, който крие милион-редова опашка, или я влага в table:table-row-group, плаща per-row запис за всеки от тези атрибути. Променята 2.382.2 е обхваната до двете неща, които HXLS-005 действително измери — височини и наследени празни стилове — а същата run техника би се приложила към останалите, ако някой файл някога го поиска. ODS reader-ът освен това не действа по style:use-optimal-row-height; row стил, който казва „optimal“ и дава височина, се импортира с тази височина
Срещу corpus-а conditional-formatting.ods вече завършва цикъла отваряне, assert, запис, повторно отваряне и повторно assert за 0.178 секунди на Win32 и 0.158 секунди на Win64, като самият етап на отваряне е 0.020 секунди, в 60-секунден бюджет, който преди изчерпваше. Интерфейсите на ниво работна книга, през които форматът тече, са описани в разходката из отваряне и запис на ODS файлове, а по-широкият набор от лостове за големи файлове — в производителността при големи работни книги; самият ODF ред-елемент, с неговите repeat и style атрибути, е специфициран в ODF 1.3 Part 3 §9.1.4
HotXLS чете и записва XLS, XLSX и ODS от нативен Delphi и C++Builder код без инсталирани Excel или LibreOffice, което е причината милион-редов repeat да е нещо, което библиотеката трябва да моделира добре, а не да предава на външен процес — страницата на HotXLS Delphi spreadsheet компонента изброява поддържаните формати и RAD Studio версии