Technical Article

Performanse velikih Excel radnih svezaka u Delphi-ju uz HotXLS

Kada izvoz sa 300.000 redova prekorači memorijski budžet, obično se krivi broj redova. Taj broj je uglavnom nevin. Skupi delovi velike radne sveske su oni koji nastaju kao nuspojava: skup stilova (style pool) koji raste za po jednu stavku po ćeliji jer je formatiranje dodato unutar petlje, XML radnog lista spojen u jedan ogroman string u trenutku čuvanja, i milion identičnih tela formula sačuvanih jedno po jedno. HotXLS, losLab-ova izvorna (native) Delphi biblioteka za XLS and XLSX datoteke, pruža vam specifičnu polugu za svaki od ovih troškova. Nijedna od njih nije podrazumevano omogućena jer svaka menja kompromise (trade-offs), pa je poznavanje toga koja poluga odgovara kom simptomu zapravo ključna veština za optimizaciju performansi.

Gde velika radna sveska troši memoriju

Postoje dva različita memorijska režima o kojima treba razmišljati. Tokom generisanja, model ćelija u memoriji raste sa svakom ćelijom koju dodirnete: vrednosti, formati i formule postaju objekti ili unosi u zajednički skup (pool). Tokom čuvanja, podrazumevana XLSX putanja dodatno renderuje XML svakog radnog lista u WideString pre nego što ga kompresuje u zip kontejner, tako da je vršna potrošnja model plus serijalizovana forma najvećeg lista. Posao koji preživi petlju izgradnje, a zatim pukne unutar SaveAs, nailazi na drugi režim, a ne na prvi, i rešenje za jedan ne pomaže kod drugog.

Veličina datoteke prati slično pravilo: ćelije su samo jedan od činilaca, pored stilova, deljenih stringova, formula, slika i komentara. Prolaz revizije (audit pass) pomoću ForEachCell i brojači kolekcija po listu govore vam koji resurs zapravo dominira u problematičnoj datoteci pre nego što počnete da optimizujete pogrešnu stvar. Jedna suptilnost kod merenja: Sheet.Cells.Count na XLSX strani prijavljuje broj instanciranih ćelija u skladištu, a ne površinu iskorišćenog opsega (used range). List čiji podaci zauzimaju pravougaonik dimenzija 1000 sa 50, pri čemu je polovina ćelija prazna, broji oko 25.000, a ne 50.000 ćelija. Ta razlika je važna kada poredite klijentovu "ogromnu" datoteku sa vašim testnim datotekama, jer se površina iskorišćenog opsega i stvarna naseljenost ćelijama mogu razlikovati za red veličine u retkim finansijskim rasporedima.

StreamingWrite rešava putanju čuvanja, a ne putanju izgradnje

Postavljanje TXLSXWorkbook.StreamingWrite := True prebacuje SaveAs na strimujući serijalizator koji upisuje XML radnog lista direktno u zip tok, eliminišući privremeni string po listu. Podrazumevano je podešen na False radi kompatibilnosti ponašanja, a njegovo uključivanje je promena od jedne linije koda:

Book := TXLSXWorkbook.Create;
try
  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;
  end;
  Book.StreamingWrite := True;   // sheet XML streams into the zip container
  Book.SaveAs('bulk.xlsx');
finally
  Book.Free;
end;

Budite precizni oko toga šta time dobijate: model ćelije izgrađen petljom zauzima potpuno isto toliko memorije kao i pre. StreamingWrite ravna skok u potrošnji memorije tokom čuvanja, što predstavlja razliku između batch posla koji se uspešno završava i onog koji pukne na 95% izvršenja. Ako sama petlja za izgradnju iscrpi memoriju, poluge koje su vam potrebne su sledeće dve.

Skupovi stilova: dodajte jednom, ponovo koristite indeks

XLSX formatiranje u HotXLS-u je zasnovano na zajedničkim skupovima (pools): Book.Fonts.Add(...), Fills.AddSolid(...) i Borders.Add(...) vraćaju indeks skupa zasnovan na 0 (0-based) na koji se ćelije pozivaju. Pozivanje Fonts.Add sa identičnim parametrima unutar petlje se deduplira, tako da gubi vreme, a ne prostor. Alignments.Add se ponaša drugačije: vraća nov objekat pri svakom pozivu, pa kreiranje poravnanja po ćeliji linearno povećava skup sa brojem redova. Jedna navika pokriva oba slučaja. Razrešite svaki indeks skupa jednom, izvan petlje, i dodeljujte indekse unutar nje.

// hoist pool lookups out of the hot loop
HeaderFont := Book.Fonts.Add('Calibri', 11, True, False);   // 0-based pool index
for C := 1 to 24 do
  Sheet.Cells[1, C].FontIndex := HeaderFont + 1;            // cells store 1-based; 0 = default

To + 1 nije greška u kucanju, a njegovo zaboravljanje je klasičan bag koji generiše probleme: zajednički skupovi dodeljuju indekse počevši od 0, dok svojstva sa strane ćelije tretiraju 0 kao "podrazumevanu vrednost", tako da se svaki indeks skupa mora pomeriti za jedan prilikom dodele. Ako ovo zaboravite, vaša zaglavlja će se tiho iscrtati u podrazumevanom fontu radne sveske, što je nedostatak koji niko ne primećuje do pregleda brenda.

Zamenite Variant dodelu po ćeliji povratnim pozivima za redove

Svako Sheet.Cells[R, C].Value := X uključuje pronalaženje ili kreiranje ćelije plus dodelu Variant vrednosti. Na nivou od nekoliko stotina hiljada ćelija, taj režijski trošak po pristupu postaje merljiv u profilisanju performansi. HotXLS nudi bulk callback API-je na obe fasade (ForEachCell i ForEachRow za čitanje, WriteCells i WriteRows za pisanje) koji premeštaju iteraciju unutar samog mehanizma i predaju vašem kodu cele redove odjednom:

procedure TLedgerExport.FillRow(Sender: TObject;
  SheetIndex, Row, FirstCol, LastCol: Integer;
  var Values: Variant; var Skip: Boolean; var Cancel: Boolean);
begin
  if Row > FCount then
  begin
    Cancel := True;     // stop the whole write
    Exit;
  end;
  Values := VarArrayOf([FRows[Row - 1].Account,
                        FRows[Row - 1].PostedOn,
                        FRows[Row - 1].Amount]);
end;

// one engine call instead of hundreds of thousands of property hits
Sheet.WriteRows(1, 1, FCount, 3, FillRow);

Zastavica Skip unutar callback-a ostavlja red netaknutim bez prekidanja, a Cancel prevremeno završava operaciju, što je korisno kada je izvor čitač čiju dužinu otkrivate u hodu. Uparite WriteRows za izgradnju sa StreamingWrite za čuvanje i putanja generisanja više neće imati uskih grla po ćeliji.

Poluge za čitanje na XLS fasadi

Velike stare .xls datoteke imaju sopstveni komplet alata. _DisableGraphics := True pre poziva Open u potpunosti preskače parsiranje grafičkog sloja (drawing layer), što ubrzava učitavanje radnih svezaka koje sadrže godinama akumulirane oblike i ugrađene slike. Ovo ograničenje je striktno: grafički sloj je tada odsutan iz modela, pa čuvanje takve radne sveske upisuje datoteku bez njenih crteža. Rezervišite ovu zastavicu samo za poslove analize koji samo čitaju podatke. SetTempDir preusmerava privremene datoteke BIFF pisca, što je važno na serverima gde podrazumevana privremena lokacija ima kvotu ili se nalazi na sporom skladištu. UseSharedFormulas grupiše ponovljena tela formula u zapise deljenih formula, smanjujući datoteke u kojima se kolona sa formulom ponavlja kroz šezdeset hiljada redova.

Petlje čitanja preko XLS podataka imaju zamku sa indeksiranjem koju vredi istaći jer duplira posao kada se njom rukuje defanzivno, a kvari rezultate kada se propusti: UsedRange prijavljuje svoje granice FirstRow, LastRow, FirstCol i LastCol počevši od 0 (0-based), dok je Cells.Item[Row, Col] zasnovan na 1 (1-based). Skeniranje koje prolazi kroz iskorišćeni opseg mora dodati jedan svakoj koordinati prilikom pristupa ćeliji, kao u Cells.Item[Row + 1, Col + 1], inače čita mrežu pomerenu dijagonalno za jednu ćeliju, tiho ispuštajući poslednji red i kolonu i uključujući fantomski prvi red. Povratni poziv ForEachCell u potpunosti izbegava ovaj nesklad, što je još jedan razlog da mu date prednost pri skeniranju celog lista.

Ispitajte datoteke pre nego što ih učitate

Najjeftinija operacija sa velikom radnom sveskom je ona koju izbegnete. Funkcija GetSheetNames na obe fasade izlistava radne listove datoteke bez učitavanja podataka ćelija. XLSX implementacija čita samo manifest radne sveske unutar zip-a i eksplicitno ostavlja instancu radne sveske nepopunjenom, a XLS fasada zaustavlja skeniranje na granici prvog podtoka (substream). To ga čini pravim pre-flight testom za pitanje "koji list treba da cilja ovaj posao uvoza", a CanReadEncrypted daje odgovor na pitanje "da li je ovo šifrovani kontejner" pre osuđenog pokušaja poziva Open.

Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
  if Book.GetSheetNames('big-unknown.xlsx', Names) <= 0 then
    raise Exception.Create('cannot enumerate sheets');   // failure clears the list
  // pick the target sheet, then decide whether a full Open is worth it
finally
  Book.Free;
  Names.Free;
end;

Obratite pažnju na konvenciju povratnog koda: ove funkcije za ispitivanje signaliziraju neuspeh vrednostima koje su jednake ili manje od nule i prazne izlaznu listu, tako da testirate <= 0 umesto poređenja sa jednom specifičnom vrednošću uspeha.

Prilagođavanje pristupa poslu

Za automatizovane procese (pipelines) koji generišu mnogo velikih datoteka u nizu, još dve navike upotpunjuju sliku. Objekti radne sveske nisu bezbedni za deljenje među nitima (thread-safe), ali vas ništa ne sprečava da imate po jednu nezavisnu radnu svesku po radnoj niti (worker thread), što čisto paralelizuje batch konverziju. A kada izlaz ide na HTTP umesto na disk, preopterećenja metode čuvanja u TStream kombinuju se sa StreamingWrite tako da se veliki odgovor nikada ne materijalizuje kao privremena datoteka. Važi i jedna operativna fusnota: čuvanje u tok piše od trenutne pozicije bez premotavanja, tako da postavite Position := 0 pre nego što tok predate okviru za odgovor (response framework). Članak o strimujućem pisanju i batch poslovima razvija taj serverski šablon, a članak o izvozu baze podataka pokazuje gde se ove poluge uklapaju u izveštaj zasnovan na skupu podataka.

Na kraju, čuvajte jednu testnu datoteku (fixture) najgoreg slučaja po familiji izveštaja i merite joj vreme u CI okruženju. Regresije performansi pri generisanju dokumenata retko se same najavljuju. Stil dodat unutar petlje ili ispitivanje zamenjeno potpunim otvaranjem preko Open ne menjaju ništa funkcionalno, a noćni batch posao jednostavno traje četrdeset minuta duže. Vremenski ograničen test na reprezentativnoj testnoj datoteci od pola miliona ćelija pretvara to odstupanje u crveni build (neuspešnu izgradnju) umesto u operativni incident.

Evaluacione verzije, demo projekti sa primerom masovnog generisanja i kompletna referenca API-ja dostupni su na stranici HotXLS komponente.