Tehnični članek

HotXLS pretočna pisanja za paketna opravila na strežniku Delphi

Recimo, da nočna storitev Delphi generira en XLSX na stranko, nekaj sto datotek, nekatere od njih 400.000 vrstic. Profilirajte jo in presenečenje redko predstavlja zanka za polnjenje celic. To je klic SaveAs. Pri privzetem pisalniku se vsak delovni list serijalizira v en sam XML niz v pomnilniku, preden se ta niz stisne v zip OOXML, in za širok list je prehodni niz lahko večji od modela celic, iz katerega je bil zgrajen. Torej opravilo, ki udobno gradi svoje podatke in sedi pri 800 MB, bo med shranjevanjem preseglodveh gigabajtov v vsebniku in ubijat pomnilnika poroča o hrošču ob 03:00, ko nihče ne gleda. HotXLS, izvorna knjižnica preglednic losLab za Delphi in C++Builder, ima lastnost, namenjeno točno temu skoku: StreamingWrite. Ob njej stojita še dve polgi, ki odločata, ali paketni delavec ostane znotraj svojega proračuna za pomnilnik in čas, namreč povratni klici pisanja na ravni vrstice in način obnašanja skupnega sloga v tesni zanki.

Kaj privzeta pot shranjevanja medpomni in kaj StreamingWrite spremeni

Privzeti pisalnik XLSX daje prednost preprostosti. XML delovnega lista upodobi v celoti, nato preda dokončan niz stiskovalniku zip. To je pravilna zamenjava za ogromno večino delovnih zvezkov, kjer celotni XML lista zavzame le nekaj megabajtov. Preneha biti pravilna, ko serializirana oblika enega lista zraste do sto megabajtov. XML preglednic je gostobeseden: vsaka numerična celica stane desetine znakov označevanja, in niz, ki jih drži vse, mora biti neprekinjen. Na grafu pomnilnika je podpis težko spregledati. Dolga ravna planota med polnjenjem vrstic, nato oster trikotni skok med SaveAs, nato sesedanje, ko je zip izpuščen.

Nastavitev Book.StreamingWrite := True preklopi SaveAs na pisalnik delovnega lista, ki XML lista neposredno oddaja v tok zip med ustvarjanjem. Vmesni niz se nikoli ne dodeli in trikotni skok se poravna v šum.

Bodite natančni glede tega, kaj to dejansko prinaša, ker precenjevanje vodi do napačnih načrtov zmogljivosti. Zastavica spremeni samo pot shranjevanja. Gradnja delovnega zvezka še vedno dodeli celoten model celic v pomnilniku, zato je planota med fazo polnjenja natanko enako visoka kot prej. Kar izgine, je skok serijalizacije, ki se je nekoč nalagal na vrh te planote ob shranjevanju, in za opravilo, ki polni 400.000 vrstic, je ta skok redno celotna razlika med ustrezanjem proračunu za pomnilnik in prekoračitvijo. Lastnost je privzeto False, da ohranja zgodovinsko obnašanje, zato se prijava vanj opravi z eno namerno vrstico.

Skupinski izvoz z vklopljeno zastavico

Book := TXLSXWorkbook.Create;
try
  BoldIdx := Book.Fonts.Add('Calibri', 11, True, False); // pool index, 0-based
  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-based at the cell
  end;
  Book.StreamingWrite := True;   // stream sheet XML straight into the zip
  Book.SaveAs('bulk.xlsx');
finally
  Book.Free;
end;

Cells[R, C] ustvari celice na zahtevo, kar ohranja telo zanke čisto. Dve mrežni omejitvi sta vredni pomnjenja: 1.048.576 vrstic in 16.384 stolpcev, izpostavljeni kot XlsxMaxRow in XlsxMaxCol. Podatkovni vir, ki preseže omejitev vrstic, mora biti razdeljen čez liste v vaši lastni kodi. Nič dolvodni ne zazna prekoračitve ali jo popravi namesto vas, in datoteka se preprosto konča pri omejitvi.

Polnjenje vrstic brez stroškov Variant na celico

Vsaka dodelitev Cells[R, C].Value plača za iskanje celice in pretvorbo Variant. Pri desetih tisoč vrsticah tega nihče ne opazi. Pri milijonu vrstic po dvajset stolpcev vsak strošek na klic postane prevladujoči strošek faze polnjenja in profilator bo nanjo neposredno pokazal. Paketni vmesniki vam omogočajo, da pisalniku posredujete celo vrstico naenkrat. WriteRows poganja povratni klic, ki zagotavlja eno vrstico na klic:

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;              // data source drained: stop cleanly
    Exit;
  end;
  Values := VarArrayCreate([FirstCol, LastCol], varVariant);
  Values[FirstCol]     := FReader.RecordId;
  Values[FirstCol + 1] := FReader.CustomerName;
  Values[FirstCol + 2] := FReader.Amount;
end;

// fill rows 2..100001, columns A..C, pulling from the reader
Sheet.WriteRows(2, 1, 100001, 3, FillRow);

Zastavica Cancel je tista, ki fiksen obseg vrstic spremeni v "do N vrstic", kar je naravna oblika, ko število vrstic izhaja iz poizvedbe, ki je še niste dokončali izvajati. Skip je lažja dotik: posamezno vrstico pusti prazno, ne da bi ustavil izvajanje. Poleg polnjenja celic se povratni klic izkaže za dober dom za operativne skrbi, ki bi sicer bile neprikladne pritrjene na zanko polnjenja. Števec napredka, ki tika vsakih tisoč vrstic, žeton preklica, ki ga anketa razporejevalnik opravil, omejevalnik hitrosti branj iz izvorne baze podatkov: vse to živi na enem mestu namesto, da bi ga bilo treba splesti skozi kodo pisanja celic. Na strani branja ForEachRow in ForEachCell zrcalita isti vzorec, kar je pomembno, ko paketno opravilo tako porablja kot producira velike datoteke.

Skupne zbirke slogov nagradijo dvigovanje

Model slogov XLSX je niz skupnih zbirk. Fonts.Add, Fills.AddSolid in Borders.Add vsi vrnejo indeks skupne zbirke z osnovo 0, celica pa na pisavo sklicuje tako, da ta indeks plus ena shrani v FontIndex, kjer je nič rezerviran za privzetek delovnega zvezka. +1 je prav tam v zgornjem skupinskem primeru. Pozabite ga in celica tiho pobere napačen slog, ker pomik za eno v indeksu skupne zbirke slogov je še vedno veljaven indeks in nič ne sproži napake.

Disciplina, ki sledi, je ustvariti vsak objekt sloga pred zanko vrstic in referenčirati njegov indeks znotraj zanke. Fonts.Add odstrani podvojene enake definicije, zato klicanje enkrat na vrstico samo zapravi CPE. Alignments.Add je past, ker vrne svežo postavko pri vsakem klicu. Znotraj zanke 100.000 vrstic to zasuje styles.xml s sto tisoč podvojenimi zapisi poravnave, kar napihne datoteko na disku in upočasni vsako poznejše odpiranje v Excelu, ko se podvojene vrednosti znova razčlenijo. Vsak slog zgradite enkrat zunaj zanke, nato po potrebi referenčirajte njegov indeks.

Tokovi, začasni imeniki in paketna zanka okoli vsega tega

Nič od tega ne zahteva datotečnega sistema. Obe fasadi nosita preobremenitve TStream čez svojo IO površino, med njimi Open in SaveAs in SaveAsCSV in SaveAsHTML in SaveAsODS, zato paketni delavec lahko upodablja neposredno v TMemoryStream, namenjen shranjevanju blobov ali HTTP odzivov, ne da bi kdaj se dotaknil diska. Obstaja ena ostra rob za zapomniti. SaveAs(Stream) piše od trenutnega položaja toka in se zatem ne navije nazaj, zato nastavite Position := 0 sami, preden tok posredujete vsemu, kar ga dostavi, ali pa potrošnik prebere nič bajtov. Fasada XLS doda dva svoja gumba. SetTempDir usmeri začasne datoteke pisalnika BIFF na zvezek, ki ima prostor in zmogljivost IO za absorpcijo, kar je pomembno na strežnikih, kjer privzeta pot za začasne datoteke sedi na stisnjenem sistemskem disku. UseSharedFormulas zloži ponovljena telesa formul v skupne skupine, resnično zmanjšanje velikosti za klasično obliko poročila, kjer je ena formula kopirana navzdol po celem stolpcu.

Sama paketna zanka ostaja namerno dolgočasna:

for FileName in SourceFiles do
begin
  Book := TXLSXWorkbook.Create;        // fresh instance: no state bleed
  try
    Book.StreamingWrite := True;
    if Book.Open(FileName) <> 1 then
      Continue;                        // one bad input must not kill the batch
    Book.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
  finally
    Book.Free;
  end;
end;

Svež primerek delovnega zvezka na datoteko stane mikrosekunde in odstrani celo kategorijo hrošča kontaminacije med datotekami: slogi, definirana imena in lastnosti dokumenta iz datoteke 17 nimajo poti za uhajanje v datoteko 18. Preskoči-in-nadaljuj pri neuspelem Open zasluži svoje ravno tako, ker ena skrajšana nalaganje v seriji 600 datotek bi morala stati eno samo vrstico dnevnika namesto preostanka zaganjanja. Vredno je opozoriti tudi na to, kar stran CSV namerno ne počne. SaveAsCSV piše formule kot dobesedno besedilo in jih nikoli ne ovrednoti, zato ima paketna konverzija, katere potrošniki pričakujejo izračunane številke, najprej izvesti Calculate na ustreznih celicah ali začeti iz delovnih zvezkov, ki že nosijo predpomnjene rezultate iz predhodnega izračuna.

Model sočasnosti: en delovni zvezek na nit

Objekti nobene fasade niso varni za niti in zasnova tega nikoli ni pretendirala. Ker med primerki ni skupnega globalnega stanja, je pravilo skaliranja preprosto en delovni zvezek na delavsko nit, brez skupne rabe delovnega zvezka med nitmi. Skupina N delavcev, vsak z lastnim TXLSXWorkbook, se skalira skoraj linearno, dokler pomnilnik ne postane strop, in ta strop je tisto, čemur lahko daste število: največji sočasni model celic pomnožen s številom delavcev, plus kakršen koli strošek ob shranjevanju, ki ga je StreamingWrite sploščil. Ko je vrsta globoka, nanesite protitlak pri vrsti opravil namesto znotraj pisalnika. Podhranjena nit, ki je napol zapisala delovni zvezek, ni ustvarila ničesar koristnega, medtem ko opravilo, ki je čakalo nekaj sekund na prostega delavca, zaključi nedotaknjeno.

Za širšo sliko uglaševanja, vključno s skupnimi formulami, preskakanjem grafik na strani branja in vzvodami, specifičnimi za XLS, glejte vodnik zmogljivosti za velike delovne zvezke. Paketna opravila, katerih vrstice prihajajo neposredno iz poizvedbe, so posebej pokrita v vzorcih izvoza baze podatkov za poročila Delphi.

HotXLS se prevede v vašo storitev Delphi ali C++Builder kot izvorna Object Pascal brez zunanjih odvisnosti; izdaje in licenciranje so na strani HotXLS Component.