Το HotXLS Delphi Component αποθηκεύει μια ODS γραμμή που κουβαλάει table:number-rows-repeated και ύψος γραμμής ως ένα μεμονωμένο record TXLSXRowHeightRun — πρώτη γραμμή, τελευταία γραμμή, ένα ύψος — αντί για μία εγγραφή ύψους ανά επαναλαμβανόμενη γραμμή, και διπλώνει το style κενών κελιών που εκείνες οι γραμμές κληρονομούν σε ένα μεμονωμένο interval style overlay. Αυτή είναι όλη η αιτία που το HotXLS 2.382.2 ανοίγει ένα spreadsheet του οποίου η ουρά επαναλαμβάνει 1,048,530 κενές γραμμές σε 0.02 δευτερόλεπτα όπου το 2.382.1 κόλλωνε, και γιατί το ίδιο αρχείο σώζεται πίσω σε ODS με το repeat count ανέπαφο και όχι ως ένα εκατομμύριο κυριολεκτικές γραμμές
Το αρχείο στη συγκεκριμένη περίπτωση είναι συνηθισμένο. Το LibreOffice Calc γράφει ένα sheet δεκατεσσάρων στηλών με 45 γραμμές δεδομένων, και μετά περιγράφει όλα όσα είναι από κάτω τους με ένα στοιχείο: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Το style ro1 θέτει style:row-height="0.452cm", και κάθε <table:table-column> κουβαλάει table:default-cell-style-name που κάθε κενό κελί του run κληρονομεί. Ολόκληρο το content.xml είναι 103 KB. Τίποτα στο αρχείο δεν λέει «ακριβό»· το κόστος ήταν εξ ολοκλήρου δικό μας
Γιατί μία επαναλαμβανόμενη γραμμή κόλλωνε ένα ODS import;
Γιατί ο importer την άπλωνε. Στο 2.382.1 ο row finisher επανελαμβάνετο SetRowHeight(RowIndex + i, RowHeight) μία φορά ανά επαναλαμβανόμενη γραμμή, γράφοντας κάθε ύψος σε λίστα strings Name=Value με κλειδί τον αριθμό γραμμής. Κάθε εισαγωγή σε εκείνη τη λίστα έτρεχε lookup IndexOfName πάνω σε όλα όσα περιείχε ήδη, οπότε ένα εκατομμύριο ύψη κόστιζαν ένα εκατομμύριο γραμμικές σαρώσεις — η τετραγωνική αναζήτηση λίστας κατά της οποίας κατατέθηκε το HXLS-005. Ταυτόχρονα το OdsCommitRow υλοποιούσε ένα cell object για κάθε στήλη που κληρονομούσε style, σε κάθε μία από τις επαναλαμβανόμενες γραμμές, γιατί ένα στυλάτο κενό κελί εξακολουθούσε να μετράει ως κελί
Η πλευρά του save είχε τη δική της εκδοχή του προβλήματος. Το αρχείο LibreOffice τελειώνει με μία ακόμα γραμμή ro1 μετά τη μεγάλη επανάληψη, οπότε η υψηλότερη στυλάτη γραμμή καθόταν στον πάτο του sheet, και το OdsBuildTableXml περπατούσε κάθε γραμμή μέχρι εκείνη εκπέμποντας elements <table:table-row> μία τη φορά. Ακόμα κι ένα workbook που είχε γίνει import φτηνά θα είχε γραφτεί ακριβά. Η διόρθωση του import χωρίς τη διόρθωση του export θα είχε μετακινήσει το timeout, όχι να το αφαιρέσει
Τι είναι ένα row-height run στο HotXLS;
Ένα run είναι το πιο μικρό πράγμα που μπορεί να περιγράψει «οι γραμμές 46 έως 1,048,575 είναι όλες 12.81 πόντους» χωρίς να το πει 1,048,530 φορές. Το TXLSXRowHeightRun είναι record από FirstRow, LastRow και Height· το TXLSXRowHeightRuns είναι dynamic array από αυτά, και κάθε TXLSXWorksheet κρατά ένα στο FRowHeightRuns δίπλα στην υπάρχουσα λίστα ύψους ανά γραμμή. Στο ODS import ο row finisher κάνει τώρα branch πάνω στο repeat count: πλήθος 1 εξακολουθεί να καλεί SetRowHeight, οτιδήποτε μεγαλύτερο καλεί XlsxAssignRowHeightRun μία φορά για όλο το εύρος. Το εύρος γίνεται clamp στο XlsxMaxRow, που είναι 1,048,576, οπότε ένα repeat count που ξεπερνά το sheet περικόπτεται αντί να απορριφθεί
Το XlsxAssignRowHeightRun είναι ο μόνο writer του array, και κρατάει τα runs ασυνένωτα εκ κατασκευής. Δοσμένο ένα νέο interval αντεγράφει κάθε υπάρχον run που βρίσκεται ολότελα έξω από αυτό, σχίζει κάθε run που επικαλύπτεται σε κομμάτι πριν και κομμάτι μετά, και μετά προσαρτά το νέο interval όταν το Present είναι True — ή δεν προσαρτά τίποτα όταν το Present είναι False, που είναι ο τρόπος που το ClearRowHeight ανοίγει τρύπα μίας γραμμής. Δύο πράγματα ακολουθούν. Το array δεν περιέχει ποτέ επικαλυπτόμενα intervals, οπότε ένα lookup μπορεί να σταματήσει στο πρώτο χτύπημα. Και το array δεν μεταλλάσσεται ποτέ στη θέση του· φρέσκο αντίγραφο χτίζεται σε κάθε κλήση, που δεν κοστίζει τίποτα στα μεγέθη που παίζουν και αφαιρεί μια ολόκληρη κατηγορία bugs aliasing
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Workbook := TXLSXWorkbook.Create;
try
// Ένα sheet του οποίου η tail γραμμή επαναλαμβάνεται 1,048,530 φορές κάτω από row style
Workbook.OpenODS('conditional-formatting.ods');
Sheet := Workbook.Sheets[1];
// Και τα δύο reads λύνονται μέσα από το ίδιο run· τίποτα δεν απλώθηκε
Writeln(Sheet.RowHeight[46]:0:2, ' pt');
Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
// Ένα override μονής γραμμής σκιάζει το run χωρίς να το σχίζει
Sheet.RowHeight[500000] := 36;
// Το σβήσιμο μιας γραμμής μέσα στο run κόβει το run σε δύο κομμάτια
Sheet.ClearRowHeight(500001);
Writeln(Sheet.HasRowHeight(500001)); // False
Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // ακόμα το ύψος του run
finally
Workbook.Free;
end;
end;
Η σειρά lookup είναι το μέρος που αξίζει να το αποστηθίσεις. Το TXLSXWorksheet.GetRowHeight τεστάρει πρώτα τη λίστα ανά γραμμή και συμβουλεύεται τα runs μόνο όταν η γραμμή δεν έχει explicit entry, και το HasRowHeight κάνει το ίδιο. Οπότε το Sheet.RowHeight[500000] := 36 δεν αγγίζει καθόλου το run — προσθέτει ένα entry στη λίστα ανά γραμμή, και εκείνο το entry κερδίζει γιατί ψάχνεται πρώτο. Το ClearRowHeight είναι το αντίστροφο: αφαιρεί κάθε entry ανά γραμμή και μετά καλεί XlsxAssignRowHeightRun με Present = False, γιατί μια σβησμένη γραμμή πρέπει να διαβάζεται «χωρίς ύψος» ακόμα κι αν ένα run την καλύπτει. Το ClearRowHeights αδειάζει και τις δύο δομές μονομιάς
Πού πάνε τα κληρονομημένα styles κενών κελιών;
Σε ένα interval style overlay ανά στήλη, όχι σε cell objects. Το OdsCommitRow αποφασίζει ανά τιμή στήλης αν είναι συμπαγές κενό: η γραμμή επαναλαμβάνεται περισσότερες από μία, το κελί δεν έχει τιμή, ούτε τύπο ούτε rich text. Για ένα συμπαγές κενό δημιουργεί πραγματικό κελί μόνο στην πρώτη γραμμή του run, εφαρμόζει πάνω του το κληρονομημένο style, και μετά καταχωρεί τους ίδιους έξι style indexes — font, fill, border, number format, alignment, protection — ως StyleOverlays.Add που καλύπτει τις γραμμές δύο έως το τέλος του run σε εκείνη τη στήλη. Οι γραμμές μετά την πρώτη παραλείπονται ολότελα στο loop υλοποίησης
Το regression test κάνει το σχήμα συγκεκριμένο. Μετά το άνοιγμα sheet του οποίου η δεύτερη γραμμή επαναλαμβάνεται 1,048,575 φορές κάτω από έντονο default style στήλης, ισχυρίζεται ότι το Sheet.Cells.Count είναι κάτω από 10, και το Sheet.Cells[700000, 1].FontIndex εξακολουθεί να λύνεται στο bold font — το overlay προμηθεύει το style τη στιγμή που αγγίζεται εκείνη η συντεταγμένη. Αυτός είναι ο ίδιος μηχανισμός που εμποδίζει μια μορφοποιημένη αλλά άδεια στήλη να κοστίσει ένα εκατομμύριο κελιά στην πλευρά XLSX· οι σημειώσεις για την αποθήκευση κελιών row-block και τα interval style overlays καλύπτουν πώς τα overlays στρώνονται και λύνονται. Αυτό που είναι νέο εδώ είναι ότι ο ODS importer τα δημιουργεί από μόνος του, από το repeat count, αντί να περιμένει μια εφαρμογή να μορφοποιήσει μια περιοχή
Πώς γράφει το SaveAsODS το repeat count πίσω;
Χωρίζοντας την κενή ουρά του sheet μόνο εκεί που κάτι αλλάζει πράγματι. Το OdsBuildTableXml παρακολουθεί πλέον δύο όρια: το contentMaxRow, την τελευταία γραμμή που κρατά τιμή, τύπο, hyperlink ή χειροκίνητο row break, και το maxRow, που επιπλέον απλώνεται μέσα από style-only κενά κελιά, ύψη μονής γραμμής, το LastRow κάθε run και το κάτω άκρο κάθε overlay. Ένα style-only κενό κελί δεν μετράει πια ως περιεχόμενο — το TXLSXCells.IsStyleOnlyBlank είναι αυτό που το εξαιρεί — οπότε η trailing στυλάτη γραμμή στο αρχείο LibreOffice σταματά να σέρνει το όριο περιεχομένου στον πάτο του sheet
Πάνω από το contentMaxRow οι γραμμές γράφονται μία τη φορά ακριβώς όπως πριν. Κάτω από αυτό ο writer υπολογίζει nextRow ως το ελάχιστο από: το FirstRow του επόμενου run, το LastRow + 1 του τρέχοντος run, το επόμενο entry ύψους μονής γραμμής, το επόμενο άκρο overlay, και το επόμενο υλοποιημένο κελί. Όλα από την τρέχουσα γραμμή έως το nextRow - 1 εκπέμπονται τότε ως ένα <table:table-row> με table:number-rows-repeated στη διαφορά, κουβαλώντας ένα <table:table-cell/> ανά στήλη με το όνομα style λυμένο από overlay όταν ένα overlay καλύπτει εκείνη τη στήλη. Το ίδιο το row style προέρχεται από το TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), που διπλώνει πλέον το text ύψους — ας πούμε 12.81pt — μέσα στο κλειδί απο-διπλοεγγραφής του δίπλα στα flags hidden και page-break, ώστε κάθε γραμμή του run να μοιράζεται ένα style 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;
Το test που το καρφώνει ισχυρίζεται ότι το saved stream είναι κάτω από 64 KB για sheet του οποίου το height run απλώνεται σε 1,048,575 γραμμές με ένα override και μια τρύπα ανοιγμένη στη μέση. Δύο τίμια όρια ανήκουν δίπλα σε εκείνο τον αριθμό. Πρώτον, ένα worksheet με οποιεσδήποτε data validations θέτει contentMaxRow στο maxRow, οπότε οι validations απενεργοποιούν τη συμπίεση ουράς σε εκείνο το sheet και ξαναγράφεται γραμμή τη γραμμή. Δεύτερον, το XLSX δεν έχει repeat attribute — ένα <row> SpreadsheetML περιγράφει μία γραμμή — οπότε το export ενός sheet σε .xlsx στηριγμένου σε runs απαριθμεί τις γραμμές που καλύπτει το run και γράφει attribute ht σε καθεμία. Το model μένει συμπαγές στη μνήμη· η μορφή αρχείου αποφασίζει πώς φαίνεται το αρχείο
Τι χρωστάνουν πια στα runs όλες οι επεξεργασίες επαναρίθμησης γραμμών;
Συντήρηση. Μια νέα αναπαράσταση metadata γραμμών είναι σωστή μόνο αν κάθε λειτουργία που αλλάζει αριθμούς γραμμών τη μετακινεί μαζί με τις λίστες ανά γραμμή δίπλα στις οποίες κάθεται, και το commit αγγίζει καθεμία από εκείνες τις λειτουργίες. Το InsertRows και το DeleteRows περνούν από το XlsxShiftRowHeightRuns, που ξαναχτίζει το array κρατώντας το τμήμα κάθε run που βρίσκεται πριν από το σημείο επεξεργασίας, πετώντας ό,τι πέφτει μέσα σε παράθυρο διαγραφής, και ξαναπροσθέτοντας το υπόλοιπο μετατοπισμένο κατά τη διαφορά — οπότε ένα run που καβαλάει μια εισαγωγή γίνεται δύο runs με κενό, και ένα run που καβαλάει μια διαγραφή μικραίνει. Το TileRangeAxisMetadata σβήνει τα runs σε όλο το πλακόστρωτο εύρος, και μετά ξανακαταχωρεί κάθε source run μία φορά ανά αντίγραφο στο offset του. Το TXLSXWorksheet.CopyFrom και το TXLSXSheets.AddCopy παίρνουν Copy() του array αντί να το αναθέτουν, γι' αυτό το test μπορεί να σβήσει όλα τα ύψη σε έναν clone και να βρει ακόμα το πρωτότυπο sheet ανέπαφο στη γραμμή 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 δύο φορές κάτω στο sheet· τα ύψη run ακολουθούν κάθε αντίγραφο
Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
Writeln(Sheet.RowHeight[7]:0:2); // το ύψος του run
end;
Τα όρια στην πλευρά ανάγνωσης έχουν την ίδια υποχρέωση. Το GetUsedRange ανεβάζει το κάτω άκρο του στο FirstRow και LastRow κάθε run, και το BuildRowMajorCellOrder απλώνει τη μέγιστη γραμμή του με τα metadata μέσα από κάθε run ώστε ο XLSX writer να εξακολουθεί να επισκέπτεται γραμμές μόνο-με-ύψος. Αν προσθέσεις ποτέ μια δική σου δομή με κλειδί γραμμές πάνω στο object model του HotXLS, αυτή είναι η λίστα ελέγχου: insert, delete, tile, copy, used range, και κάθε serializer. Χάσεις ένα και η αποτυχία είναι σιωπηλή — τα ύψη ξεγλιστράνε κατά το πλήθος της εισαγωγής, και τίποτα δεν πετάει exception
Τι μένει ανά γραμμή, και πώς δείχνουν οι αριθμοί τώρα
Τα flags hidden, τα outline levels και η κατάσταση collapsed εξακολουθούν να απλώνονται. Ο row finisher επαναλαμβάνει SetRowHidden και SetRowOutlineLevel μία φορά ανά επαναλαμβανόμενη γραμμή, οπότε ένα sheet που κρύβει ουρά ενός εκατομμυρίου γραμμών, ή τη φωλιάζει μέσα σε table:table-row-group, πληρώνει entry ανά γραμμή για καθεμία από εκείνες τις ιδιότητες. Η αλλαγή του 2.382.2 έχει scope τα δύο πράγματα που το HXLS-005 πράγματι μέτρησε — ύψη και κληρονομημένα κενά styles — και η ίδια τεχνική run θα εφαρμοζόταν στα υπόλοιπα αν κάποιο αρχείο το απαιτούσε ποτέ. Ο ODS reader επίσης δεν ενεργεί πάνω στο style:use-optimal-row-height· ένα row style που λέει «optimal» και δίνει ύψος γίνεται import με εκείνο το ύψος
Απέναντι στο corpus, το conditional-formatting.ods ολοκληρώνει πλέον τον κύκλο open, assert, save, reopen και re-assert σε 0.178 δευτερόλεπτα σε Win32 και 0.158 σε Win64, με το ίδιο το στάδιο open στα 0.020 δευτερόλεπτα, μέσα σε προϋπολογισμό 60 δευτερολέπτων που προηγουμένως εξάντλησε. Τα interfaces επιπέδου workbook μέσα από τα οποία ρέει η μορφή περιγράφονται στην ανάλυση του ανοίγματος και σώματος αρχείων ODS, και το ευρύτερο σύνολο μοχλών για μεγάλα αρχεία στο large workbook performance· το ίδιο το ODF row element, με τα attributes repeat και style του, προδιαγράφεται στο ODF 1.3 Part 3 §9.1.4
Το HotXLS διαβάζει και γράφει XLS, XLSX και ODS από native Delphi και C++Builder κώδικα χωρίς εγκατεστημένο Excel ή LibreOffice, γι' αυτό μια επανάληψη εκατομμυρίου γραμμών είναι κάτι που η βιβλιοθήκη πρέπει να μοντελοποιήσει καλά κι όχι να παραχωρήσει σε εξωτερική διαδικασία — η σελίδα του HotXLS Delphi spreadsheet component απαριθμεί τις υποστηριζόμενες μορφές και εκδόσεις RAD Studio