Technical Article

HotXLS strujno pisanje za Delphi grupne poslove na poslužitelju

Recimo da noćna Delphi usluga generira jednu XLSX datoteku po kupcu, nekoliko stotina datoteka, od kojih su neke široke 400.000 redaka. Profilirajte to i iznenađenje je rijetko u petlji za popunjavanje ćelija. To je poziv SaveAs. Kod zadanog pisca, svaki se radni list serijalizira u jedan XML niz u memoriji prije nego što se taj niz komprimira u OOXML zip, a za široki list taj prolazni niz može patuljkom učiniti model ćelije iz kojeg je izgrađen. Tako će posao koji ugodno gradi svoje podatke i sjedi na 800 MB skočiti preko ograničenja spremnika od 2 GB tokom spremanja, a OOM killer podnosi izvješće o pogrešci u 03:00 kada nitko ne gleda. HotXLS, nativna losLab biblioteka za proračunske tablice za Delphi i C++Builder, ima svojstvo usmjereno izravno na tu vršnu točku: StreamingWrite. Oko njega nalaze se još dvije poluge koje odlučuju hoće li grupni radnik ostati unutar svog proračuna memorije i vremena, a to su povratni pozivi za pisanje na razini redaka i način na koji se skup stilova ponaša unutar uske petlje.

Što zadana putanja spremanja sprema u međuspremnik i što StreamingWrite mijenja

Zadani XLSX pisac preferira jednostavnost. U potpunosti renderira XML radnog lista, a zatim predaje gotov niz kompresoru zipa. To je ispravan kompromis za veliku većinu radnih knjiga, gdje XML cijelog lista stane u nekoliko megabajta. Prestaje biti ispravan kada serijalizirani oblik jednog lista dosegne stotine megabajta. XML proračunske tablice je opširan: svaka numerička ćelija košta desetke znakova oznake, a niz koji sadrži sve to mora biti neprekinut. Na grafikonu memorije taj je potpis teško promašiti. Dugi ravni plato dok se redci pune, zatim oštar trokutasti skok tijekom SaveAs, pa kolaps nakon što se zip isprazni.

Postavljanje Book.StreamingWrite := True prebacuje SaveAs na pisca radnog lista koji emitira XML lista izravno u zip tok podataka dok se generira. Privremeni niz se nikada ne dodjeljuje, a trokutasti skok se stapa s pozadinskim šumom.

Budite precizni oko toga što vam to točno donosi, jer preuveličavanje toga vodi do pogrešnih planova kapaciteta. Zastavica mijenja samo putanju spremanja. Izgradnja radne knjige i dalje dodjeljuje puni model ćelije u memoriji, tako da je plato tijekom faze popunjavanja potpuno jednako visok kao i prije. Ono što nestaje jest skok serijalizacije koji se nekada slagao na vrh tog platoa u vrijeme spremanja, a za posao koji popunjava 400.000 redaka taj je skok rutinski cijela razlika između uklapanja u proračun memorije i njegova probijanja. Svojstvo je prema zadanim postavkama postavljeno na False kako bi se očuvalo povijesno ponašanje, pa je uključivanje jedna eksplicitna linija koju pišete namjerno.

Grupni izvoz s uključenom zastavicom

Cells[R, C] stvara ćelije na zahtjev, što drži tijelo petlje čistim. Dva ograničenja mreže vrijedi zapamtiti: 1,048,576 redaka i 16,384 stupaca, izložena kao XlsxMaxRow i XlsxMaxCol. Izvor podataka koji premašuje ograničenje redaka mora se podijeliti na više listova u vašem vlastitom kodu. Ništa nizvodno ne primjećuje to prekoračenje niti ga popravlja umjesto vas, a datoteka jednostavno završava skraćena na granici.

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;

Popunjavanje redaka bez režijskog troška Variant tipa po ćeliji

Svaka dodjela Cells[R, C].Value plaća trošak traženja ćelije i pretvorbe Variant tipa. Na deset tisuća redaka to nitko ne primjećuje. Na milijun redaka od po dvadeset stupaca, taj režijski trošak po pozivu postaje dominantan trošak faze popunjavanja, a profiler performansi će pokazati izravno na njega. Grupna sučelja omogućuju vam da piscu predate cijeli redak odjednom. WriteRows pokreće povratni poziv koji pruža jedan redak po pozivu:

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 ono što pretvara fiksni raspon redaka u "do N redaka", što je prirodan oblik kada broj redaka dolazi iz upita koji još niste dovršili s izvođenjem. Skip je lakši dodir: ostavlja pojedinačni redak praznim bez zaustavljanja izvođenja. Osim popunjavanja ćelija, povratni poziv se pokazuje kao dobro mjesto za operativne brige koje se inače na nezgodne načine pričvršćuju na petlju popunjavanja. Brojač napretka koji kuca svakih tisuću redaka, token otkazivanja koji se dohvaća iz raspoređivača poslova, ograničivač brzine čitanja iz izvorne baze podataka: sve to živi na jednom mjestu umjesto da bude protkano kroz kod za pisanje ćelija. Na strani čitanja, ForEachRow i ForEachCell odražavaju isti uzorak, što je važno kada grupni posao i troši i proizvodi velike datoteke.

Skupovi stilova nagrađuju podizanje izvan petlje

Model XLSX oblikovanja je skup dijeljenih bazena. Fonts.Add, Fills.AddSolid i Borders.Add svi vraćaju 0-bazirani indeks skupa, a ćelija se referira na font pohranjivanjem tog indeksa plus jedan u FontIndex, gdje je nula rezervirana za zadanu vrijednost radne knjige. Ovaj +1 nalazi se u gornjem skupnom primjeru. Zaboravite ga i ćelija tiho preuzima pogrešan stil jer je pomak za jedan u indeksu skupa stilova i dalje valjan indeks i ništa ne javlja pogrešku.

Disciplina koja slijedi jest stvoriti svaki objekt stila prije petlje redaka i referencirati njegov indeks unutar petlje. Fonts.Add uklanja duplikate identičnih definicija, pa njegovo pozivanje jednom po retku samo troši CPU. Alignments.Add je zamka jer vraća novi unos pri svakom pozivu. Unutar petlje od 100.000 redaka to zakopava styles.xml ispod sto tisuća dupliciranih zapisa poravnanja, što napuhuje datoteku na disku i usporava svako kasnije otvaranje u Excelu jer se duplikati ponovno analiziraju. Izgradite svaki stil jednom izvan petlje, a zatim referencirajte njegov indeks onoliko puta koliko vam je potrebno.

Tokovi, privremeni direktoriji i grupna petlja oko svega

Ništa od ovoga ne zahtijeva datotečni sustav. Oba sučelja nose preopterećenja TStream preko svoje IO površine, uključujući Open, SaveAs, SaveAsCSV, SaveAsHTML i SaveAsODS, tako da grupni radnik može renderirati izravno u TMemoryStream namijenjen pohrani objekata (blob) ili HTTP odgovoru bez doticaja s diskom. Postoji jedan oštar rub koji treba zapamtiti. SaveAs(Stream) piše s trenutne pozicije toka i ne premotava se nakon toga, stoga sami postavite Position := 0 prije nego što predate tok onome što ga isporučuje, inače potrošač čita nula bajtova. XLS sučelje dodaje dva vlastita gumba. SetTempDir usmjerava privremene datoteke BIFF pisca na volumen koji ima prostor i IO slobodan prostor da ih apsorbira, što je važno na poslužiteljima gdje zadana privremena putanja sjedi na tijesnom sistemskom disku. UseSharedFormulas sklapa ponovljena tijela formula u dijeljene grupe, što je stvarna redukcija veličine za klasični oblik izvješća gdje se jedna formula kopiira niz cijeli stupac.

Sama grupna petlja ostaje namjerno jednostavna

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;

Nova instanca radne knjige po datoteci košta mikrosekunde i uklanja cijelu kategoriju pogrešaka kontaminacije među datotekama: stilovi, definirani nazivi i svojstva dokumenta iz datoteke 17 nemaju načina iscuriti u datoteku 18. Preskakanje i nastavak pri neuspjelom Open zarađuje svoje mjesto jednako tako, jer bi jedan skraćeni prijenos u paketu od 600 datoteka trebao koštati samo jednu liniju dnevnika (log line), a ne prekid ostatka izvođenja. Vrijedi istaknuti i ono što CSV dio namjerno ne radi. SaveAsCSV ispisuje formule kao doslovan tekst i nikada ih ne izračunava, tako da pretvorbena grupa čiji potrošači očekuju izračunate brojeve mora najprije pokrenuti Calculate na relevantnim ćelijama, ili krenuti od radnih knjiga koje već nose predmemorirane rezultate iz prethodnog izračuna.

Model konkurentnosti: jedna radna knjiga po niti

Objekti niti jednog sučelja nisu sigurni za višenitnost (thread-safe), a dizajn se nikada nije ni pretvarao da jesu. Budući da nema zajedničkog globalnog stanja između instanci, pravilo skaliranja je jednostavno jedna radna knjiga po radnoj niti, bez dijeljenja radne knjige među nitima. Bazen od N radnika, od kojih svaki posjeduje vlastiti TXLSXWorkbook, skalira se gotovo linearno sve dok memorija ne postane gornja granica, a ta je granica nešto na što možete staviti broj: najveći konkurentni model ćelije pomnožen s brojem radnika, plus bilo koji režijski trošak vremena spremanja koji je StreamingWrite spljoštio. Kada red čekanja postane dubok, primijenite povratni pritisak (back-pressure) na redu poslova umjesto unutar pisca. Izgladnjela nit koja je napola zapisala radnu knjigu nije proizvela ništa korisno, dok posao koji je pričekao nekoliko sekundi na slobodnog radnika završava netaknut.

Model konkurentnosti: jedna radna knjiga po niti

Za širu sliku ugađanja, uključujući dijeljene formule, preskakanje grafike na strani čitanja i poluge specifične za XLS, pogledajte vodič za performanse velikih radnih knjiga. Grupni poslovi čiji redci dolaze izravno iz upita pokriveni su zasebno u uzorcima izvoza baze podataka za Delphi izvješća.

HotXLS se prevodi u vašu Delphi ili C++Builder uslugu kao nativni Object Pascal bez vanjskih ovisnosti; izdanja i licenciranje nalaze se na stranici proizvoda HotXLS komponente.