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