Tekninen artikkeli

HotPDF-sivujen purkamisen suorituskyky Delphissä

Kolmen sivun kopioiminen 40-sivuisesta PDF-tiedostosta ei pitäisi kestää kahta minuuttia. Se ei ole suorituskyvyn viritysongelma. Se on merkki siitä, että käytetään väärää API-polkua. Kun näin tämän ajan ensimmäisen kerran HotPDF Component -sivun kopiointiesimerkissä, ensimmäinen vaistoni oli tarkistaa ensin asiakirjan rakenne ja sitten koodi. Sillä järjestyksellä oli lopulta merkitystä

Mikä todellisuudessa oli hidasta

Kyseessä oleva PDF-tiedosto oli 40-sivuinen viiteasiakirja, jolla oli ei-triviaalinen sivupuu: useita väliportaan /Pages -solmuja yhden tasaisen taulukon sijaan. Alkuperäinen esimerkkikoodi kutsui LoadFromFile-funktiota, loi uuden asiakirjan BeginDoc-funktiolla, kävi läpi valitut sivunumerot silmukassa, ja latasi kullakin iteraatiolla lähdeasiakirjan uudelleen levyltä noutaakseen yhden sivun. Tämä on täyden jäsennyksen (parse) hinta kerrottuna haluttujen sivujen määrällä. 12 MB tiedostoa luettiin levyltä kuusi kertaa kolmen sivun purkamiseksi, koska kukaan ei tarkistanut, pitääkö tiedostoa pitää auki iteraatioiden välillä

Toinen tekijä oli näkymätön koodissa: HotPDF:n LoadFromFile purkaa koko ristiinviittaustaulukon ja jokaisen objektivirran latauksen yhteydessä. Tämä on oikea toimintatapa asiakirjalle, jota aiotaan muokata, mutta se tekee enemmän työtä kuin on tarpeen, jos haluat vain sivumäärän ja osajoukon sivuja. Vain luku -käyttöön rakenteen tarkasteluun DAOpenFileReadOnly välttää koko objektipuun purkamisen deserialisoimatta sitä, mikä on tärkeää pakatuille tiedostoille, joissa on suuria kuvaresursseja

Kumpikaan näistä ei ole kirjaston virhe. Molemmissa tapauksissa kutsujat valitsevat APIn, joka on suunniteltu yhteen tehtävään, ja käyttävät sitä eri tehtävään

InsertPagesFromDocument -funktion käyttö sivujen purkamiseen

Oikea polku tietyn sivualueen kopioimiseksi yhdestä HotPDF-asiakirjasta toiseen on InsertPagesFromDocument, jota kutsutaan lähteen LoadFromFile-funktion jälkeen. Lataat lähteen kerran, lataat tai luot kohteen kerran, siirrät sivut ja tallennat. Lähde pysyy muistissa kaikkien sivulisäysten ajan:

procedure ExtractPages(const SourceFile, DestFile: string;
  const PageRange: string);
var
  Source, Dest: THotPDF;
begin
  Source := THotPDF.Create(nil);
  Dest   := THotPDF.Create(nil);
  try
    // Load source once: full parse happens here and only here
    Source.LoadFromFile(SourceFile);

    // Build a minimal destination document
    Dest.FileName := DestFile;
    Dest.BeginDoc;

    // Copy the requested range; '1-3' inserts pages 1 through 3
    // starting at position 1 in the destination
    Dest.InsertPagesFromDocument(Source, PageRange, 1);

    Dest.EndDoc;
  finally
    Source.Free;
    Dest.Free;
  end;
end;

PageRange-parametri hyväksyy saman muodon kuin komentoriviesimerkki: pilkulla erotellun luettelon sivunumeroista tai -alueista, kuten '1-3' tai '1,5,7-9'. Sivunumerot alkavat luvusta 1. InsertPagesFromDocument kopioi sisältövirrat, resurssihakemistot ja sivun geometrian koskematta metatietoihin, kirjanmerkkeihin tai upotettuihin tiedostoliitteisiin, ellei kopioitavilta sivuilta viitata niihin. Kolmen sivun purkamisessa 40-sivuisesta asiakirjasta työstöjoukko on pieni

Saman 12 MB:n tiedoston, jonka käsittely kesti aiemmin kaksi minuuttia, käsittelyaika tällä mallilla on alle 1,5 sekuntia. Suurin osa tästä ajasta kuluu yhteen LoadFromFile-kutsuun. Asiakirjan rakenteella ei ole merkitystä sen jälkeen, kun objektitaulukko on purettu ensimmäisellä kerralla

Kun LoadFromFile tekee liikaa: Direct File API

Jos tarvitset vain sivumäärän, asiakirjan tietojen tarkastelua tai tiedoston kopiointia sen sisältöön koskematta, Direct File API välttää täyden jäsennyksen kokonaan. DAOpenFileReadOnly kartoittaa ristiinviittaustaulukon purkamatta objektivirtoja, joten sivumäärän haku on O(xref-koko) O(tiedostokoon) sijaan:

procedure InspectPDF(const FileName: string);
var
  Pdf: THotPDF;
  Handle, PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Handle := Pdf.DAOpenFileReadOnly(FileName, '');
    if Handle <= 0 then
      Exit;
    try
      PageCount := Pdf.DAGetPageCount(Handle);
      Writeln('Pages: ', PageCount);

      // DACopyFile is a byte-preserving copy, no re-serialization
      Pdf.DACopyFile(FileName, 'archive-copy.pdf');
    finally
      Pdf.DACloseFile(Handle);
    end;
  finally
    Pdf.Free;
  end;
end;

Huomioitavaa: DAOpenFileReadOnly hyväksyy salasanaparametrin, mutta turvautuu täyteen jäsennykseen salatuilla syötteillä, koska purku edellyttää objektipuun läpikäymistä salausdictionaryn selvittämiseksi. Jos lähdetiedostot on salattu, pura ne ensin DecryptFile-funktiolla saadaksesi salaamattoman kopion, ja avaa se sitten Direct File API -rajapinnalla. Tiedostotason DecryptFile-funktio käyttää suoraa AES-256-uudelleenkirjoituspolkua standardisalaukseen ja on nopeampi kuin LoadFromFile, jota seuraa SaveLoadedDocument, kun kyse on suurista tiedostoista, koska se ei rakenna koko objektimallia muistiin

Muistin hallinta suurissa eräajoissa

Eräajoilla, jotka käsittelevät kymmeniä tiedostoja silmukassa, on malli, joka näyttää oikealta mutta kerryttää muistia: luodaan THotPDF silmukan sisällä, kutsutaan LoadFromFile, tehdään työtä, ja kutsutaan Free. Se on rakenteellisesti hienosti tehty. Ongelma syntyy, kun sisempi työ varaa väliaikaisia objekteja, nappaa poikkeuksia (exceptions) ja jättää nämä väliaikaiset objektit varaamaan muistia virhepoluilla. Delphin muistinhallinta ei tiivistä muistia, joten sata virhepolkuvuotoa eräajossa voi nostaa muistinkäytön niin korkealle, että kaikkien muiden varausten tekeminen hidastuu

Korjaus ei ole eksoottinen. Jokainen THotPDF ja jokainen väliaikainen TStream tai TBitmap, joka osallistuu PDF-työhön, kuuluu try/finally-lohkoon, jossa Free on viimeinen lauseke. Aseta paikalliset osoittimet arvoon nil ennen try-lohkoa, jotta finally-haara voi turvallisesti käyttää if Assigned(x) then x.Free -rakennetta, kun alustus epäonnistuu kesken kaiken. Tämä on standardia Delphin omistajuuskuria, ja se ratkaisee tämän tyyppiset ongelmat täydellisesti

Yksi asia vielä tarkistettavaksi eräajokonteksteissa: AddImage rekisteröi kuvat sisäiseen luetteloon, joka säilyy THotPDF-instanssin eliniän ajan. Jos käytät yhtä instanssia uudelleen monissa asiakirjoissa kutsumalla LoadFromFile toistuvasti, aiemmista asiakirjoista rekisteröidyt kuvat jäävät luetteloon. Luo joko uusi instanssi jokaista asiakirjaa kohden tai kutsu kuvaluettelon tyhjennyspolkua asiakirjojen välillä

Mittaus ennen muutosten tekemistä

Mittaa ennen kuin turvaudut mihinkään näistä malleista. Delphin System.Diagnostics -yksikön TStopwatch käärii QueryPerformanceCounter-toiminnon sisäänsä ja on riittävän tarkka tiedoston I/O:n profilointiin. Kääri vain LoadFromFile ja katso, kuinka suuren osan ajasta se vie. Jos se vie 90 % kokonaisajasta, korjaus on Direct File API tai saman tiedoston jäsennyskertojen vähentäminen. Jos se on alle 20 %, pullonkaula on jossain muualla ja jahtaat väärää asiaa

Tämän julkaisun aloittanut kaksiminuuttinen purkaminen johtui täysin toistuvasta latausmallista. Asiakirjan rakenteella ei ollut osuutta asiaan; tasainen sivupuu olisi toiminut samalla tavalla. Siirtyminen yhteen LoadFromFile-kutsuun, jota seuraa yksi InsertPagesFromDocument-kutsu, pudotti ajan 1,3 sekuntiin samalla laitteistolla koskematta mihinkään muuhun

Tässä näytetty sivujen manipulointi-API on osa HotPDF Component -komponenttia Delphille ja C++Builderille