Articol tehnic

Scrierea în flux cu HotXLS pentru procese pe server în Delphi

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

Memoria lotului Delphi în timp cu HotXLS: SaveAs implicit stivează un vârf tranșant de șir XML de worksheet peste platoul de umplere, în timp ce Book.StreamingWrite := True menține profilul plat pe parcursul salvării
Platoul de umplere este identic oricum, deoarece modelul de celule este construit oricum în memorie; StreamingWrite înlătură doar vârful de serializare de la momentul salvării

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:

Flux de callback HotXLS WriteRows în Delphi: un cursor de interogare dă un rând per apel callback-ului FillRow, care umple o matrice variant de valori sau ridică Skip și Cancel, iar worksheet-ul se umple rând cu rând
WriteRows predă bucla HotXLS, în timp ce callback-ul furnizează câte un rând variant-array per invocație, cu Skip drept renunțare per rând și Cancel drept oprire curată a întregii rulări
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

Model de concurență HotXLS pentru joburile de lot pe server Delphi: o coadă de joburi alimentează firuri de lucru care dețin fiecare o instanță privată TXLSXWorkbook, cu contrapresiune aplicată la coadă și memoria ca plafon de scalare
Instanțele de registru de lucru nu partajează nicio stare globală, astfel încât câte un registru per thread scalează până când modelele de celule concurente ating plafonul de memorie

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