Tekninen artikkeli

HotXLS-suoratoistokirjoitukset Delphin palvelimen eräajoissa

Oletetaan, että öinen Delphi-palvelu tuottaa yhden XLSX-tiedoston asiakasta kohden, muutamia satoja tiedostoja, joista osa on 400 000 riviä leveitä. Profiloi se, ja yllätys on harvoin solujen täyttösilmukka. Se on SaveAs-kutsu. Oletuskirjoittajalla jokainen laskentataulukko sarjallistetaan yhdeksi muistissa olevaksi XML-merkkijonoksi ennen kuin merkkijono pakataan OOXML-zipiin, ja leveässä taulukossa tilapäinen merkkijono voi olla suurempi kuin solumalli, josta se rakennettiin. Siksi työ, joka rakentaa tietonsa mukavasti 800 Mt:ssa, voi piikata yli 2 Gt:n säiliörajan tallennuksen aikana, ja OOM-tappaja jättää virheraportin klo 03.00, kun kukaan ei katso. HotXLS:ssä, losLabin Delphin ja C++Builderin natiivissa laskentataulukkokirjastossa, on ominaisuus, joka on suunnattu juuri tähän piikkiin: StreamingWrite. Sen ympärillä on kaksi muuta vipua, jotka määräävät, pysyykö erätyöntekijä muisti- ja aikabudjetissaan: rivitason kirjoitustakaisinkutsut ja tyylipoolin käyttäytyminen tiukassa silmukassa

Mitä oletustallennuspolku puskuroi ja mitä StreamingWrite muuttaa

Oletusarvoinen XLSX-kirjoittaja suosii yksinkertaisuutta. Se renderöi laskentataulukon XML:n kokonaan ja antaa sitten valmiin merkkijonon zip-pakkaajalle. Se on oikea valinta valtaosalle työkirjoista, joissa koko taulukon XML mahtuu muutamaan megatavuun. Se lakkaa olemasta oikea, kun yhden taulukon sarjallistettu muoto kasvaa satoihin megatavuihin. Laskentataulukon XML on monisanaista: jokainen numeerinen solu maksaa kymmeniä merkkejä merkintäkieltä, ja koko sen sisältävän merkkijonon on oltava yhtenäinen. Muistikaaviossa tunnusmerkkiä on vaikea olla huomaamatta. Pitkä tasainen tasanne rivien täyttyessä, sitten terävä kolmiomainen piikki SaveAs-kutsun aikana ja lopuksi romahdus, kun zip on huuhdeltu

Asettamalla Book.StreamingWrite := True vaihdat SaveAs-metodin laskentataulukkokirjoittajaan, joka lähettää taulukon XML:n suoraan zip-tietovirtaan sitä tuotettaessa. Välimerkkijonoa ei koskaan varata, ja kolmiomainen piikki tasoittuu kohinaksi

Ole tarkka siitä, mitä tämä todellisuudessa tuo, sillä sen liioittelu johtaa vääriin kapasiteettisuunnitelmiin. Lippu muuttaa vain tallennuspolkua. Työkirjan rakentaminen varaa edelleen koko muistissa olevan solumallin, joten täyttövaiheen tasanne on täsmälleen yhtä korkea kuin ennen. Se, mikä katoaa, on sarjallistuspiikki, joka aiemmin kasautui tallennushetkellä tasanteen päälle, ja 400 000 riviä täyttävälle työlle tämä piikki on rutiininomaisesti koko ero muistibudjettiin mahtumisen ja sen ylittämisen välillä. Ominaisuuden oletusarvo on False historiallisen käyttäytymisen säilyttämiseksi, joten käyttöönotto on yksi nimenomainen rivi, jonka kirjoitat tarkoituksella

Delphi-erän muisti ajan yli HotXLS:llä: oletus SaveAs pinoaa ohimenevän laskentataulukon XML-merkkijonopiikin täyttötasanteen päälle, kun taas Book.StreamingWrite := True pitää profiilin tasaisena tallennuksen läpi
Täyttötaso on identtinen kummalla tavalla, koska solumalli rakennetaan yhä muistiin; StreamingWrite poistaa vain tallennushetken sarjoituspiikin

Massavienti lippu käytössä

Book := TXLSXWorkbook.Create;
try
  BoldIdx := Book.Fonts.Add('Calibri', 11, True, False); // pool-indeksi, 0-pohjainen
  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;        // 1-pohjainen solussa
  end;
  Book.StreamingWrite := True;   // virtaa arkin XML suoraan zipiin
  Book.SaveAs('bulk.xlsx');
finally
  Book.Free;
end;

Cells[R, C] luo soluja tarvittaessa, mikä pitää silmukan rungon siistinä. Kaksi ruudukon rajaa kannattaa painaa muistiin: 1 048 576 riviä ja 16 384 saraketta, jotka ovat saatavilla nimillä XlsxMaxRow ja XlsxMaxCol. Rajoituksen ylittävä tietosyöte on jaettava omassa koodissasi useille taulukoille. Mikään jatkovaihe ei havaitse ylitystä tai korjaa sitä puolestasi, ja tiedosto päätyy yksinkertaisesti katkaistuksi rajalla

Rivien täyttäminen ilman solukohtaista Variant-ylikuormitusta

Jokainen Cells[R, C].Value-asetus maksaa solun haun ja Variant-muunnoksen. Kymmenellätuhannella rivillä kukaan ei huomaa sitä. Miljoonalla rivillä, joissa jokaisessa on 20 saraketta, kutsukohtaisesta ylikuormituksesta tulee täyttövaiheen hallitseva kustannus, ja profiloija osoittaa suoraan siihen. Erärajapinnat antavat sinun luovuttaa kirjoittajalle kerrallaan kokonaisen rivin. WriteRows ohjaa takaisinkutsua, joka toimittaa yhden rivin kutakin kutsua kohden:

HotXLS WriteRows -callback-virta Delphissä: kyselykursori antaa yhden rivin kutsua kohden FillRow-callbackille, joka täyttää variantitaulukon arvoja tai nostaa Skipin ja Cancelin, ja laskentataulukko täyttyy rivi riviltä
WriteRows luovuttaa silmukan HotXLS:lle, kun taas callback toimittaa yhden varianttitaulukkorivin per kutsu, Skip rivi kohti poistumisvalintana ja Cancel siistinä koko-ajon pysäytyksenä
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;              // tietolähde tyhjennetty: lopeta siististi
    Exit;
  end;
  Values := VarArrayCreate([FirstCol, LastCol], varVariant);
  Values[FirstCol]     := FReader.RecordId;
  Values[FirstCol + 1] := FReader.CustomerName;
  Values[FirstCol + 2] := FReader.Amount;
end;

// täytä rivit 2..100001, sarakkeet A..C, vedä lukijasta
Sheet.WriteRows(2, 1, 100001, 3, FillRow);

Cancel-lippu muuttaa kiinteän rivialueen muotoon "enintään N riviä", mikä on luonteva muoto, kun rivimäärä tulee kyselystä, jonka suorittaminen ei ole vielä päättynyt. Skip on kevyempi kosketus: se jättää yksittäisen rivin tyhjäksi pysäyttämättä ajoa. Solujen täyttämisen lisäksi takaisinkutsu osoittautuu hyväksi paikaksi toiminnallisille huolille, jotka muutoin ruuvattaisiin täyttösilmukkaan kömpelösti. Joka tuhannella rivillä päivittyvä edistymislaskuri, työajastimesta kyseltävä peruutustunnus, lähdetietokannan lukuja rajoittava nopeusrajoitin: kaikki se elää yhdessä paikassa sen sijaan, että se pujotettaisiin solukirjoituskoodin läpi. Lukupuolella ForEachRow ja ForEachCell peilaavat samaa mallia, mikä on tärkeää, kun erätyö sekä kuluttaa että tuottaa suuria tiedostoja

Tyylipoolit palkitsevat nostamisen silmukan ulkopuolelle

XLSX-tyylimalli on joukko jaettuja pooleja. Fonts.Add, Fills.AddSolid ja Borders.Add palauttavat kaikki 0-pohjaisen pooli-indeksin, ja solu viittaa fonttiin tallentamalla indeksin plus yksi FontIndex-ominaisuuteen, jossa nolla on varattu työkirjan oletukselle. +1 näkyy suoraan yllä olevassa massaesimerkissä. Unohda se, ja solu saa hiljaisesti väärän tyylin, koska yhden verran pielessä oleva tyylipoolin indeksi on edelleen kelvollinen indeksi eikä mikään aiheuta virhettä

Seuraava kurinalaisuus on luoda jokainen tyyliobjekti ennen rivisilmukkaa ja viitata sen indeksiin silmukan sisällä. Fonts.Add poistaa kaksoiskappaleet samoista määritelmistä, joten sen kutsuminen kerran riviä kohden vain tuhlaa suoritinaikaa. Alignments.Add on ansa, koska se palauttaa tuoreen merkinnän jokaisella kutsulla. 100 000 rivin silmukassa tämä hautaa styles.xml-tiedoston sadantuhannen päällekkäisen tasaustietueen alle, mikä paisuttaa levyllä olevaa tiedostoa ja hidastaa jokaista myöhempää avausta Excelissä, kun kaksoiskappaleet jäsennetään uudelleen. Rakenna jokainen tyyli kerran silmukan ulkopuolella ja viittaa sen indeksiin niin monta kertaa kuin tarvitset

Tietovirrat, väliaikaishakemistot ja niitä ympäröivä eräsilmukka

Mikään tästä ei vaadi tiedostojärjestelmää. Molemmissa rajapinnoissa on TStream-ylikuormitukset koko IO-pinnallaan, muun muassa metodeissa Open, SaveAs, SaveAsCSV, SaveAsHTML ja SaveAsODS, joten erätyöntekijä voi renderöidä suoraan blob-tallennukseen tai HTTP-vastaukseen sidottuun TMemoryStream-tietovirtaan koskematta koskaan levyyn. Muistettavana on yksi terävä reuna. SaveAs(Stream) kirjoittaa tietovirran nykyisestä sijainnista eikä kelaa sitä jälkikäteen alkuun, joten aseta itse Position := 0, ennen kuin luovutat tietovirran sen toimittavalle taholle, tai kuluttaja lukee nolla tavua. XLS-rajapinta lisää kaksi omaa säädintä. SetTempDir ohjaa BIFF-kirjoittajan väliaikaistiedostot asemalle, jolla on tilaa ja IO-kapasiteettia niiden vastaanottamiseen; tämä on tärkeää palvelimilla, joissa oletusväliaikapolku sijaitsee ahtaalla järjestelmälevyllä. UseSharedFormulas yhdistää toistuvat kaavatekstit jaetuiksi ryhmiksi, mikä pienentää aidosti tiedostokokoa klassisessa raporttimuodossa, jossa yksi kaava kopioidaan koko sarakkeeseen

Eräsilmukka itsessään pysyy tarkoituksella tylsänä:

for FileName in SourceFiles do
begin
  Book := TXLSXWorkbook.Create;        // uusi instanssi: ei tilavuotoa
  try
    Book.StreamingWrite := True;
    if Book.Open(FileName) <> 1 then
      Continue;                        // yksi huono syöte ei saa tappaa erää
    Book.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
  finally
    Book.Free;
  end;
end;

Tuore työkirjaesiintymä tiedostoa kohden maksaa mikrosekunteja ja poistaa kokonaisen luokan tiedostojen välisiä saastumisvirheitä: tiedoston 17 tyyleillä, määritellyillä nimillä ja asiakirjaominaisuuksilla ei ole reittiä vuotaa tiedostoon 18. Epäonnistuneen Open-kutsun ohitus ja jatkaminen ansaitsee paikkansa aivan yhtä paljon, sillä yksi katkennut lataus 600 tiedoston erässä saa maksaa yhden lokirivin eikä loppua ajoa. On myös syytä korostaa, mitä CSV-osuus tarkoituksella ei tee. SaveAsCSV kirjoittaa kaavat kirjaimellisena tekstinä eikä koskaan laske niitä, joten muunnoserän, jonka kuluttajat odottavat laskettuja lukuja, on ajettava Calculate asianomaisille soluille ensin tai aloitettava työkirjoista, joissa on jo aiemman laskennan välimuistitulokset

Samanaikaisuusmalli: yksi työkirja säiettä kohden

Kummankaan rajapinnan objektit eivät ole säieturvallisia, eikä suunnittelu koskaan väittänyt muuta. Koska esiintymien välillä ei ole jaettua globaalia tilaa, skaalaussääntö on yksinkertaisesti yksi työkirja työntekijäsäiettä kohden ilman työkirjan jakamista säikeiden välillä. N työntekijän ryhmä, jossa kukin omistaa oman TXLSXWorkbook-esiintymänsä, skaalautuu lähes lineaarisesti, kunnes muistista tulee katto, ja sille katolle voi antaa luvun: suurin samanaikainen solumalli kerrottuna työntekijöiden määrällä sekä mikä tahansa StreamingWrite-ominaisuuden tasaama tallennusaikainen ylikuormitus. Kun jono syvenee, sovella vastapainetta työjonossa kirjoittajan sisäpuolen sijaan. Nälkiintynyt säie, joka on puoliksi kirjoittanut työkirjan, ei ole tuottanut mitään hyödyllistä, kun taas muutaman sekunnin vapaata työntekijää odottanut työ valmistuu ehjänä

HotXLS-rinnakkaismalli Delphi-palvelimen erätöille: työjono ruokkii työsäikeitä, joista kukin omistaa yksityisen TXLSXWorkbook-instanssin, ja takapaine kohdistetaan jonossa ja muisti skaalautumisen kattona
Työkirjainstanssit eivät jaa globaalia tilaa, joten yksi työkirja per säie skaalautuu, kunnes samanaikaiset solumallit saavuttavat muistikaton

Laajempaa virityskuvaa varten, mukaan lukien jaetut kaavat, grafiikan ohitus lukupuolella ja XLS-kohtaiset säätimet, katso suurten työkirjojen suorituskykyopas. Erätyöt, joiden rivit tulevat suoraan kyselystä, käsitellään erikseen Delphi-raporttien tietokantavientimalleissa

HotXLS kääntyy Delphi- tai C++Builder-palveluusi natiivina Object Pascalina ilman ulkoisia riippuvuuksia; versiot ja lisensointi ovat HotXLS Delphi Component -tuotesivulla