Si at en nattlig Delphi-tjeneste genererer én XLSX per kunde, noen hundre filer, noen av dem 400 000 rader brede. Profiler den, og overraskelsen er sjelden celle-fyllingsløkken. Det er SaveAs-kallet. Med standardskriveren blir hvert ark serialisert til én enkelt XML-streng i minnet før den strengen komprimeres inn i OOXML-zipen, og for et bredt ark kan den forbigående strengen langt overgå cellemodellen den ble bygget fra. Så en jobb som komfortabelt bygger dataene sine og ligger på 800 MB, vil spike forbi en beholdergrense på 2 GB under lagringen, og OOM-drapsmannen fyller ut feilrapporten klokken tre om natten når ingen ser på. HotXLS, losLabs native regnearkbibliotek for Delphi og C++Builder, har en egenskap rettet nøyaktig mot den spiken: StreamingWrite. Rundt den sitter to ytterligere spaker som avgjør om en batcharbeidsprosess holder seg innenfor sitt minne- og tidsbudsjett, nemlig radnivå-skrivetilbakekall og måten stilpoolen oppfører seg på inne i en tett løkke
Hva standard lagringssti bufrer, og hva StreamingWrite endrer
Standard XLSX-skriveren favoriserer enkelhet. Den gjengir arkets XML fullstendig, og gir så den ferdige strengen til zip-komprimatoren. Det er riktig avveining for det overveldende flertallet av arbeidsbøker, der hele arkets XML får plass på noen få megabyte. Det slutter å være riktig når ett arks serialiserte form løper opp i hundrevis av megabyte. Regneark-XML er ordrikt: hver numeriske celle koster titalls tegn med markup, og strengen som holder alt dette, må være sammenhengende. På en minnegraf er signaturen vanskelig å overse. Et langt flatt platå mens radene fylles, deretter en skarp trekantet spike under SaveAs, deretter kollapsen når zipen skylles ut
Å sette Book.StreamingWrite := True bytter SaveAs til en arkskriver som sender arkets XML direkte inn i zip-strømmen etter hvert som den genereres. Den mellomliggende strengen blir aldri allokert, og den trekantede spiken flater ut i støyen
Vær presis om hva det faktisk gir deg, for å overselge det fører til feil kapasitetsplaner. Flagget endrer bare lagringsstien. Å bygge arbeidsboken allokerer fortsatt hele cellemodellen i minnet, så platået under fyllingsfasen er akkurat like høyt som før. Det som forsvinner, er serialiseringsspiken som pleide å stables oppå det platået ved lagringstidspunktet, og for en jobb som fyller 400k rader, er den spiken som regel hele forskjellen mellom å holde seg innenfor et minnebudsjett og å sprenge det. Egenskapen har som standard False for å bevare den historiske atferden, så å velge den inn er én eksplisitt linje du skriver med hensikt
En masseeksport med flagget på
Book := TXLSXWorkbook.Create;
try
BoldIdx := Book.Fonts.Add('Calibri', 11, True, False); // poolindeks, 0-basert
Sheet := Book.Sheets.Add('Bulk');
for R := 1 to 100000 do
begin
Sheet.Cells[R, 1].Value := R;
Sheet.Cells[R, 2].Value := 'Row ' + IntToStr(R);
Sheet.Cells[R, 3].Value := R * 1.5;
if (R mod 1000) = 0 then
Sheet.Cells[R, 2].FontIndex := BoldIdx + 1; // 1-basert på cellen
end;
Book.StreamingWrite := True; // strøm arkets XML rett inn i zipen
Book.SaveAs('bulk.xlsx');
finally
Book.Free;
end;
Cells[R, C] oppretter celler på forespørsel, noe som holder løkkekroppen ren. To rutenettgrenser er verdt å lære utenat: 1 048 576 rader og 16 384 kolonner, eksponert som XlsxMaxRow og XlsxMaxCol. En datastrøm som overskrider radgrensen, må deles opp over flere ark i din egen kode. Ingenting nedstrøms merker overskridelsen eller fikser den for deg, og filen ender rett og slett avkuttet ved grensen
Å fylle rader uten Variant-overhead per celle
Hver Cells[R, C].Value-tilordning betaler for et celleoppslag og en Variant-konvertering. Ved ti tusen rader merker ingen det. Ved en million rader med tjue kolonner hver, blir denne overheaden per kall den dominerende kostnaden i fyllingsfasen, og profileringsverktøyet vil peke rett på den. Batchgrensesnittene lar deg i stedet gi skriveren en hel rad om gangen. WriteRows driver et tilbakekall som leverer én rad per kall:
procedure TBulkExporter.FillRow(Sender: TObject; SheetIndex, Row, FirstCol,
LastCol: Integer; var Values: Variant; var Skip: Boolean;
var Cancel: Boolean);
begin
if not FReader.Next then
begin
Cancel := True; // datakilden er tom: stopp rent
Exit;
end;
Values := VarArrayCreate([FirstCol, LastCol], varVariant);
Values[FirstCol] := FReader.RecordId;
Values[FirstCol + 1] := FReader.CustomerName;
Values[FirstCol + 2] := FReader.Amount;
end;
// fyll rad 2..100001, kolonne A..C, hentet fra leseren
Sheet.WriteRows(2, 1, 100001, 3, FillRow);
Cancel-flagget er det som gjør et fast radintervall om til "opptil N rader," som er den naturlige formen når radantallet kommer fra en spørring du ikke er ferdig med å kjøre. Skip er det lettere grepet: det lar en enkelt rad stå tom uten å stoppe kjøringen. Utover å fylle celler viser tilbakekallet seg å være et godt sted for de driftsmessige hensynene som ellers blir klint på en fyllingsløkke på klønete vis. En fremdriftsteller som tikker for hver tusende rad, et avbruddstoken som polles fra jobbplanleggeren, en hastighetsbegrenser på lesing fra kildedatabasen: alt sammen bor ett sted i stedet for å bli tredd gjennom cellskrivende kode. På lesesiden speiler ForEachRow og ForEachCell det samme mønsteret, noe som betyr noe når en batchjobb både konsumerer og produserer store filer
Stilpooler belønner heising
XLSX-stilmodellen er et sett med delte pooler. Fonts.Add, Fills.AddSolid og Borders.Add returnerer alle en 0-basert poolindeks, og en celle refererer til en skrift ved å lagre den indeksen pluss én i FontIndex, hvor null er reservert for arbeidsbokens standard. +1-en er rett der i masseeksempelet over. Glem den, og cellen plukker stille opp feil stil, fordi en av-med-én-feil i en stilpoolindeks fortsatt er en gyldig indeks, og ingenting kaster en feil
Disiplinen som følger, er å opprette hvert stilobjekt før radløkken og referere til indeksen dens inne i løkken. Fonts.Add deduplikerer identiske definisjoner, så å kalle den én gang per rad bare sløser med CPU. Alignments.Add er fellen, fordi den returnerer en fersk oppføring for hvert kall. Inne i en løkke på 100k rader begraver det styles.xml under hundre tusen duplikate justeringsposter, noe som blåser opp filen på disk og bremser hver senere åpning i Excel etter hvert som duplikatene tolkes på nytt. Bygg hver stil én gang utenfor løkken, og referer så til indeksen dens så mange ganger du trenger
Strømmer, midlertidige mapper og batchløkken rundt alt sammen
Ingenting av dette krever et filsystem. Begge fasadene bærer TStream-overlastinger over hele IO-overflaten sin, deriblant Open og SaveAs og SaveAsCSV og SaveAsHTML og SaveAsODS, slik at en batcharbeidsprosess kan gjengi rett inn i en TMemoryStream bestemt for blob-lagring eller et HTTP-svar uten noensinne å røre disk. Det er én skarp kant å huske. SaveAs(Stream) skriver fra strømmens gjeldende posisjon og spoler ikke tilbake etterpå, så sett Position := 0 selv før strømmen overleveres til det som leverer den videre, ellers leser mottakeren null byte. XLS-fasaden legger til to egne knapper. SetTempDir peker BIFF-skriverens midlertidige filer mot et volum som har plassen og IO-slingringsrommet til å absorbere dem, noe som betyr noe på servere hvor standard temp-sti ligger på en trang systemdisk. UseSharedFormulas folder gjentatte formelkropper inn i delte grupper, en reell størrelsesreduksjon for den klassiske rapportformen hvor én formel er kopiert nedover en hel kolonne
Selve batchløkken forblir kjedelig med hensikt:
for FileName in SourceFiles do
begin
Book := TXLSXWorkbook.Create; // fersk instans: ingen tilstandslekkasje
try
Book.StreamingWrite := True;
if Book.Open(FileName) <> 1 then
Continue; // én dårlig inndata skal ikke drepe hele batchen
Book.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
finally
Book.Free;
end;
end;
En fersk arbeidsbokinstans per fil koster mikrosekunder og fjerner en hel kategori av feil med kryssfil-forurensning: stiler, definerte navn og dokumentegenskaper fra fil 17 har ingen vei inn i fil 18. Hopp-over-og-fortsett ved en mislykket Open fortjener plassen sin like mye, fordi én avkuttet opplasting i en batch på 600 filer bør koste deg én loggilinje fremfor resten av kjøringen. Verdt å påpeke er også hva CSV-grenen bevisst ikke gjør. SaveAsCSV skriver ut formler som ren tekst og evaluerer dem aldri, så en konverteringsbatch hvis konsumenter forventer beregnede tall, må kjøre Calculate på de aktuelle cellene først, eller starte fra arbeidsbøker som allerede bærer bufrede resultater fra en tidligere beregning
Samtidighetsmodell: én arbeidsbok per tråd
Ingen av fasadenes objekter er trådsikre, og designet har aldri latt som noe annet. Fordi det ikke finnes noen delt global tilstand mellom instanser, er skaleringsregelen ganske enkelt én arbeidsbok per arbeidertråd, uten deling av en arbeidsbok på tvers av tråder. En pool med N arbeidsprosesser, hver med sin egen TXLSXWorkbook, skalerer nesten lineært helt til minnet blir taket, og det taket er noe du kan sette et tall på: den største samtidige cellemodellen multiplisert med antall arbeidsprosesser, pluss det lagringstidsoverheadet StreamingWrite har flatet ut. Når køen blir dyp, bruk mottrykk ved jobbkøen i stedet for inne i skriveren. En sultefôret tråd som har halvskrevet en arbeidsbok, har produsert ingenting nyttig, mens en jobb som ventet noen sekunder på en ledig arbeidsprosess, fullføres intakt
For det bredere bildet av tuning, inkludert delte formler, hopping over grafikk på lesesiden, og de XLS-spesifikke spakene, se ytelsesveiledningen for store arbeidsbøker. Batchjobber hvis rader kommer rett fra en spørring, er dekket separat i eksportmønstrene for database til Delphi-rapporter
HotXLS kompileres inn i Delphi- eller C++Builder-tjenesten din som nativ Object Pascal uten eksterne avhengigheter; utgaver og lisensiering finner du på HotXLS Delphi Component-produktsiden