En rapporteringsjobb kjører fint i et år. Den bygger en arbeidsbok, fyller et ark med det spørringen returnerer, og lagrer det. Så ber en kunde med fem års historikk om en full eksport, antall rader passerer en million, og prosessen dør med en tom for minne-feil lenge før filen når disken. Det var ikke noe feil med koden. Den holdt hele arbeidsboken i RAM slik at den kunne serialisere den på slutten, og minnet den trengte vokste i takt med antall rader den ble bedt om å skrive
Løsningen er ikke en større maskin. Det er en annen skrivemodell. Den strømmende direkteskriveren i HotXLS sender ut OOXML-pakken trinnvis etter hvert som radene ankommer, så minnet den bruker er ikke avhengig av hvor mange rader du skriver. Det er skrivesidens motstykke til den strømmende leseren: der leseren går gjennom et enormt ark uten å bygge et celletre, produserer skriveren ett uten å bygge et celletre heller
Hvorfor den vanlige lagringsveien vokser med dataene
Den vanlige TXLSXWorkbook-veien bygger først en full objektmodell. Hver celle, med sin verdi, type og stilreferanse, lever som et objekt i minnet til du kaller lagre, og da blir hele treet serialisert inn i pakken. Den modellen er den rette når du vil lese et ark, redigere det, beregne på nytt og skrive det tilbake, fordi tilfeldig tilgang til hvilken som helst celle er nøyaktig hva redigering krever. Det er feil modell når du tømmer rader i én retning og aldri ser deg tilbake, fordi du betaler for å holde hver rad residerende uten noen fordel. En million rader med objekter er en million rader med objekter, uansett om du noen gang besøker dem igjen eller ikke
Den strømmende skriveren fjerner treet. Så snart en celle er skrevet, blir den byter i regnearkdelen, og disse bytene overleveres til zip-utdataene. Regnearkstrømmen er den eneste bufferen som vokser, og den vokser på utdatasiden, ikke som levende Delphi-objekter på heapen. Det som forblir residerende er en fast mengde bokføring: arknavnene, noen få flagg, gjeldende radnummer, en celleteller. Det settet endres ikke mellom rad én og rad ti millioner
Den delte strengtabellen er fellen, og innebygde strenger er veien ut
De fleste strømmende XLSX-skrivere klarer seg bra inntil de møter tekst. OOXML-formatet lagrer normalt strenger i en delt strengtabell: hver distinkte streng skrives én gang til en separat del, og hver celle som holder den strengen bærer en indeks til tabellen i stedet for teksten. Det er en god plassoptimalisering for filer fulle av repeterte etiketter, og det er standarden den vanlige lagringsveien bruker. Problemet for en strømmende skriver er brutalt. For å deduplikere må tabellen forbli residerende under hele jobben, fordi enhver rad som fremdeles skal komme kan gjenta en streng fra en rad som allerede er skrevet, og bare et komplett kart i minnet over sette strenger kan tilordne riktig indeks. Så den ene strukturen en strømmende skriver ikke kan strømme, er nettopp strukturen som er ment å gjøre filen liten. Teksttunge data beseirer strømmingen du kom for
Direkteskriveren omgår tabellen fullstendig. Strenger skrives innebygd, som t="inlineStr"-celler hvis tekst sitter direkte inni cellen med et <is><t>-element. Det er ingen tabell å akkumulere og ingen kart over sette strenger å holde, så tekstkolonner koster ikke mer minne enn numeriske. Kompromisset er eksplisitt og verdt å slå fast tydelig. Innebygde strenger gjentar den samme teksten uansett hvor den forekommer, så en fil med mange identiske etiketter er større på disken enn varianten med delt streng. Du bruker filstørrelse for å kjøpe konstant minne. For en en-pass-eksport er det den riktige siden av kompromisset, og zip-komprimering absorberer mye av repetisjonen på veien ut uansett
Stiltabellen kommer til slutt, med ett datoformat
Stiler byr på den samme spenningen som strenger. En arbeidsbok refererer til sin formatering gjennom en stildel, og en strømmende skriver kan ikke holde en voksende palett med stiler i takt med celler den allerede har skylt ut. Direkteskriveren svarer på dette ved å holde stiltabellen liten og fast, og sender den ut ved lukking i stedet for på forhånd. Ett standard celleformat dekker vanlige celler. Ett format for datotall dekker datoer, registrert med en formatkode på yyyy-mm-dd på en kjent posisjon i celleformatlisten
Det datoformatet er grunnen til at WriteDateTime eksisterer som sitt eget kall. Excel har ingen innebygd datotype; en dato er et tall som bærer et datoformat. WriteDateTime skriver verdien som et vanlig serielt tall og tagger cellen med den ene datostilen, slik at regnearket gjengir det som en dato i stedet for et femsifret heltall. Seriekoden den skriver betyr noe for å kunne lese filen tilbake (round-tripping). Den lagrer TDateTime-verdien direkte under 1900-datosystemet, som er den samme konvensjonen den vanlige lagringsveien for TXLSXWorkbook bruker. Fordi begge baner er enige om serienummeret, leses en fil som den strømmende skriveren produserer, tilbake gjennom HotXLS-leseren og åpnes i Excel med datoer som samsvarer med det du hadde til hensikt, uten noen "off-by-one" eller epokeoverraskelser mellom skriver og leser
Rekkefølge er obligatorisk, fordi bytene allerede er borte
Strømming kjøper sin minneprofil med én regel du må respektere. Utdata sendes ut etter hvert som du jobber og kan ikke besøkes på nytt, så alt må skrives i den rekkefølgen det vises i filen. Inne i en rad går celler i stigende kolonnerekkefølge. Inne i et ark går rader i stigende rekkefølge. Det finnes ingen buffer som lar skriveren sortere cellene dine i etterkant, fordi raden du nettopp lukket allerede er byter i zip-strømmen og ikke lenger er tilgjengelig. Gi den kolonne 5 og deretter kolonne 2 i samme rad, og resultatet blir feilformatert, siden skriveren ganske enkelt sender ut det du gir den i den sekvensen du gir den
Rad-API-et har en liten bekvemmelighet for det vanlige tilfellet. AddRow tar en 1-basert radindeks, men å passere 0 betyr at den tar neste rad etter den forrige, slik at en sekvensiell fylling ikke trenger å spore og overføre en inkrementell teller. Hver AddRow lukker raden før den, og hver AddSheet lukker arket før det, slik at du aldri eksplisitt avslutter en rad eller et ark. Du starter den neste, og skriveren ferdigstiller den åpne strukturen for deg
Rømming (escaping) håndteres der tekst kommer inn i XML-en
Enhver tekst du skriver blir en del av et XML-dokument, så de fem forhåndsdefinerte XML-entitetene må escapes, ellers blir pakken ugyldig i det øyeblikket en verdi inneholder et og-tegn eller en vinkelparentes. Skriveren escaper &, <, >, " og ' for deg på både innebygd strengtekst og formeltekst, de to stedene der innkalleroppgitte tegn lander på innsiden av oppmerkingen. Du sender inn en rå WideString, og skriveren gjør den trygg. Et produktnavn som Smith & Co <Ltd> eller en formel som refererer til et regnearknavn i anførselstegn, kommer ut som velformet XML uten noen form for escaping fra din side
Livssyklus, og hvorfor Destroy fremdeles lukker
Fullføring av pakken er det som skriver arbeidsbokdelen, stildelen, innholdstyper og relasjonsdeler, og til slutt den sentrale zip-katalogen. Det arbeidet skjer i Close. En pakke som aldri lukkes, er en ufullstendig zip som intet regnearkprogram vil åpne, så lukking er ikke en valgfri opprydding, det er trinnet som gjør filen gyldig. For å gardere seg mot en gjenglemt Close i en feilbane, utfører Destroy en lukking etter beste evne hvis pakken fremdeles er åpen, slik at frigjøring av skriveren ikke lekker det underliggende zip-objektet selv om et unntak hoppet over det eksplisitte kallet. Det pålitelige mønsteret er fremdeles det vanlige Delphi-mønsteret: skriv inne i en try, kall Close og frigjør i finally
Strømming av et stort ark fra ende til ende
Jobbens form er å begynne, legge til et ark, helle inn rader, lukke. Eksempelet nedenfor skriver en overskriftsrad og deretter en lang rekke av typede datarader, hvor strenger, tall, en formel uten bufret resultat, og en dato blandes. Minnet det bruker på ti rader og på ti millioner rader er det samme, fordi hver celle avgår til zip-strømmen så snart den er skrevet
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;
Et andre ark er ganske enkelt en ny AddSheet før du fortsetter, og skriveren lukker det første arket idet den åpner det andre. Boolske flagg bruker WriteBoolean, som skriver en typet boolsk celle fremfor teksten "True". Hvis du vil bekrefte at filen er feilfri og fungerer for å lese frem og tilbake (round-trips), rapporterer egenskapen CellCount hvor mange celler som ble skrevet, og å lese resultatet tilbake med den strømmende leseren bør rapportere den samme totalen
// 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]));
Skriving til en strøm i stedet for en fil er den samme koden med BeginStream i stedet for BeginFile, som lar en server sende arbeidsboken til et HTTP-svar eller en minnestrøm uten en midlertidig fil på disken. Skriveren eier ikke strømmen du sender inn, så du beholder kontrollen over dens levetid
Når arbeidet er et server-endepunkt som bygger arbeidsbøker på forespørsel, viser mønstrene i strømmende skriv for server og batchjobber hvordan du kobler dette inn i en forespørselshåndterer og en planlagt eksport. Når spørsmålet er den bredere kostnaden av svært store arbeidsbøker, både for lesing og skriving, dekker ytelse for store arbeidsbøker i Delphi hvor tiden og minnet faktisk forsvinner. Den strømmende direkteskriveren leveres som en del av HotXLS-komponenten for Delphi og C++Builder, sammen med de fulle API-ene for lesing, redigering og lagring som dekkes andre steder på denne bloggen