Een rapportagetaak draait een jaar lang prima. Hij bouwt een werkmap, vult een blad met wat de query ook maar retourneert, en slaat deze op. Vervolgens vraagt een klant met vijf jaar historie om een volledige export, het aantal rijen passeert het miljoen, en het proces sterft af met een out-of-memory-fout, lang voordat het bestand de schijf bereikt. Er was niets mis met de code. Hij hield gewoon de volledige werkmap in het RAM-geheugen vast, zodat hij deze aan het einde kon serialiseren, en het geheugen dat hij nodig had groeide in de pas met het aantal rijen dat hij geacht werd weg te schrijven
De oplossing is niet een grotere machine. Het is een ander schrijfmodel. De direct streamende writer in HotXLS zendt (emits) het OOXML-pakket stapsgewijs uit naarmate rijen arriveren, dus de hoeveelheid geheugen die hij gebruikt is niet afhankelijk van hoeveel rijen u schrijft. Het is de tegenhanger aan de schrijfkant van de streaming reader: waar de reader een reusachtig blad doorloopt zonder een cellenboom op te bouwen, produceert de writer er één, tevens zonder een cellenboom op te bouwen
Waarom het normale opslagpad met de data meegroeit
Het reguliere TXLSXWorkbook-pad bouwt eerst een volledig objectmodel op. Elke cel, met zijn waarde, type en stijlreferentie, leeft als een object in het geheugen totdat u op save (opslaan) aanroept, op welk moment de gehele boomstructuur in het pakket geserialiseerd wordt. Dat model is het juiste wanneer u een blad wilt lezen, bewerken, herberekenen, en terug wilt schrijven, want willekeurige toegang (random access) tot een willekeurige cel is nu precies wat er voor bewerkingen nodig is. Het is het verkeerde model wanneer u louter in één richting rijen aan het ingieten bent en nooit meer omkijkt, omdat u ervoor betaalt om iedere rij resident te houden zonder dat daar een voordeel tegenover staat. Een miljoen rijen aan objecten blijft een miljoen rijen aan objecten, of u ze nu wel of niet opnieuw bekijkt
De streaming writer verwijdert de boom. Zodra een cel is geschreven, worden het bytes in het worksheet-deel, en die bytes worden overgedragen aan de zip-uitvoer. De worksheet-stream is de enige buffer die groeit, en die groeit aan de uitvoerkant, niet als levende Delphi-objecten op de heap (heap). Wat resident blijft is een vaste hoeveelheid aan administratie: de bladnamen, een paar vlaggetjes, het huidige rijnummer, een cel-teller. Die set verandert niet tussen rij één en rij tien miljoen
De shared-string tabel is de valstrik, en inline strings bieden de uitweg
De meeste streamende XLSX-writers doen het goed totdat ze tekst tegenkomen. Het OOXML-formaat slaat strings normaliter op in een tabel met gedeelde strings (shared-string table): elke unieke string wordt eenmalig weggeschreven naar een afzonderlijk deel, en iedere cel die de desbetreffende string bevat, draagt een index naar de tabel met zich mee in plaats van de tekst zelf. Het is een goede ruimtelijke optimalisatie voor bestanden vol met zich herhalende labels, en dit is de standaard die door het standaard opslagpad (save path) gebruikt wordt. Het probleem voor een streaming writer is bruut. Om te ontdubbelen, moet de tabel voor de hele klus in het geheugen (resident) blijven, want een rij die nog moet komen, herhaalt mogelijk een string uit een rij die reeds geschreven is, en enkel een complete in-memory kaart van geziene strings kan de juiste index toekennen. Dus uitgerekend de ene structuur die een streaming writer niet kan streamen, is juist de structuur die verondersteld wordt het bestand klein te houden. Data met zware teksten verijdelt exact het streamen waar u nu juist voor kwam
De direct writer omzeilt de tabel volledig. Strings worden inline geschreven, als t="inlineStr" cellen waarvan de tekst rechtstreeks binnenin de cel zit, samen met een <is><t> element. Er is geen tabel die accumuleert en geen in-memory map met reeds waargenomen strings om vast te houden, zodat tekstkolommen niet meer geheugen kosten dan numerieke kolommen. De afweging is expliciet en het is de moeite waard om deze ronduit te benoemen. Inline strings herhalen exact dezelfde tekst op de plek waar deze voorkomt, dus een bestand met talloze identieke labels is groter op de schijf dan de tegenhanger met een shared-string. U levert op de bestandsgrootte in om een constant geheugen (constant memory) te kopen. Bij een one-pass export is dit de juiste kant van de afweging, en zipcompressie (zip compression) absorbeert overigens toch veel van de herhaling aan de uitgang
De stijltabel komt aan het einde, voorzien van één datumopmaak
Stijlen creëren hetzelfde spanningsveld als strings. Een werkmap verwijst naar de opmaak ervan door middel van een stijlen-deel (styles part), en een streaming writer is niet in staat om een groeiend smaldeel (palette) aan stijlen in de pas te laten lopen met cellen die reeds zijn weggeschreven (flushed). De direct writer tackelt dit door de stijltabel klein en vaststaand (fixed) te houden, en door hem bij de afsluiting (close) uit te zenden, en niet aan de start (up front). Één standaard celopmaak dekt de normale cellen af. Één opmaak voor het datum-nummer (date number format) dekt data af, vastgelegd met een opmaakcode van yyyy-mm-dd op een bekende positie in de celopmaaklijst
Die datumopmaak is de reden waarom WriteDateTime als afzonderlijke aanroep bestaat. Excel kent geen native datum-type; een datum is een getal dat de opmaak van een datum draagt. WriteDateTime schrijft de waarde op als een gewoon serie-nummer en tagt de cel met die ene datum-stijl, waardoor het spreadsheet deze als een datum rendert en niet als een 5-cijferig geheel getal (integer). De seriële waarde die opgeschreven wordt, is van wezenlijk belang om dit via round-tripping weer te kunnen inlezen. Deze slaat de TDateTime waarde linea recta op volgens het 1900 date system, en dit is exact dezelfde conventie die bij het reguliere TXLSXWorkbook opslagpad (save path) gebruikt wordt. Omdat beide paden op één lijn zitten aangaande de seriële waarde, kan een bestand dat door de streaming writer is geproduceerd via de HotXLS reader worden teruggelezen en worden geopend in Excel met data die overeenstemmen met wat u voor ogen had, geheel zonder off-by-one of verrassende epoches-wisselingen tussen de schrijvende partij en degene die leest
De volgorde is dwingend, want de bytes zijn al weg
Aan streamen hangt één gouden regel verbonden om diens geheugenprofiel (memory profile) te kunnen inkopen. Uitvoer wordt uitgezonden naarmate u schrijft en kan niet opnieuw bezocht worden, alles dient derhalve geschreven te worden in de volgorde waarin dit in het bestand voorkomt. Binnenin een rij lopen cellen op in stijgende kolomvolgorde (ascending column order). Binnen een vel, lopen rijen op in een oplopende volgorde (ascending order). De writer heeft niet de beschikking over een buffer om achteraf uw cellen alsnog the sorteren, de rij die zojuist is afgesloten, werd al als bytes doorgestuurd in de zip stream, deze kan dus niet meer bereikt worden. Overhandig hem in één en dezelfde rij kolom 5 en vervolgens kolom 2, en de uitvoer is misvormd (malformed), de writer stoot tenslotte domweg uit wat hem wordt gepresenteerd en doet dat in de sequentie (sequence) die is aangeleverd
Voor de gebruikelijkere casus (common case) is de row API uitgerust met een klein snufje. AddRow hanteert een op 1 gebaseerde rij-index, met 0 doorspelen betekent echter 'neem na de vorige rij de daaropvolgende', bij opeenvolgend (sequential) vullen is het zodoende niet noodzakelijk om een meelopende teller mee te sturen. Iedere afzonderlijke AddRow-actie sluit tevens de voorliggende rij af, en bij de AddSheet actie wordt ook voorafgaand het blad dichtgeklapt, zodoende hoeft er nimmer via een expliciet statement (explicitly) een afzonderlijke rij- of bladbeëindiging aangebracht te worden. Terwijl het daaropvolgende deel start, formaliseert (finalises) de schrijver de openstaande constructie reeds voor u
Escapen vindt daar plaats waar tekst de XML binnengaat
Elke tekst die u wegschrijft wordt onderdeel van een XML document, zodat de vijf vastgestelde (predefined) XML-entiteiten moeten worden ge-escaped (escaped), anders deugt het pakket al niet zodra de waarde een ampersand (ampersand) of scherpe haak herbergt (angle bracket). De writer escapet &, <, >, ", en ' automatisch (escapes) aan zowel de zijde van de inline string als van het formule element (formula text), de twee locaties waarin via aanroeper verstrekte karaktersets hun weg vinden naar de binnenkant van een markup. Indien u een basis WideString passeert stelt de writer deze geheel veilig. Luidt een de productnaam iets van Smith & Co <Ltd> of betreft dit een formule waarbij wordt gekoppeld aan de aanhalingsteken benaming (quoted sheet name), dan vloeit daar welgevormde (well-formed) XML uit en heeft dit ten aanzien van het escape process bij u geen verdere consequentie
Levenscyclus, en de vraag waarom de Destroy functie nog steeds afsluit
Uiteindelijk staat het afronden (finishing) van het pakket garant voor datgene dat werkelijk het workbook-deel, the styles-sectie (styles part), the content-types samen met relatie-gedeelte vormgeeft, alsmede op de laatste plaats de centrale directory-locatie afrondt (zip central directory). Alle operaties hiertoe spelen zich af ten tijde van de uitvoering in het kader van het sluiten in de Close functie. Staat u met een niet afgerond (never closed) archiefje dan staat een spreadsheet-applicatie niet zomaar machtig om deze incomplete structuur succesvol aan de praat te krijgen (open). De stap 'afsluiting' behelst immers niet een willekeurige opruimfase; hier stapt men van corrupt af (invalid) richting valide model (valid). Vanwege het risico dat een op pad gebrachte foutieve foutenstructuur omwille van deze zelfde routine via een omweg nog tot lek leidt behelst de verwerking in the Destroy-modus tegelijkertijd één solide en gedegen slotoperatie aangaande de pakketverhoudingen teneinde dat ten aanzien het onderliggende zip component nimmer gelekt zal optreden, niet eens toen exception-codes een directe afroeping feitelijk onmogelijk deden uitwijzen. Ten dien aanzien ligt the oplossing veilig verankert conform een gebruikelijk Delphi protocol: wegschrijven geschied in een try omgeving, er volgt een aanroep naar de routine Close, ter afronding een vrijgave aangaande middelen ten tijde van een afsluitende fase alvorens finally
Een enorm werkblad end-to-end streamen
Aanvangen, het tussenvoegen omtrent het blad-niveau, vervolgens reeksen via rij operaties storten tot ten tijde dat deze in sluitstand verkeren, dat luidt the stappenprocedure. Bijgevoegd illustratief gedeelte vangt ten uitvoer per titel en gaat nadien voor een grotere uitgeschreven hoeveelheid qua reeksen-getypeerde datarecords over, om vervolgens de strijd qua gevarieerd samenstel-aangaande teken reeksen en het samenbrengen onder geformaliseerd numeriek bestel (formula) plus de uiteindelijke tijdsbestek aspecten te vangen. Binnen tien dan wel binnen de veelzijdigheid qua die deeltaken tot miljoenen stroomafwaarts via geheugen capaciteiten; al deze data verdwijnen gelijktijdig per schrijf operatie de zipstroom in
uses
lxDirectWrite;
procedure StreamReport(const Path: string; RowCount: Integer);
var
W: TXLSDirectWriter;
I: Integer;
begin
W := TXLSDirectWriter.Create;
try
W.BeginFile(Path);
W.AddSheet('Sales');
// Header row, written in ascending column order
W.AddRow(1);
W.WriteString(1, 'Item');
W.WriteString(2, 'Qty');
W.WriteString(3, 'Price');
W.WriteString(4, 'Total');
W.WriteString(5, 'Date');
// Data rows; pass 0 to AddRow to take the next row automatically
for I := 1 to RowCount do
begin
W.AddRow(0);
W.WriteString(1, 'Item ' + IntToStr(I));
W.WriteNumber(2, I);
W.WriteNumber(3, 1.5 + (I mod 10));
W.WriteFormula(4, Format('B%d*C%d', [I + 1, I + 1]));
W.WriteDateTime(5, EncodeDate(2026, 1, 1) + I);
end;
W.Close; // finalises the package
finally
W.Free;
end;
end;
U vervolgt de voortzetting op de manier omtrent alweer dat toegevoegd werk op de wijze omtrent een nieuwe opstap richting het volgende AddSheet teneinde dat sluiten van eerder getreden acties soepel in tandem zal doorgang omwille aan vang fase. Met toekenningen aangaande Boolse waarden kan via vlag indicatoren WriteBoolean per type geactiveerd ten opzichte van tekstueel "True". Binnen een operatie waarin verificatie een vergaande opbouw staving rechtvaardigt ten dienste qua 'round-trips', voorziet het eigenschapselement de functionaliteit omtrent de hoeveelheid weggeschreven aantallen cellen-bestand (CellCount), daar the uitgelezen resultaten eveneens corresponderen om de som als zodanig gelijkluidend middels een the reader tool in weergavevorm terug te sturen te bewijzen
// A second sheet of typed flags after the data sheet above
W.AddSheet('Flags');
W.AddRow(1);
W.WriteString(1, 'Name');
W.WriteString(2, 'Active');
W.AddRow(0);
W.WriteString(1, 'alpha');
W.WriteBoolean(2, True);
WriteLn(Format('wrote %d cells', [W.CellCount]));
Het wegschrijven richting het stromings (stream) mechanisme al om en om dat ten bate via file handelen, dit draait uit een vergelijkbaar code principe met name om BeginStream, opererend ter zake bij vervanging aan BeginFile acties ter weergave of overzet procedurele server processen in afstemming qua HTTP request, op het zelfde qua memory handelen ten koste of ten minste omtrent eventuele temp of een vaste opslag variant (temporary file). Bij het sturen ligt de controle via die levensverwachting (lifetime) qua bestanden onmiskenbaar nimmer en ten tijde omtrent wegschrijven op dat deel maar eenduidig aan de kant-inzake the verwerker en roeper van inroep zijde
Op de uiteindelijke situatie of verwerken qua on demand aspecten aangaande servers kan middels inpas the inzet patronen of een en ander rondom de theorie streaming-writes for server-omgeving en de batch verwerkingen voor werk gerelateerd the handelaar plus tijd export doeleinden the inzage geschieden. Waarbij aangaande bredere blikken op zwaar of gigantisch, of in algemene en opgezette vormen of leestechniek of terzake schrijven aangaande, dan is op pagina in Delphi, the weging bij die factoren om die tijden memory parameters te vangen onderverdeeld conform the inzichten over: werkboek performantie voor gigantische formaten Delphi. Deze writer die streamt behelst de uitlever standaard van wat aangeduid is per het HotXLS Component omtrent de in omloop varianten C++Builder samen per die qua inleestechnieken (read), en aangaande the save functies al op blog site