Să spunem că un serviciu Delphi rulat pe timp de noapte generează câte un XLSX per client, câteva sute de fișiere, unele dintre ele late de 400.000 de rânduri. Profilați-l și surpriza rareori este bucla de umplere a celulelor. Este apelul SaveAs. Cu scriitorul implicit, fiecare foaie de lucru este serializată într-un singur șir XML în memorie înainte ca acel șir să fie comprimat în arhiva OOXML, iar pentru o foaie largă șirul tranzitoriu poate depăși cu mult modelul de celule din care a fost construit. Așa că un job care își construiește confortabil datele și stă la 800 MB va sări brusc peste o limită de container de 2 GB în timpul salvării, iar OOM killer-ul depune raportul de eroare la ora 3 dimineața, când nimeni nu urmărește. HotXLS, biblioteca nativă a losLab pentru foi de calcul, pentru Delphi și C++Builder, are o proprietate țintită direct spre acel vârf: StreamingWrite. În jurul ei stau alte două pârghii care decid dacă un worker de lot rămâne în bugetul său de memorie și timp, și anume callback-urile de scriere la nivel de rând și modul în care se comportă rezerva de stiluri într-o buclă strânsă
Ce pune în buffer calea de salvare implicită, și ce schimbă StreamingWrite
Scriitorul XLSX implicit favorizează simplitatea. Randează complet XML-ul foii de lucru, apoi predă șirul finalizat compresorului zip. Acesta este compromisul corect pentru marea majoritate a registrelor de lucru, unde XML-ul întregii foi încape în câțiva megaocteți. Încetează să fie corect când forma serializată a unei foi ajunge la sute de megaocteți. XML-ul foilor de calcul este verbos: fiecare celulă numerică costă zeci de caractere de marcaj, iar șirul care le conține pe toate trebuie să fie contiguu. Pe un grafic de memorie, semnătura este greu de ratat. Un platou plat și lung cât timp se umplu rândurile, apoi un vârf triunghiular ascuțit în timpul SaveAs, apoi prăbușirea odată ce arhiva zip este scrisă pe disc
Setarea Book.StreamingWrite := True comută SaveAs pe un scriitor de foi de lucru care emite XML-ul foii direct în fluxul zip, pe măsură ce este generat. Șirul intermediar nu este niciodată alocat, iar vârful triunghiular se aplatizează în zgomotul de fond
Fiți exacți în privința a ceea ce vă aduce de fapt acest lucru, pentru că supraestimarea lui duce la planuri de capacitate greșite. Flag-ul schimbă doar calea de salvare. Construirea registrului de lucru încă alocă întregul model de celule în memorie, așa că platoul din faza de umplere este exact la fel de înalt ca înainte. Ce dispare este vârful de serializare care obișnuia să se adauge peste acel platou la momentul salvării, iar pentru un job care umple 400k rânduri, acel vârf reprezintă de regulă toată diferența dintre a vă încadra în bugetul de memorie și a-l depăși. Proprietatea are implicit valoarea False pentru a păstra comportamentul istoric, așa că activarea ei este o linie explicită pe care o scrieți intenționat
Un export în masă cu flag-ul activat
Book := TXLSXWorkbook.Create;
try
BoldIdx := Book.Fonts.Add('Calibri', 11, True, False); // index în rezervă, bazat pe 0
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; // bazat pe 1 la nivelul celulei
end;
Book.StreamingWrite := True; // trimite XML-ul foii direct în arhiva zip
Book.SaveAs('bulk.xlsx');
finally
Book.Free;
end;
Cells[R, C] creează celule la cerere, ceea ce ține corpul buclei curat. Merită reținute în memorie două limite de grilă: 1.048.576 de rânduri și 16.384 de coloane, expuse ca XlsxMaxRow și XlsxMaxCol. Un flux de date care depășește limita de rânduri trebuie împărțit pe mai multe foi în propriul dvs. cod. Nimic din aval nu observă depășirea și nici nu o corectează pentru dvs., iar fișierul pur și simplu ajunge trunchiat la limită
Umplerea rândurilor fără suprasarcina de Variant per celulă
Fiecare atribuire Cells[R, C].Value plătește pentru o căutare de celulă și o conversie Variant. La zece mii de rânduri nimeni nu observă. La un milion de rânduri a câte douăzeci de coloane fiecare, această suprasarcină per apel devine costul dominant al fazei de umplere, iar profilerul va indica exact spre ea. Interfețele de lot vă permit, în schimb, să predați scriitorului câte un rând întreg odată. WriteRows conduce un callback care furnizează un rând la fiecare invocare:
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; // sursa de date epuizată: oprire curată
Exit;
end;
Values := VarArrayCreate([FirstCol, LastCol], varVariant);
Values[FirstCol] := FReader.RecordId;
Values[FirstCol + 1] := FReader.CustomerName;
Values[FirstCol + 2] := FReader.Amount;
end;
// umple rândurile 2..100001, coloanele A..C, extrăgând din cititor
Sheet.WriteRows(2, 1, 100001, 3, FillRow);
Flag-ul Cancel este cel care transformă un interval fix de rânduri în „până la N rânduri”, ceea ce este forma naturală atunci când numărul de rânduri provine dintr-o interogare pe care nu ați terminat de executat-o. Skip este atingerea mai ușoară: lasă un rând individual gol, fără să oprească rularea. Dincolo de umplerea celulelor, callback-ul se dovedește a fi un loc bun pentru preocupările operaționale care altfel se lipesc de o buclă de umplere în moduri stângace. Un contor de progres care avansează la fiecare mie de rânduri, un token de anulare interogat din planificatorul de job-uri, un limitator de rată pentru citirile din baza de date sursă: totul trăiește într-un singur loc, în loc să fie înșirat prin codul de scriere a celulelor. Pe partea de citire, ForEachRow și ForEachCell oglindesc același model, ceea ce contează atunci când un job de lot atât consumă, cât și produce fișiere mari
Rezervele de stiluri răsplătesc ridicarea în afara buclei
Modelul de stilizare XLSX este un set de rezerve partajate. Fonts.Add, Fills.AddSolid și Borders.Add returnează toate un index de rezervă bazat pe 0, iar o celulă referențiază un font stocând acel index plus unu în FontIndex, unde zero este rezervat pentru valoarea implicită a registrului de lucru. Acel +1 se vede chiar acolo, în exemplul în masă de mai sus. Uitați-l, și celula preia tacit stilul greșit, pentru că o eroare off-by-one într-un index de rezervă de stiluri este tot un index valid și nimic nu ridică o excepție
Disciplina care rezultă este să creați fiecare obiect de stil înainte de bucla de rânduri și să referențiați indexul lui în interiorul buclei. Fonts.Add elimină duplicatele pentru definiții identice, așa că apelarea lui o dată per rând doar irosește CPU. Alignments.Add este capcana, pentru că returnează o intrare nouă la fiecare apel. Într-o buclă de 100k rânduri, asta îngroapă styles.xml sub o sută de mii de înregistrări de aliniere duplicate, ceea ce umflă fișierul pe disc și încetinește fiecare deschidere ulterioară în Excel, pe măsură ce duplicatele sunt reanalizate. Construiți fiecare stil o singură dată în afara buclei, apoi referențiați-i indexul de câte ori aveți nevoie
Fluxuri, directoare temporare și bucla de lot din jurul tuturor
Nimic din toate acestea nu necesită un sistem de fișiere. Ambele fațade poartă supraîncărcări TStream pe toată suprafața lor de I/O, printre care Open, SaveAs, SaveAsCSV, SaveAsHTML și SaveAsODS, astfel încât un worker de lot poate randa direct într-un TMemoryStream destinat unei stocări blob sau unui răspuns HTTP, fără să atingă vreodată discul. Există o muchie ascuțită de reținut. SaveAs(Stream) scrie de la poziția curentă a fluxului și nu derulează înapoi după aceea, așa că setați Position := 0 chiar dvs., înainte de a preda fluxul către orice îl livrează, altfel consumatorul citește zero octeți. Fațada XLS adaugă două butoane proprii. SetTempDir îndreaptă fișierele temporare ale scriitorului BIFF către un volum care are spațiul și rezerva de I/O necesare pentru a le absorbi, ceea ce contează pe servere unde calea temporară implicită se află pe un disc de sistem înghesuit. UseSharedFormulas pliază corpurile de formule repetate în grupuri partajate, o reducere reală de dimensiune pentru forma clasică de raport în care o formulă este copiată în jos pe o coloană întreagă
Bucla de lot în sine rămâne plictisitoare, intenționat:
for FileName in SourceFiles do
begin
Book := TXLSXWorkbook.Create; // instanță nouă: fără scurgere de stare
try
Book.StreamingWrite := True;
if Book.Open(FileName) <> 1 then
Continue; // o intrare defectă nu trebuie să omoare lotul
Book.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
finally
Book.Free;
end;
end;
O instanță nouă de registru de lucru per fișier costă microsecunde și elimină o întreagă categorie de erori de contaminare între fișiere: stilurile, numele definite și proprietățile documentului din fișierul 17 nu au nicio cale de a se scurge în fișierul 18. Săritul-și-continuarea la un Open eșuat își merită la fel de mult locul, pentru că un upload trunchiat într-un lot de 600 de fișiere ar trebui să vă coste o singură linie de jurnal, nu restul rulării. Merită semnalat și ce anume nu face în mod deliberat etapa CSV. SaveAsCSV scrie formulele ca text literal și nu le evaluează niciodată, așa că un lot de conversie ale cărui consumatori așteaptă numere calculate trebuie să ruleze mai întâi Calculate pe celulele relevante, sau să pornească de la registre de lucru care poartă deja rezultate din cache dintr-o calculare anterioară
Modelul de concurență: un registru de lucru per thread
Obiectele niciuneia dintre fațade nu sunt thread-safe, iar proiectarea nu a pretins niciodată altfel. Pentru că nu există stare globală partajată între instanțe, regula de scalare este pur și simplu un registru de lucru per thread de worker, fără partajarea unui registru de lucru între thread-uri. Un pool de N workeri, fiecare deținând propriul TXLSXWorkbook, scalează aproape liniar până când memoria devine plafonul, iar acel plafon este ceva pentru care puteți pune un număr: cel mai mare model de celule concurent înmulțit cu numărul de workeri, plus orice suprasarcină la momentul salvării pe care StreamingWrite a aplatizat-o. Când coada devine adâncă, aplicați contrapresiune la nivelul cozii de job-uri, nu în interiorul scriitorului. Un thread înfometat care a scris pe jumătate un registru de lucru nu a produs nimic util, în timp ce un job care a așteptat câteva secunde un worker liber se finalizează intact
Pentru imaginea mai largă a reglajelor, inclusiv formulele partajate, omiterea graficii pe partea de citire și pârghiile specifice XLS, consultați ghidul de performanță pentru registre de lucru mari. Job-urile de lot ale căror rânduri provin direct dintr-o interogare sunt acoperite separat în tiparele de export din baza de date pentru rapoarte Delphi
HotXLS se compilează în serviciul dvs. Delphi sau C++Builder ca Object Pascal nativ, fără dependențe externe; edițiile și licențierea se află pe pagina de produs HotXLS Delphi Component