Technisch artikel

HotXLS Delphi Component: large workbook performance in Delphi

Wanneer een export van 300.000 rijen zijn geheugenbudget doorbreekt, krijgt het rijaantal meestal de schuld. Het rijaantal is meestal onschuldig. De dure onderdelen van een grote werkmap zijn degene die als bijeffect ontstaan: een stijlpool die met één item per cel groeit omdat opmaak binnen de lus werd toegevoegd, werkblad-XML die op opslagmoment als één gigantische string wordt opgebouwd, een miljoen identieke formulebodies één voor één opgeslagen. HotXLS, losLabs native Delphi-bibliotheek voor XLS- en XLSX-bestanden, geeft je een specifieke hendel voor elk van deze kosten. Geen enkele staat standaard aan, want elke verandert een afweging, dus weten welke hendel bij welk symptoom past is de eigenlijke prestatievaardigheid

Waar een grote werkmap geheugen besteedt

Er zijn twee afzonderlijke geheugenregimes om over na te denken. Tijdens generatie groeit het in-memory celmodel met elke cel die je aanraakt: waarden, formaten en formules worden allemaal objecten of poolitems. Tijdens opslaan rendert het standaard XLSX-pad daarnaast de XML van elk werkblad naar een brede string voordat het die comprimeert in de zip-container, dus het piekgebruik is het model plus de geserialiseerde vorm van het grootste blad. Een taak die de bouwlus overleeft en dan binnen SaveAs sterft, raakt het tweede regime, niet het eerste, en de fix voor het ene doet niets voor het andere

Twee geheugenregimes in een Delphi HotXLS-groot-werkboektaak: het in-memory celmodel gebouwd door de generatielus, plus de geserialiseerde XML-string van het grootste blad tijdens een standaardopslag, die StreamingWrite verwijdert
De bouwlus en de opslagaanroep falen in twee verschillende geheugenregimes, dus StreamingWrite platst alleen de piek bij het opslaan terwijl het geheugen van het bouwtraject de style-pool- en callback-hendels nodig heeft

Bestandsgrootte volgt een verwante regel: cellen zijn maar één bijdrager, naast stijlen, gedeelde strings, formules, afbeeldingen en opmerkingen. Een auditpas met ForEachCell en de per-blad-verzamelingsaantallen vertelt je welke resource een probleembestand daadwerkelijk domineert voordat je het verkeerde optimaliseert. Eén meetsubtiliteit: Sheet.Cells.Count aan de XLSX-kant meldt het aantal geïnstantieerde cellen in de sparse store, niet het oppervlak van het gebruikte bereik. Een blad waarvan de data een rechthoek van 1000 bij 50 beslaat met de helft van de cellen leeg, telt ruwweg 25.000, niet 50.000. Dat onderscheid is van belang wanneer je het "enorme" bestand van een klant vergelijkt met je eigen fixtures, want het oppervlak van het gebruikte bereik en de werkelijke celpopulatie kunnen een orde van grootte verschillen in schaars gevulde financiële lay-outs

StreamingWrite fixt het opslagpad, niet het bouwpad

TXLSXWorkbook.StreamingWrite := True instellen schakelt SaveAs over naar een streamende serializer die werkblad-XML rechtstreeks naar de zip-stream schrijft, waardoor de tussenliggende string per blad wegvalt. Standaard staat het op False voor gedragscompatibiliteit, en het aanzetten is een wijziging van één regel:

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;   // blad-XML streamt naar de zip-container
  Book.SaveAs('bulk.xlsx');
finally
  Book.Free;
end;

Wees precies over wat dit oplevert: het celmodel dat de lus bouwt, neemt precies evenveel geheugen in beslag als voorheen. StreamingWrite vlakt de piek op opslagmoment af, wat het verschil is tussen een batchtaak die voltooit en één die faalt bij de 95%-markering. Als de bouwlus zelf het geheugen uitput, zijn de volgende twee hendels degene die je nodig hebt

Stijlpools: eenmaal toevoegen, index hergebruiken

XLSX-opmaak in HotXLS is poolgebaseerd: Book.Fonts.Add(...), Fills.AddSolid(...) en Borders.Add(...) geven een 0-gebaseerde poolindex terug waar cellen naar verwijzen. Fonts.Add aanroepen met identieke parameters binnen een lus wordt gededupliceerd, dus het verspilt tijd in plaats van ruimte. Alignments.Add gedraagt zich anders: het geeft een vers object terug per aanroep, dus per-cel-uitlijning aanmaken laat de pool lineair meegroeien met het rijaantal. Eén gewoonte dekt beide gevallen. Los elke poolindex eenmaal op, buiten de lus, en wijs indices binnen de lus toe

HotXLS Delphi-stijlpoolgebruik vergeleken: een vers Alignments.Add-object per rij gecreëerd laat de pool lineair groeien, terwijl een boven de lus eenmaal geresolvde Fonts.Add-index door elke cel wordt hergebruikt met de 0-gebaseerde index verschoven met één
Los elke font-, vul-, rand- en uitlijningsindex één keer op buiten de lus, en wijs dan die 0-gebaseerde pool-index, verschoven met één, erbinnen toe
// til poollookups uit de hete lus
HeaderFont := Book.Fonts.Add('Calibri', 11, True, False);   // 0-gebaseerde poolindex
for C := 1 to 24 do
  Sheet.Cells[1, C].FontIndex := HeaderFont + 1;            // cellen slaan 1-gebaseerd op; 0 = standaard

De + 1 is geen typfout, en hem vergeten is hier de klassieke symptoomveroorzakende bug: de pools geven 0-gebaseerde indices uit, terwijl de celzijde-eigenschappen 0 behandelen als "standaard", dus elke poolindex moet bij toewijzing met één worden opgeschoven. Doe het per ongeluk fout en je koppen renderen stilletjes in het standaardlettertype van de werkmap, een defect dat niemand opmerkt tot de brandingreview

Vervang per-cel-Variant-verkeer door rijcallbacks

Elke Sheet.Cells[R, C].Value := X omvat een cel-lookup-of-aanmaak plus een Variant-toewijzing. Bij een paar honderdduizend cellen wordt die overhead per toegang meetbaar in profielen. HotXLS biedt bulk-callback-API's op beide facades (ForEachCell en ForEachRow voor lezen, WriteCells en WriteRows voor schrijven) die de iteratie binnen de engine verplaatsen en je code hele rijen tegelijk geven:

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 het hele schrijven
    Exit;
  end;
  Values := VarArrayOf([FRows[Row - 1].Account,
                        FRows[Row - 1].PostedOn,
                        FRows[Row - 1].Amount]);
end;

// één engine-aanroep in plaats van honderdduizenden eigenschapstoegangen
Sheet.WriteRows(1, 1, FCount, 3, FillRow);

De Skip-vlag van de callback laat een rij onaangeraakt zonder af te breken, en Cancel beëindigt de bewerking vroegtijdig, wat nuttig is wanneer de bron een reader is waarvan je de lengte gaandeweg ontdekt. Combineer WriteRows voor de bouw met StreamingWrite voor het opslaan en het generatiepad heeft geen overgebleven per-cel-hotspot meer

Leeshendels aan de XLS-facade

Grote legacy .xls-bestanden hebben hun eigen toolkit. _DisableGraphics := True vóór Open slaat het parsen van de tekenlaag helemaal over, wat het laden van werkmappen versnelt die jaren aan opgehoopte vormen en ingesloten afbeeldingen dragen. De beperking is hard: de tekenlaag ontbreekt dan in het model, dus opslaan van zo'n werkmap schrijft een bestand zonder zijn tekeningen. Bewaar deze vlag voor alleen-lezen analysetaken. SetTempDir herleidt de tijdelijke bestanden van de BIFF-schrijver, wat van belang is op servers waar de standaard tijdelijke locatie een quotum heeft of op trage opslag staat. UseSharedFormulas groepeert herhaalde formulebodies in shared-formula-records, wat bestanden verkleint waar een formulekolom zich zestigduizend rijen lang herhaalt

Leeslussen over XLS-data hebben een indexeringsvalkuil die het waard is te markeren, want die verdubbelt het werk wanneer defensief afgehandeld en verknoeit resultaten wanneer gemist: UsedRange meldt zijn FirstRow-, LastRow-, FirstCol- en LastCol-grenzen 0-gebaseerd, terwijl Cells.Item[Row, Col] 1-gebaseerd is. Een scan die het gebruikte bereik doorloopt, moet bij de celtoegang één optellen bij elke coördinaat, zoals in Cells.Item[Row + 1, Col + 1], anders leest hij een raster dat diagonaal één cel is verschoven, waarbij stilletjes de laatste rij en kolom wegvallen en een fantoom eerste wordt meegenomen. De ForEachCell-callback omzeilt de mismatch volledig, wat nog een reden is om hem te verkiezen voor scans van hele bladen

Bestanden onderzoeken voordat je ze laadt

De goedkoopste grote-werkmapbewerking is degene die je vermijdt. GetSheetNames op beide facades somt de werkbladen van een bestand op zonder celdata te laden. De XLSX-implementatie leest alleen het workbook-manifest binnen de zip en laat de workbook-instantie expliciet ongevuld, en de XLS-facade stopt met scannen bij de eerste substream-grens. Dat maakt het de juiste pre-flight-controle voor "op welk blad moet deze importtaak zich richten", en CanReadEncrypted beantwoordt "is dit een versleutelde container" vóór een gedoemde Open-poging

Pre-flightstroom voor een onbekend Excel-bestand in Delphi met HotXLS: GetSheetNames somt werkbladen op zonder celdata te laden, een retourcode van nul of lager maakt de lijst leeg en signaleert mislukking, CanReadEncrypted vlagt versleutelde containers vóór een gedoemde Open, en pas daarna draait de volledige lading
GetSheetNames en CanReadEncrypted beantwoorden welk werkblad het doel is en of de container leesbaar is voordat er ook maar één celgegeven wordt geparseerd
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
  if Book.GetSheetNames('big-unknown.xlsx', Names) <= 0 then
    raise Exception.Create('cannot enumerate sheets');   // bij mislukking wordt de lijst geleegd
  // kies het doelblad, en bepaal dan of een volledige Open de moeite waard is
finally
  Book.Free;
  Names.Free;
end;

Let op de returncode-conventie: deze onderzoeksfuncties signaleren mislukking met waarden op of onder nul en legen de uitvoerlijst, dus test op <= 0 in plaats van te vergelijken met één specifieke succeswaarde

De aanpak afstemmen op de taak

Voor onbewaakte pipelines die achtereenvolgens veel grote bestanden genereren, maken twee gewoonten meer het plaatje compleet. Workbook-objecten zijn niet thread-safe voor delen, maar niets houdt één onafhankelijke werkmap per workerthread tegen, wat batchconversie netjes parallelliseert. En wanneer uitvoer naar HTTP gaat in plaats van naar schijf, combineren de TStream-opslag-overloads met StreamingWrite zodat een grote respons nooit als tijdelijk bestand materialiseert. Eén operationele voetnoot geldt: het opslaan naar stream schrijft vanaf de huidige positie zonder terug te spoelen, dus zet Position := 0 voordat je de stream aan het responseframework overhandigt. Het artikel over streaming write en batchtaken werkt dat server-side patroon verder uit, en het artikel over database-export laat zien waar deze hendels passen in een dataset-gestuurd rapport

Bewaar ten slotte één worst-case-fixture per rapportfamilie en meet de tijd ervan in CI. Prestatieregressies in documentgeneratie kondigen zichzelf zelden aan. Een stijl toegevoegd binnen een lus of een probe vervangen door een volledige Open verandert functioneel niets, en de nachtelijke batch duurt gewoon veertig minuten langer. Een getimede test op een representatieve fixture van een half miljoen cellen verandert die drift in een rode build in plaats van een operations-incident

Evaluatiebuilds, demoprojecten met een voorbeeld van bulkgeneratie, en de volledige API-referentie zijn beschikbaar op de HotXLS Delphi Component-pagina