Technical Article

HotXLS strimujuće pisanje za batch poslove na Delphi serverima

Pretpostavimo da noćni Delphi servis generiše jednu XLSX datoteku po klijentu – ukupno nekoliko stotina datoteka, od kojih su neke široke i po 400.000 redova. Profilišite taj proces i iznenađenje će retko biti petlja za popunjavanje ćelija. To je zapravo poziv metode SaveAs. Sa podrazumevanim piscem, svaki radni list se serijalizuje u jedan XML string u memoriji pre nego što se taj string kompresuje u OOXML zip, a za širok list taj privremeni string može učiniti smešnim model ćelija iz kog je izgrađen. Tako da će posao koji udobno gradi svoje podatke i stoji na 800 MB naglo skočiti preko limita kontejnera od 2 GB tokom čuvanja, a OOM killer (pokretač za oslobađanje memorije operativnog sistema) podnosi izveštaj o grešci u 03:00 ujutru kada niko ne gleda. HotXLS, losLab-ova izvorna biblioteka za radne tabele namenjena Delphi-ju i C++Builder-u, ima svojstvo usmereno direktno na taj skok: StreamingWrite. Oko njega stoje još dve poluge koje odlučuju o tome da li će batch radnik ostati unutar svog memorijskog i vremenskog budžeta, a to su povratni pozivi (callbacks) za pisanje na nivou reda i način na koji se skup stilova ponaša unutar tesne petlje.

Šta podrazumevana putanja čuvanja baferuje, a šta StreamingWrite menja

Podrazumevani XLSX pisac favorizuje jednostavnost. On u potpunosti renderuje XML radnog lista, a zatim predaje gotov string zip kompresoru. To je ispravan kompromis za ogromnu većinu radnih svezaka, gde XML celog lista staje u nekoliko megabajta. Prestaje da bude ispravan kada serijalizovana forma jednog lista dostigne stotine megabajta. XML radnih tabela je opširan: svaka numerička ćelija košta na desetine karaktera markup-a, a string koji drži sve to mora biti neprekidan (contiguous). Na grafikonu memorije taj potpis je teško promašiti: dugačak ravan plato dok se redovi popunjavaju, zatim oštar trouglasti skok tokom SaveAs, a onda kolaps nakon što se zip isprazni.

Postavljanje Book.StreamingWrite := True prebacuje SaveAs na pisac radnog lista koji emituje XML lista direktno u zip tok onako kako se generiše. Privremeni string se nikada ne alocira, a trouglasti skok se smanjuje na nivo šuma.

Budite precizni oko toga šta vam to zapravo donosi, jer preuveličavanje vodi do pogrešnih planova kapaciteta. Ova zastavica menja samo putanju čuvanja. Izgradnja radne sveske i dalje alocira kompletan model ćelija u memoriji, tako da je plato tokom faze popunjavanja potpuno iste visine kao i ranije. Ono što nestaje jeste skok serijalizacije koji se nekada slagao na vrh tog platoa u vreme čuvanja, a za posao koji popunjava 400.000 redova taj skok je rutinski čitava razlika između uklapanja u memorijski budžet i njegovog prekoračenja. Svojstvo je podrazumevano podešeno na False radi očuvanja istorijskog ponašanja, tako da je uključivanje jedna eksplicitna linija koda koju pišete namerno.

Masovni izvoz sa uključenom zastavicom

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;

Poziv Cells[R, C] kreira ćelije na zahtev, što održava telo petlje čistim. Dve granice mreže vredi zapamtiti: 1.048.576 redova i 16.384 kolona, izložene kao XlsxMaxRow and XlsxMaxCol. Izvor podataka koji prekorači ograničenje redova mora biti podeljen na više listova u vašem sopstvenom kodu. Ništa nizvodno neće primetiti to prekoračenje niti ga ispraviti umesto vas, i datoteka će jednostavno završiti skraćena na toj granici.

Popunjavanje redova bez režijskog troška Varijanti po ćeliji

Svaka dodela vrednosti Cells[R, C].Value plaća cenu pronalaženja ćelije i konverzije Variant-a. Na deset hiljada redova to niko ne primećuje. Na milion redova od po dvadeset kolona, taj režijski trošak po pozivu postaje dominantan trošak faze popunjavanja, i profilajler će pokazati pravo na njega. Batch interfejsi vam omogućavaju da umesto toga piscu predate ceo red odjednom. WriteRows pokreće povratni poziv (callback) koji obezbeđuje po jedan red 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 opseg redova u "do N redova", što je prirodan oblik kada broj redova dolazi iz upita čije izvršavanje još niste završili. Skip je blaži pristup: ostavlja pojedinačni red praznim bez zaustavljanja rada. Pored popunjavanja ćelija, povratni poziv se ispostavlja kao dobro mesto za operativne brige koje se inače na nezgodne načine montiraju na petlju popunjavanja. Brojač napretka koji otkucava na svakih hiljadu redova, cancellation token koji se preuzima iz planera poslova, limiter brzine čitanja iz izvorne baze podataka – sve to živi na jednom mestu umesto da bude provučeno kroz kod za pisanje ćelija. Na strani čitanja, ForEachRow i ForEachCell odražavaju isti šablon, što je važno kada batch posao i troši i proizvodi velike datoteke.

Zajednički skupovi stilova nagrađuju podizanje koda

XLSX model stilizovanja je skup deljenih resursa (pools). Fonts.Add, Fills.AddSolid i Borders.Add vraćaju indeks skupa zasnovan na 0, a ćelija referencira font čuvanjem tog indeksa plus jedan u FontIndex, gde je nula rezervisana za podrazumevani font radne sveske. To +1 se nalazi direktno u masovnom primeru iznad. Zaboravite ga i ćelija će tiho pokupiti pogrešan stil, ove su vrednosti pomerene za jedan i dalje važeći indeks i ništa neće pokrenuti grešku.

Disciplina koja sledi jeste kreiranje svakog objekta stila pre petlje redova i referenciranje njegovog indeksa unutar petlje. Fonts.Add uklanja duplikate identičnih definicija, tako da njegovo pozivanje jednom po redu samo troši procesorsko vreme. Alignments.Add je zamka jer vraća nov unos pri svakom pozivu. Unutar petlje od 100.000 redova, to zatrpava styles.xml sa sto hiljada dupliranih zapisa o poravnanju, što naduvava datoteku na disku i usporava svako kasnije otvaranje u Excel-u jer se duplikati moraju ponovo parsirati. Izgradite svaki stil jednom izvan petlje, a zatim referencirajte njegov indeks onoliko puta koliko vam je potrebno.

Tokovi, privremeni direktorijumi i batch petlja oko svega

Ništa od ovoga ne zahteva fajl sistem. Obe fasade nose preopterećenja sa TStream kroz svoju celokupnu I/O površinu, uključujući Open, SaveAs, SaveAsCSV, SaveAsHTML i SaveAsODS, tako da batch radnik može renderovati podatke direktno u TMemoryStream namenjen skladištu objekata (blob storage) ili HTTP odgovoru, bez dodirivanja diska. Postoji jedna oštra ivica koju treba zapamtiti: SaveAs(Stream) piše od trenutne pozicije toka i ne premotava ga nakon toga, tako da sami postavite Position := 0 pre nego što predate tok onome što ga isporučuje, inače potrošač čita nula bajtova. XLS fasada dodaje dva sopstvena dugmeta: SetTempDir usmerava privremene datoteke BIFF pisca na particiju koja ima prostor i I/O propusni opseg da ih apsorbuje, što je važno na serverima gde podrazumevana privremena putanja leži na tesnom sistemskom disku. UseSharedFormulas sklapa ponovljena tela formula u deljene grupe, što donosi stvarno smanjenje veličine za klasični oblik izveštaja gde se jedna formula kopira duž cele kolone.

Sama batch petlja namerno ostaje 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 sveske po datoteci košta mikrosekunde i uklanja čitavu kategoriju bagova kontaminacije između datoteka: stilovi, definisana imena i svojstva dokumenta iz datoteke 17 nemaju putanju da procure u datoteku 18. Preskakanje i nastavak rada kod neuspelog poziva Open opravdava svoje postojanje u istoj meri, jer jedno prekinuto otpremanje u grupi od 600 datoteka treba da vas košta samo jedne linije u logu, a ne ostatka celog pokretanja. Vredi istaći i ono što CSV smer namerno ne radi: SaveAsCSV upisuje formule kao doslovni tekst i nikada ih ne procenjuje, tako da grupa za konverziju čiji potrošači očekuju izračunate brojeve mora prvo da pokrene Calculate na odgovarajućim ćelijama, ili da krene od radnih svezaka koje već nose keširane rezultate iz prethodnog proračuna.

Model konkurentnosti: jedna radna sveska po niti

Objekti nijedne fasade nisu bezbedni za niti (thread-safe), i dizajn nikada nije tvrdio suprotno. Pošto ne postoji deljeno globalno stanje između instanci, pravilo skaliranja je jednostavno: jedna radna sveska po radnoj niti (worker thread), bez deljenja radne sveske između više niti. Skup (pool) od N radnika, od kojih svaki poseduje sopstveni TXLSXWorkbook, skalira se skoro linearno sve dok memorija ne postane plafon, a taj plafon je nešto na šta možete staviti broj: najveći konkurentni model ćelija pomnožen sa brojem radnika, plus bilo koji režijski trošak vremena čuvanja koji je StreamingWrite izravnao. Kada red za čekanje postane dugačak, primenite povratni pritisak (back-pressure) na redu poslova umesto unutar samog pisca. Izgladnela nit koja je polovično napisala radnu svesku nije proizvela ništa korisno, dok se posao koji je sačekao nekoliko sekundi na slobodnog radnika završava uspešno i netaknut.

Za širu sliku podešavanja, uključujući deljene formule, preskakanje grafike na strani čitanja i poluge specifične za XLS, pogledajte vodič za performanse velikih radnih svezaka. Batch poslovi čiji redovi dolaze direktno iz upita pokriveni su odvojeno u članku o šablonima izvoza baze podataka za Delphi izveštaje.

HotXLS se kompajlira u vaš Delphi ili C++Builder servis kao izvorni Object Pascal bez spoljnih zavisnosti; izdanja i licenciranje nalaze se na stranici proizvoda HotXLS komponente.