Műszaki cikk

HotXLS Stream írások Delphi szerver oldali kötegelt feladatokhoz

Tegyük fel, hogy egy éjszakánként futó Delphi szolgáltatás ügyfelenként egy XLSX fájlt generál, néhány száz fájlt, közülük némelyik 400 000 sor széles. Ha profilozzuk, a meglepetés ritkán a cellakitöltő ciklusban rejlik, hanem a SaveAs hívásban. Az alapértelmezett íróval minden munkalap egyetlen memóriában tartott XML-karakterlánccá szerializálódik, mielőtt ez a karakterlánc tömörítve bekerülne az OOXML zip fájlba, és egy széles munkalap esetén ez az átmeneti karakterlánc messze meghaladhatja azt a cellamodellt, amelyből épült. Így egy feladat, amely kényelmesen felépíti az adatait, és 800 MB-on áll meg, a mentés közben túllépheti a 2 GB-os konténerkorlátot, és az OOM killer hajnali 03:00-kor jelenti a hibát, amikor senki sem figyel. A HotXLS, a losLab natív táblázatkezelő könyvtára Delphihez és C++Builderhez, pontosan erre a csúcsra célzott tulajdonsággal rendelkezik: StreamingWrite. Emellett még két kar dönti el, hogy egy kötegelt feldolgozó munkafolyamat a memória- és időkeretén belül marad-e, nevezetesen a sorszintű írási visszahívások és az, ahogyan a stílustár egy szoros ciklusban viselkedik

Mit pufferel az alapértelmezett mentési útvonal, és mit változtat ezen a StreamingWrite

Az alapértelmezett XLSX-író az egyszerűséget részesíti előnyben. Teljesen legenerálja a munkalap XML-jét, majd a kész karakterláncot átadja a zip tömörítőnek. Ez a helyes kompromisszum a munkafüzetek túlnyomó többségénél, ahol a teljes munkalap XML-je néhány megabájtba belefér. Akkor szűnik meg helyesnek lenni, amikor egy munkalap szerializált formája több száz megabájtra rúg. A táblázat-XML terjengős: minden numerikus cella több tucat karakternyi jelölést emészt fel, és az ezt tartalmazó karakterláncnak összefüggőnek kell lennie. Egy memóriagrafikonon a jel nehezen téveszthető el. Hosszú, lapos plató, amíg a sorok feltöltődnek, majd éles, háromszög alakú csúcs a SaveAs alatt, majd összeomlás, amint a zip kiürül

A Book.StreamingWrite := True beállítása átkapcsolja a SaveAs-t egy olyan munkalap-íróra, amely a munkalap XML-jét közvetlenül a zip adatfolyamba írja ki, ahogy az generálódik. A köztes karakterlánc soha nem foglalódik le, és a háromszög alakú csúcs beleolvad a háttérzajba

Legyünk pontosak abban, hogy ez valójában mit nyújt, mert a túlértékelése hibás kapacitástervekhez vezet. A jelző csak a mentési útvonalat változtatja meg. A munkafüzet felépítése továbbra is lefoglalja a teljes memóriában tartott cellamodellt, tehát a feltöltési fázis platója pontosan olyan magas marad, mint korábban. Ami eltűnik, az a szerializációs csúcs, amely korábban erre a platóra rakódott rá mentéskor, és egy 400 ezer sort feltöltő feladatnál ez a csúcs rendszerint a teljes különbség a memóriakeret betartása és túllépése között. A tulajdonság alapértelmezetten False, hogy megőrizze a korábbi viselkedést, így a bekapcsolása egy tudatosan, kifejezetten megírt sor

Delphi köteg memória időben HotXLS-szel: az alapértelmezett SaveAs átmeneti munkalap XML karakterlánccsúcsot halmoz a kitöltési platóra, míg a Book.StreamingWrite := True laposan tartja a profilt mentés közben
A feltöltési fennsík mindkét esetben azonos, mert a cellamodell továbbra is memóriában épül; a StreamingWrite csak a mentési szerializálási csúcsot szünteti meg

Tömeges export bekapcsolt jelzővel

Book := TXLSXWorkbook.Create;
try
  BoldIdx := Book.Fonts.Add('Calibri', 11, True, False); // tár index, 0-alapú
  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-alapú a cellánál
  end;
  Book.StreamingWrite := True;   // a munkalap XML-jét közvetlenül a zipbe streameli
  Book.SaveAs('bulk.xlsx');
finally
  Book.Free;
end;

A Cells[R, C] igény szerint hozza létre a cellákat, ami tisztán tartja a ciklus törzsét. Két rácskorlátot érdemes fejben tartani: 1 048 576 sor és 16 384 oszlop, amelyeket a XlsxMaxRow és a XlsxMaxCol tár fel. Egy olyan adatfolyamot, amely túllépi a sorkorlátot, a saját kódodban kell munkalapok közé szétosztanod. Semmi lentebb nem veszi észre a túllépést, és nem javítja ki helyetted, a fájl egyszerűen csonkolva végzi a korlátnál

Sorok feltöltése cellánkénti Variant-overhead nélkül

Minden Cells[R, C].Value értékadás megfizeti egy cellakeresés és egy Variant-konverzió árát. Tízezer sornál ezt senki sem veszi észre. Egymillió, egyenként húsz oszlopos sornál ez a hívásonkénti overhead válik a feltöltési fázis domináns költségévé, és a profiler egyenesen rámutat. A kötegelt felületek lehetővé teszik, hogy egyszerre egy egész sort adj át az írónak. A WriteRows egy visszahívást vezérel, amely hívásonként egy sort szolgáltat:

HotXLS WriteRows callback folyamat Delphi-ben: egy lekérdezéskurzor hívásonként egy sort ad a FillRow callbacknek, amely értékek variant tömbjét tölti, vagy Skipet és Cancelt emel, és a munkalap soronként töltődik
A WriteRows a ciklust a HotXLS-re bízza, miközben a visszahívás hívásonként egy variant-tömb sort szolgáltat, a Skip soronkénti kiszállás, a Cancel pedig a tiszta, egész futást lezáró megállás
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;              // az adatforrás kiürült: tiszta leállás
    Exit;
  end;
  Values := VarArrayCreate([FirstCol, LastCol], varVariant);
  Values[FirstCol]     := FReader.RecordId;
  Values[FirstCol + 1] := FReader.CustomerName;
  Values[FirstCol + 2] := FReader.Amount;
end;

// a 2..100001 sorokat tölti ki, A..C oszlopokban, az olvasóból húzva az adatokat
Sheet.WriteRows(2, 1, 100001, 3, FillRow);

A Cancel jelző alakítja át a rögzített sortartományt „legfeljebb N sorrá", ami a természetes forma, amikor a sorok száma egy még be nem fejezett lekérdezésből származik. A Skip enyhébb eszköz: üresen hagy egy adott sort anélkül, hogy megállítaná a futást. A cellák feltöltésén túl a visszahívás jó helynek bizonyul azoknak az üzemeltetési szempontoknak, amelyeket egyébként esetlenül kellene a feltöltő ciklusra ráépíteni. Egy ezer soronként ketyegő folyamatjelző, egy a jobütemezőből lekérdezett megszakítási token, egy sebességkorlátozó a forrás-adatbázis olvasásain: mindez egy helyen él, ahelyett hogy szétszórva futna a cellaíró kódban. Az olvasás oldalán a ForEachRow és a ForEachCell ugyanezt a mintát tükrözi, ami akkor számít, amikor egy kötegelt feladat nagy fájlokat egyszerre fogyaszt és állít elő

A stílustárak megjutalmazzák a kiemelést

Az XLSX stílusmodell megosztott tárak halmaza. A Fonts.Add, a Fills.AddSolid és a Borders.Add mind 0-alapú tárindexet ad vissza, és egy cella úgy hivatkozik egy betűtípusra, hogy ezt az indexet plusz egyet eltárolja a FontIndex-ben, ahol a nulla a munkafüzet alapértelmezettjének van fenntartva. A +1 pontosan ott van a fenti tömeges példában. Ha elfelejted, a cella csendben a rossz stílust veszi fel, mert egy eltolással hibás stílustár-index még mindig érvényes index, és semmi nem dob hibát

Az ebből következő fegyelem az, hogy minden stílusobjektumot a sorciklus előtt hozz létre, és a ciklusban csak az indexére hivatkozz. A Fonts.Add deduplikálja az azonos definíciókat, így soronkénti egyszeri meghívása csak CPU-t pazarol. Az Alignments.Add a csapda, mert minden hívásnál egy friss bejegyzést ad vissza. Egy 100 ezer soros ciklusban ez százezer duplikált igazítási rekord alá temeti a styles.xml-t, ami felfújja a fájlt a lemezen, és lelassítja minden későbbi Excel-megnyitást, ahogy a duplikátumokat újra feldolgozza. Építs fel minden stílust egyszer, a cikluson kívül, majd hivatkozz az indexére annyiszor, ahányszor szükséges

Adatfolyamok, ideiglenes könyvtárak, és az egészet körülvevő kötegelt ciklus

Mindehhez nincs szükség fájlrendszerre. Mindkét felület TStream túlterhelést hordoz az IO-felületén, köztük az Open, a SaveAs, a SaveAsCSV, a SaveAsHTML és a SaveAsODS, így egy kötegelt feldolgozó közvetlenül egy blob-tárolásra vagy HTTP-válaszra szánt TMemoryStream-be tud renderelni anélkül, hogy valaha is a lemezhez nyúlna. Van egy éles buktató, amit érdemes megjegyezni. A SaveAs(Stream) az adatfolyam aktuális pozíciójától kezdve ír, és utána nem tekeri vissza, ezért a Position := 0-t neked magadnak kell beállítanod, mielőtt átadod az adatfolyamot annak, ami kézbesíti, különben a fogyasztó nulla bájtot olvas. Az XLS-felület két saját kapcsolót ad hozzá. A SetTempDir a BIFF-író ideiglenes fájljait egy olyan kötetre irányítja, amelynek van elég helye és IO-tartaléka ezek befogadásához, ami olyan szervereken számít, ahol az alapértelmezett ideiglenes útvonal egy szűkös rendszerlemezen van. A UseSharedFormulas az ismétlődő képlettörzseket megosztott csoportokba vonja össze, ami valódi méretcsökkentés a klasszikus jelentésformánál, ahol egy képletet egy teljes oszlopon végigmásolnak

Maga a kötegelt ciklus szándékosan unalmas marad:

for FileName in SourceFiles do
begin
  Book := TXLSXWorkbook.Create;        // friss példány: nincs állapotszivárgás
  try
    Book.StreamingWrite := True;
    if Book.Open(FileName) <> 1 then
      Continue;                        // egyetlen rossz bemenet sem ölheti meg a köteget
    Book.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
  finally
    Book.Free;
  end;
end;

Egy friss munkafüzet-példány fájlonként mikroszekundumokba kerül, és megszünteti a fájlok közötti szennyeződési hibák egész kategóriáját: a 17. fájl stílusainak, elnevezett tartományainak és dokumentumtulajdonságainak nincs útja átszivárogni a 18. fájlba. A sikertelen Open utáni kihagyás-és-folytatás ugyanennyire megéri, mert egy csonka feltöltésnek egy 600 fájlos kötegben egyetlen naplósort kellene kóstálnia, nem a futás hátralévő részét. Érdemes azt is megjegyezni, amit a CSV-ág szándékosan nem tesz meg. A SaveAsCSV a képleteket szó szerinti szövegként írja ki, és soha nem értékeli ki őket, így egy konverziós kötegnek, amelynek fogyasztói kiszámított számokra számítanak, előbb futtatnia kell a Calculate-et az érintett cellákon, vagy olyan munkafüzetekből kell indulnia, amelyek már egy korábbi számításból származó gyorsítótárazott eredményeket hordoznak

Párhuzamossági modell: egy munkafüzet szálanként

Egyik felület objektumai sem szálbiztosak, és a tervezés soha nem is állította ennek ellenkezőjét. Mivel a példányok között nincs megosztott globális állapot, a skálázási szabály egyszerűen egy munkafüzet munkaszálanként, munkafüzet szálak közötti megosztása nélkül. Egy N munkásból álló pool, amelyben mindegyik a saját TXLSXWorkbook-ját birtokolja, közel lineárisan skálázódik, amíg a memória nem válik korláttá, és ez a korlát olyasmi, aminek meg tudod adni a számát: a legnagyobb egyidejű cellamodell szorozva a munkások számával, plusz bármennyi mentéskori overhead, amit a StreamingWrite már ellaposított. Amikor a sor mélyre nyúlik, a visszanyomást a jobsornál alkalmazd, ne az írón belül. Egy kiéheztetett szál, amely félig megírt egy munkafüzetet, semmi hasznosat nem termelt, míg egy feladat, amely néhány másodpercet várt egy szabad munkásra, sértetlenül fejeződik be

HotXLS párhuzamosítási modell Delphi szerver kötegfeladatokhoz: feladatsor táplál munkaszálakat, amelyek mindegyike privát TXLSXWorkbook példányt birtokol, visszanyomással a soron és memóriával skálázási plafonként
A munkafüzet-példányok nem osztanak meg globális állapotot, így szálonként egy munkafüzet addig skálázódik, amíg az egyidejű cellamodellek el nem érik a memóriaplafont

A szélesebb hangolási képhez, beleértve a megosztott képleteket, a grafikák kihagyását az olvasás oldalán, és az XLS-specifikus karokat, lásd a nagy munkafüzetek teljesítményéről szóló útmutatót. Azokat a kötegelt feladatokat, amelyek sorai közvetlenül egy lekérdezésből származnak, külön tárgyalja a Delphi jelentések adatbázis-exportálási mintáiról szóló cikk

A HotXLS natív Object Pascalként épül be a Delphi vagy C++Builder szolgáltatásodba, külső függőségek nélkül; a kiadásokat és a licencelést a HotXLS Delphi Component termékoldala ismerteti