Teknisk artikel

HotXLS strömmande skrivning för batchjobb i Delphi

Säg att en nattlig Delphi-tjänst genererar en XLSX per kund, några hundra filer, en del av dem 400 000 rader breda. Profilera den så är överraskningen sällan cellfyllningsloopen. Det är anropet SaveAs. Med standardskrivaren serialiseras varje kalkylblad till en enda XML-sträng i minnet innan den strängen komprimeras in i OOXML-zipen, och för ett brett blad kan den tillfälliga strängen överskugga cellmodellen den byggdes av. Så ett jobb som bekvämt bygger sin data och ligger på 800 MB skjuter förbi en containergräns på 2 GB under sparandet, och OOM-dödaren skickar in buggrapporten klockan 03 när ingen tittar. HotXLS, losLabs inbyggda kalkylbladsbibliotek för Delphi och C++Builder, har en egenskap riktad rakt mot den toppen: StreamingWrite. Runt den sitter två ytterligare spakar som avgör om en batcharbetare håller sig innanför sin minnes- och tidsbudget, nämligen skrivåteranrop på radnivå och hur stilpoolen beter sig inuti en tät loop

Vad standardvägen för sparande buffrar, och vad StreamingWrite ändrar

Standardskrivaren för XLSX föredrar enkelhet. Den renderar kalkylbladets XML fullständigt, och lämnar sedan den färdiga strängen till zip-komprimeraren. Det är rätt avvägning för den överväldigande majoriteten av arbetsböcker, där hela bladets XML får plats i några megabyte. Det slutar vara rätt när ett blads serialiserade form löper till hundratals megabyte. Kalkylblads-XML är mångordig: varje numerisk cell kostar tiotals tecken markup, och strängen som håller alltihop måste vara sammanhängande. På ett minnesdiagram är signaturen svår att missa. En lång platt platå medan raderna fylls, sedan en skarp triangulär topp under SaveAs, sedan kollapsen när zipen väl spolats ut

Att sätta Book.StreamingWrite := True växlar SaveAs till en kalkylbladsskrivare som avger blad-XML direkt in i zip-strömmen allteftersom den genereras. Den mellanliggande strängen allokeras aldrig, och den triangulära toppen plattas ut i bruset

Var precis med vad det faktiskt köper dig, eftersom att översälja det leder till felaktiga kapacitetsplaner. Flaggan ändrar bara vägen för sparande. Att bygga arbetsboken allokerar fortfarande hela cellmodellen i minnet, så platån under fyllnadsfasen är exakt lika hög som förut. Det som försvinner är den serialiseringstopp som förut staplades ovanpå den platån vid sparandet, och för ett jobb som fyller 400 000 rader är den toppen rutinmässigt hela skillnaden mellan att rymmas i en minnesbudget och att spränga den. Egenskapen har False som standard för att bevara det historiska beteendet, så att välja den är en uttrycklig rad du skriver med avsikt

Minne över tid i ett Delphi-batchjobb med HotXLS: standardanropet SaveAs staplar en tillfällig topp av kalkylblads-XML ovanpå fyllnadsplatån, medan Book.StreamingWrite := True håller profilen platt genom sparandet
Fyllnadsplatån är identisk hur som helst eftersom cellmodellen fortfarande byggs i minnet; StreamingWrite tar bara bort serialiseringstoppen vid sparandet

En massexport med flaggan på

Book := TXLSXWorkbook.Create;
try
  BoldIdx := Book.Fonts.Add('Calibri', 11, True, False); // poolindex, 0-baserat
  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-baserat på cellen
  end;
  Book.StreamingWrite := True;   // strömma blad-XML rakt in i zipen
  Book.SaveAs('bulk.xlsx');
finally
  Book.Free;
end;

Cells[R, C] skapar celler på begäran, vilket håller loopkroppen ren. Två rutnätstak är värda att lägga på minnet: 1 048 576 rader och 16 384 kolumner, exponerade som XlsxMaxRow och XlsxMaxCol. Ett dataflöde som spränger radtaket måste delas över blad i din egen kod. Ingenting nedströms märker överskridandet eller rättar det åt dig, och filen slutar helt enkelt avhuggen vid gränsen

Att fylla rader utan Variant-omkostnad per cell

Varje tilldelning till Cells[R, C].Value betalar för en celluppslagning och en Variant-konvertering. Vid tiotusen rader märker ingen det. Vid en miljon rader med tjugo kolumner var blir den omkostnaden per anrop den dominerande kostnaden i fyllnadsfasen, och profileraren pekar rakt på den. Batchgränssnitten låter dig lämna över en hel rad åt gången istället. WriteRows driver ett återanrop som levererar en rad per anrop:

Flödet för HotXLS WriteRows-återanrop i Delphi: en frågemarkör lämnar en rad per anrop till FillRow-återanropet, som fyller en variant-array med värden eller höjer Skip och Cancel, och kalkylbladet fylls rad för rad
WriteRows lämnar loopen till HotXLS medan återanropet levererar en variant-array-rad per anrop, med Skip som avhopp per rad och Cancel som ren stopp för hela körningen
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;              // datakällan tömd: stoppa rent
    Exit;
  end;
  Values := VarArrayCreate([FirstCol, LastCol], varVariant);
  Values[FirstCol]     := FReader.RecordId;
  Values[FirstCol + 1] := FReader.CustomerName;
  Values[FirstCol + 2] := FReader.Amount;
end;

// fyll raderna 2..100001, kolumnerna A..C, med data från läsaren
Sheet.WriteRows(2, 1, 100001, 3, FillRow);

Cancel-flaggan är det som gör ett fast radintervall till ”upp till N rader”, vilket är den naturliga formen när radantalet kommer från en fråga du inte kört färdigt. Skip är den lättare handen: den lämnar en enskild rad tom utan att stoppa körningen. Utöver att fylla celler visar sig återanropet vara ett bra hem för de operativa hänsyn som annars skruvas fast på en fyllnadsloop på klumpiga sätt. En förloppsräknare som tickar var tusende rad, en avbrottssignal som pollas från jobbschemaläggaren, en hastighetsbegränsare på läsningar från källdatabasen: allt bor på ett ställe istället för att trädas genom cellskrivande kod. På läsidan speglar ForEachRow och ForEachCell samma mönster, vilket spelar roll när ett batchjobb både konsumerar och producerar stora filer

Stilpooler belönar hissning

XLSX-stilmodellen är en uppsättning delade pooler. Fonts.Add, Fills.AddSolid och Borders.Add returnerar alla ett 0-baserat poolindex, och en cell refererar till ett typsnitt genom att lagra det indexet plus ett i FontIndex, där noll är reserverat för arbetsbokens standardvärde. Det plus ett står där i massexemplet ovan. Glöm det och cellen plockar tyst upp fel stil, eftersom ett plus-ett-fel i ett stilpoolindex fortfarande är ett giltigt index och inget utlöses

Disciplinen som följer är att skapa varje stilobjekt före radloopen och referera till dess index inuti loopen. Fonts.Add avduplicerar identiska definitioner, så att anropa den en gång per rad slösar bara CPU. Alignments.Add är fällan, eftersom den returnerar en färsk post vid varje anrop. Inuti en loop på 100 000 rader begraver det styles.xml under hundratusen dubblerade justeringsposter, vilket sväller filen på disk och saktar ner varje senare öppning i Excel medan dubbletterna tolkas om. Bygg varje stil en gång utanför loopen, och referera sedan till dess index så många gånger du behöver

Strömmar, temp-kataloger och batchloopen runt alltihop

Inget av det här kräver ett filsystem. Båda fasaderna bär TStream-överlagringar över hela sin IO-yta, Open och SaveAs och SaveAsCSV och SaveAsHTML och SaveAsODS bland dem, så en batcharbetare kan rendera rakt in i en TMemoryStream på väg till blob-lagring eller ett HTTP-svar utan att någonsin röra disken. Det finns en vass kant att minnas. SaveAs(Stream) skriver från strömmens aktuella position och spolar inte tillbaka efteråt, så sätt Position := 0 själv innan du lämnar strömmen till vad som än levererar den, annars läser konsumenten noll byte. XLS-fasaden lägger till två egna rattar. SetTempDir riktar BIFF-skrivarens temporära filer mot en volym som har utrymmet och IO-marginalen att svälja dem, vilket spelar roll på servrar där standardsökvägen för temp ligger på en trång systemdisk. UseSharedFormulas viker ihop upprepade formelkroppar i delade grupper, en verklig storleksminskning för den klassiska rapportformen där en formel kopierats ned genom en hel kolumn

Själva batchloopen förblir tråkig med flit:

for FileName in SourceFiles do
begin
  Book := TXLSXWorkbook.Create;        // färsk instans: inget tillståndsläckage
  try
    Book.StreamingWrite := True;
    if Book.Open(FileName) <> 1 then
      Continue;                        // en dålig indata får inte döda batchen
    Book.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
  finally
    Book.Free;
  end;
end;

En färsk arbetsboksinstans per fil kostar mikrosekunder och tar bort en hel kategori av buggar med korskontaminering mellan filer: stilar, definierade namn och dokumentegenskaper från fil 17 har ingen väg att läcka in i fil 18. Att hoppa över och fortsätta vid ett misslyckat Open förtjänar sin plats lika mycket, eftersom en avhuggen uppladdning i en batch på 600 filer bör kosta dig en enda loggrad snarare än resten av körningen. Värt att flagga är också vad CSV-benet medvetet inte gör. SaveAsCSV skriver ut formler som literal text och utvärderar dem aldrig, så en konverteringsbatch vars konsumenter väntar sig beräknade tal måste köra Calculate på de berörda cellerna först, eller utgå från arbetsböcker som redan bär cachelagrade resultat från en tidigare beräkning

Samtidighetsmodell: en arbetsbok per tråd

Ingen av fasadernas objekt är trådsäkra, och designen har aldrig påstått annat. Eftersom det inte finns något delat globalt tillstånd mellan instanser är skalningsregeln helt enkelt en arbetsbok per arbetartråd, utan delning av en arbetsbok över trådar. En pool av N arbetare, var och en med sin egen TXLSXWorkbook, skalar nära linjärt tills minnet blir taket, och det taket är något du kan sätta en siffra på: den största samtidiga cellmodellen multiplicerad med antalet arbetare, plus vilken omkostnad vid sparande som StreamingWrite nu plattat ut. När kön går djup, lägg mottrycket vid jobbkön istället för inuti skrivaren. En utsvulten tråd som halvskrivit en arbetsbok har inte producerat något användbart, medan ett jobb som väntat några sekunder på en ledig arbetare blir färdigt helt

HotXLS samtidighetsmodell för batchjobb på Delphi-servrar: en jobbkö matar arbetartrådar som var och en äger en privat TXLSXWorkbook-instans, med mottryck lagt vid kön och minnet som skalningstak
Arbetsboksinstanser delar inget globalt tillstånd, så en arbetsbok per tråd skalar tills de samtidiga cellmodellerna når minnestaket

För den bredare trimningsbilden, inklusive delade formler, överhoppning av grafik på läsidan och de XLS-specifika spakarna, se guiden till prestanda för stora arbetsböcker. Batchjobb vars rader kommer rakt ur en fråga täcks separat i exportmönster från databas för Delphi-rapporter

HotXLS kompileras in i din Delphi- eller C++Builder-tjänst som inbyggd Object Pascal utan externa beroenden; utgåvor och licensiering finns på produktsidan för HotXLS Delphi Component