Articolo tecnico

Prestazioni su cartelle Excel grandi in Delphi con HotXLS

Quando un export da 300.000 righe sfonda il proprio budget di memoria, di solito la colpa ricade sul numero di righe. Il numero di righe di solito è innocente. Le parti costose di una cartella di lavoro grande sono quelle create come effetto collaterale: un pool di stili che cresce di una voce per cella perché la formattazione è stata aggiunta dentro il ciclo, l'XML del foglio assemblato come una sola stringa gigante al momento del salvataggio, un milione di corpi di formula identici memorizzati uno a uno. HotXLS, la libreria Delphi nativa di losLab per i file XLS e XLSX, vi dà una leva specifica per ciascuno di questi costi. Nessuna è attiva per impostazione predefinita, perché ognuna cambia un compromesso, quindi sapere quale leva corrisponde a quale sintomo è la vera abilità sulle prestazioni

Dove una cartella di lavoro grande consuma memoria

Ci sono due regimi di memoria distinti su cui ragionare. Durante la generazione, il modello di celle in memoria cresce con ogni cella che toccate: valori, formati e formule diventano tutti oggetti o voci di pool. Durante il salvataggio, il percorso XLSX predefinito rende in più l'XML di ogni foglio in una stringa larga prima di comprimerlo nel contenitore zip, quindi il picco di utilizzo è il modello più la forma serializzata del foglio più grande. Un lavoro che sopravvive al ciclo di costruzione e poi muore dentro SaveAs sta colpendo il secondo regime, non il primo, e la correzione per l'uno non fa nulla per l'altro

I due regimi di memoria in un lavoro Delphi HotXLS su una cartella di lavoro grande: il modello di celle in memoria costruito dal ciclo di generazione, più la stringa XML serializzata del foglio più grande durante un salvataggio predefinito, che StreamingWrite elimina
Il ciclo di costruzione e la chiamata di salvataggio falliscono in due regimi di memoria diversi, quindi StreamingWrite appiattisce solo il picco al salvataggio mentre la memoria del percorso di costruzione richiede le leve del pool di stili e delle callback

La dimensione del file segue una regola collegata: le celle sono solo uno dei contributi, accanto a stili, stringhe condivise, formule, immagini e commenti. Una passata di verifica con ForEachCell e i conteggi delle collezioni per foglio vi dice quale risorsa domini davvero in un file problematico, prima che ottimizziate quella sbagliata. Una sottigliezza di misura: Sheet.Cells.Count sul lato XLSX riporta il numero di celle istanziate nell'archivio sparso, non l'area dell'intervallo usato. Un foglio i cui dati occupano un rettangolo di 1000 per 50 con metà delle celle vuote conta circa 25.000, non 50.000. Quella distinzione conta quando confrontate il file "enorme" di un cliente con i vostri riferimenti, perché area dell'intervallo usato e popolazione reale delle celle possono differire di un ordine di grandezza nei layout finanziari sparsi

StreamingWrite corregge il percorso di salvataggio, non quello di costruzione

Impostare TXLSXWorkbook.StreamingWrite := True commuta SaveAs su un serializzatore a flusso che scrive l'XML dei fogli direttamente nel flusso zip, eliminando la stringa intermedia per foglio. Vale False per impostazione predefinita per compatibilità di comportamento, e attivarlo è una modifica di una riga:

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;   // l'XML del foglio scorre nel contenitore zip
  Book.SaveAs('bulk.xlsx');
finally
  Book.Free;
end;

Siate precisi su cosa vi compra questo: il modello di celle costruito dal ciclo occupa esattamente la stessa memoria di prima. StreamingWrite appiattisce il picco al momento del salvataggio, che è la differenza fra un lavoro batch che arriva in fondo e uno che fallisce al 95%. Se è il ciclo di costruzione stesso a esaurire la memoria, le leve che vi servono sono le due seguenti

Pool di stili: aggiungete una volta, riusate l'indice

La formattazione XLSX in HotXLS è basata su pool: Book.Fonts.Add(...), Fills.AddSolid(...) e Borders.Add(...) restituiscono un indice di pool in base 0 che le celle richiamano. Chiamare Fonts.Add con parametri identici dentro un ciclo viene deduplicato, quindi spreca tempo anziché spazio. Alignments.Add si comporta diversamente: restituisce un oggetto nuovo a ogni chiamata, quindi creare un allineamento per cella fa crescere il pool linearmente con il numero di righe. Una sola abitudine copre entrambi i casi. Risolvete ogni indice di pool una volta, fuori dal ciclo, e assegnate gli indici dentro

Confronto sull'uso del pool di stili di HotXLS in Delphi: un oggetto Alignments.Add nuovo creato una volta per riga fa crescere il pool linearmente, mentre un indice Fonts.Add risolto una sola volta sopra il ciclo viene riusato da ogni cella con lo scostamento di uno sull'indice in base 0
Risolvete una sola volta fuori dal ciclo ogni indice di font, riempimento, bordo e allineamento, poi assegnate dentro quell'indice di pool in base 0 aumentato di uno
// portate le ricerche nel pool fuori dal ciclo caldo
HeaderFont := Book.Fonts.Add('Calibri', 11, True, False);   // indice del pool in base 0
for C := 1 to 24 do
  Sheet.Cells[1, C].FontIndex := HeaderFont + 1;            // le celle memorizzano in base 1; 0 = predefinito

Il + 1 non è un refuso e dimenticarlo è il classico difetto generatore di sintomi qui: i pool distribuiscono indici in base 0, mentre le proprietà lato cella trattano lo 0 come "predefinito", quindi ogni indice di pool va aumentato di uno in assegnazione. Sbagliate per omissione e le vostre intestazioni si rendono in silenzio con il font predefinito della cartella di lavoro, un difetto che nessuno nota fino alla revisione del marchio

Sostituite il traffico di Variant per cella con callback di riga

Ogni Sheet.Cells[R, C].Value := X comporta una ricerca o creazione di cella più un'assegnazione di Variant. A qualche centinaio di migliaia di celle, quel sovraccarico per accesso diventa misurabile nei profili. HotXLS fornisce API di callback in blocco su entrambe le facciate (ForEachCell e ForEachRow in lettura, WriteCells e WriteRows in scrittura) che spostano l'iterazione dentro il motore e consegnano al vostro codice righe intere per volta:

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;     // interrompe l'intera scrittura
    Exit;
  end;
  Values := VarArrayOf([FRows[Row - 1].Account,
                        FRows[Row - 1].PostedOn,
                        FRows[Row - 1].Amount]);
end;

// una sola chiamata al motore invece di centinaia di migliaia di accessi a proprietà
Sheet.WriteRows(1, 1, FCount, 3, FillRow);

Il flag Skip della callback lascia una riga intatta senza interrompere, e Cancel chiude l'operazione in anticipo, cosa utile quando la sorgente è un lettore la cui lunghezza scoprite strada facendo. Abbinate WriteRows per la costruzione a StreamingWrite per il salvataggio e il percorso di generazione non ha più alcun punto caldo per cella

Leve di lettura sulla facciata XLS

I file .xls legacy di grandi dimensioni hanno il loro corredo di strumenti. _DisableGraphics := True prima di Open salta del tutto l'analisi del livello di disegno, il che accelera il caricamento di cartelle di lavoro che portano anni di forme accumulate e immagini incorporate. La restrizione è netta: il livello di disegno è allora assente dal modello, quindi salvare una simile cartella di lavoro scrive un file senza i suoi disegni. Riservate questo flag ai lavori di analisi in sola lettura. SetTempDir reindirizza i file temporanei del writer BIFF, cosa che conta sui server dove la posizione temporanea predefinita ha una quota o sta su archiviazione lenta. UseSharedFormulas raggruppa i corpi di formula ripetuti in record di formula condivisa, riducendo i file in cui una colonna di formule si ripete per sessantamila righe

I cicli di lettura sui dati XLS hanno una trappola di indicizzazione che vale la pena segnalare, perché raddoppia il lavoro quando la si gestisce in modo difensivo e corrompe i risultati quando la si manca: UsedRange riporta i propri confini FirstRow, LastRow, FirstCol e LastCol in base 0, mentre Cells.Item[Row, Col] è in base 1. Una scansione che percorre l'intervallo usato deve aggiungere uno a ciascuna coordinata all'accesso della cella, come in Cells.Item[Row + 1, Col + 1], altrimenti legge una griglia spostata in diagonale di una cella, perdendo in silenzio l'ultima riga e l'ultima colonna e includendone una prima fantasma. La callback ForEachCell aggira del tutto la discrepanza, il che è un motivo in più per preferirla nelle scansioni di interi fogli

Sondate i file prima di caricarli

L'operazione più economica su una cartella di lavoro grande è quella che evitate. GetSheetNames su entrambe le facciate elenca i fogli di un file senza caricare i dati delle celle. L'implementazione XLSX legge solo il manifesto della cartella di lavoro dentro lo zip e lascia esplicitamente non popolata l'istanza della cartella, e la facciata XLS smette di scansionare al primo confine di sottoflusso. Questo ne fa il controllo preliminare giusto per la domanda "quale foglio deve puntare questo lavoro di importazione", mentre CanReadEncrypted risponde alla domanda "questo contenitore è cifrato" prima di un tentativo di Open destinato a fallire

Flusso preliminare per un file Excel sconosciuto in Delphi con HotXLS: GetSheetNames elenca i fogli senza caricare i dati delle celle, un codice di ritorno pari o inferiore a zero svuota l'elenco e segnala il fallimento, CanReadEncrypted segnala i contenitori cifrati prima di un Open destinato a fallire, e solo allora parte il caricamento completo
GetSheetNames e CanReadEncrypted rispondono su quale foglio puntare e se il contenitore sia leggibile prima che venga analizzato qualsiasi dato di cella
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
  if Book.GetSheetNames('big-unknown.xlsx', Names) <= 0 then
    raise Exception.Create('cannot enumerate sheets');   // il fallimento svuota l'elenco
  // scegliete il foglio di destinazione, poi decidete se un Open completo valga la pena
finally
  Book.Free;
  Names.Free;
end;

Notate la convenzione sui codici di ritorno: queste funzioni di sondaggio segnalano il fallimento con valori pari o inferiori a zero e svuotano l'elenco di output, quindi verificate <= 0 invece di confrontare con un singolo valore di successo

Dimensionare l'approccio al lavoro

Per le pipeline non presidiate che generano molti file grandi in sequenza, altre due abitudini completano il quadro. Gli oggetti cartella di lavoro non sono sicuri per la condivisione fra thread, ma nulla impedisce una cartella di lavoro indipendente per thread di lavoro, il che parallelizza in modo pulito la conversione batch. E quando l'output va su HTTP anziché su disco, gli overload di salvataggio su TStream si combinano con StreamingWrite così che una risposta grande non si materializzi mai come file temporaneo. Si applica una nota operativa: il salvataggio su flusso scrive dalla posizione corrente senza riavvolgere, quindi impostate Position := 0 prima di consegnare il flusso al framework della risposta. L'articolo sulla scrittura a flusso e sui lavori batch sviluppa quello schema lato server, e l'articolo sull'esportazione da database mostra dove queste leve si inseriscono in un report guidato da un dataset

Infine, tenete un file di riferimento nel caso peggiore per ogni famiglia di report e cronometratelo in CI. Le regressioni di prestazioni nella generazione di documenti raramente si annunciano. Uno stile aggiunto dentro un ciclo o un sondaggio sostituito da un Open completo non cambia nulla dal punto di vista funzionale, e il batch notturno semplicemente impiega quaranta minuti in più. Un test cronometrato su un riferimento rappresentativo da mezzo milione di celle trasforma quella deriva in una build rossa invece che in un incidente operativo

Le build di valutazione, i progetti dimostrativi con un esempio di generazione in blocco e il riferimento API completo sono disponibili sulla pagina HotXLS Delphi Component