Két perc ahhoz, hogy három oldalt másoljunk ki egy 40 oldalas PDF-ből, nem teljesítményhangolási probléma. Ez annak a jele, hogy rossz API-útvonalat használnak. Amikor először láttam ezt az időzítést egy HotPDF Component oldalmásoló példán, az volt az első ösztönöm, hogy először a dokumentum szerkezetét, majd a kódot vizsgáljam meg. Kiderült, hogy ez a sorrend számít
Mi volt valójában lassú
A szóban forgó PDF egy 40 oldalas referenciadokumentum volt, nem triviális oldalfával: egyetlen lapos tömb helyett több köztes /Pages csomóponttal. Az eredeti példakód a LoadFromFile-t hívta meg, majd egy új dokumentumot épített a BeginDoc segítségével, végighaladt a kiválasztott oldalszámokon, és minden egyes iterációnál újra betöltötte a forrásdokumentumot a lemezről, hogy kihúzzon egy oldalt. Ez a teljes elemzési költség szorozva annyi oldallal, amennyit csak akarunk. Egy 12 MB-os fájl hatszor érte el a lemezt egy három oldalas kinyeréshez, mert senki sem vizsgálta meg, hogy a fájlnak nyitva kell-e maradnia az iterációk során
A második hozzájáruló tényező láthatatlan volt a kódban: a HotPDF LoadFromFile metódusa feloldja a teljes kereszthivatkozási táblát, és betöltéskor kitömörít minden objektumfolyamot. Ez a megfelelő viselkedés egy módosítani kívánt dokumentum esetében, de több munka, mint amennyire szükség van, ha csak az oldalszámra és az oldalak egy részhalmazára van szükségünk. A struktúrához való csak olvasható hozzáférés esetén a DAOpenFileReadOnly elkerüli a teljes objektumfa deszerializálását, ami a nagy képi erőforrásokat tartalmazó tömörített fájloknál számít
Ezek egyike sem könyvtári hiba. Mindkettő esetében a hívók egy adott feladatra tervezett API-t választanak, és egy másikra használják azt
Az InsertPagesFromDocument használata az oldal kinyeréséhez
A megfelelő útvonal ahhoz, hogy oldalak egy tartományát másoljuk egyik HotPDF dokumentumból a másikba, az InsertPagesFromDocument, amelyet a forráson történő LoadFromFile után hívunk meg. Egyszer tölti be a forrást, egyszer tölti be vagy hozza létre a célt, áthelyezi az oldalakat és elmenti. A forrás a memóriában marad az összes oldal beillesztése során:
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;
A PageRange paraméter ugyanazt a formátumot fogadja el, mint a parancssori példa: az oldalszámok vagy tartományok vesszővel elválasztott listáját, mint például '1-3' vagy '1,5,7-9'. Az oldalak 1-alapúak. Az InsertPagesFromDocument átmásolja a tartalomfolyamokat, az erőforrás-szótárakat és az oldalgeometriát anélkül, hogy érintené a metaadatokat, a könyvjelzőket vagy a beágyazott fájlmellékleteket, hacsak nem hivatkoznak rájuk a másolt oldalakról. Egy 40 oldalas dokumentumból történő három oldalas kinyerés esetén ez egy kis munkakészlet
Ugyanannak a 12 MB-os fájlnak az időzítése, amely korábban két percig futott: kevesebb mint 1,5 másodperc ezzel a mintával. Ennek az időnek a nagy része az egyetlen LoadFromFile hívás. A dokumentumszerkezet lényegtelen, miután az objektumtáblát először feloldották
Amikor a LoadFromFile túl sok: a Direct File API
Ha csak az oldalakat kell megszámolnia, meg kell vizsgálnia a dokumentum adatait, vagy le kell másolnia egy fájlt anélkül, hogy érintené a tartalmát, a Direct File API teljesen elkerüli a teljes elemzést. A DAOpenFileReadOnly feltérképezi a kereszthivatkozási táblát az objektumfolyamok kitömörítése nélkül, így az oldalszám O(xref méret) az O(fájlméret) helyett:
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;
A figyelmeztetés: A DAOpenFileReadOnly elfogad egy jelszó paramétert, de titkosított bemenetek esetén visszatér a teljes elemzéshez, mivel a visszafejtés megköveteli az objektumfától a titkosítási szótár feloldását. Ha a forrásfájlok titkosítottak, először fejtse vissza őket a DecryptFile segítségével, hogy megkapja a titkosítatlan másolatot, majd nyissa meg azt a Direct File API-val. A fájlszintű DecryptFile függvény a közvetlen AES-256 újraírási útvonalat alkalmazza a szabványos titkosításhoz, és gyorsabb, mint a LoadFromFile, majd a SaveLoadedDocument a nagy fájlok esetében, mert nem építi fel a teljes memóriabeli objektummodellt
Memória a nagy kötegelt feldolgozás során
Azok a kötegelt feladatok, amelyek tucatnyi fájlt dolgoznak fel egy ciklusban, olyan mintázattal rendelkeznek, amely helyesnek tűnik, de memóriát halmoz fel: THotPDF létrehozása a cikluson belül, a LoadFromFile meghívása, munka végzése, a Free meghívása. Szerkezetileg ez rendben van. A probléma akkor jelentkezik, ha a belső munka átmeneti objektumokat (scratch objects) foglal le, kivételeket kap el, és ezeket az átmeneti objektumokat a hibaútvonalakon élve hagyja. A Delphi memóriakezelője nem tömörít, így egy kötegelt futtatás során száz hibaútvonal-szivárgás elég magasra nyomhatja a memóriát ahhoz, hogy minden más számára lelassítsa a lefoglalást
A javítás nem egzotikus. Minden THotPDF és minden köztes TStream vagy TBitmap, amely részt vesz a PDF-munkában, egy try/finally blokkba tartozik, ahol a Free az utolsó utasítás. Állítsa a helyi mutatókat nil értékre a try előtt, így a finally ág biztonságosan használhatja az if Assigned(x) then x.Free kifejezést, ha az inicializálás részben sikertelen. Ez a szabványos Delphi tulajdonjogi fegyelem, és ez a teljes történet ehhez a problémakörhöz
Még egy dolog, amit ellenőrizni kell a kötegelt kontextusokban: Az AddImage egy belső listába regisztrálja a képeket, amely a THotPDF példány élettartama alatt megmarad. Ha egyetlen példányt használ fel újra számos dokumentumban a LoadFromFile ismételt meghívásával, a korábbi dokumentumok kép regisztrációi a listában maradnak. Vagy hozzon létre egy új példányt dokumentumonként, vagy hívja meg a képlista törlési útvonalat a dokumentumok között
Mérés a változtatás előtt
Mielőtt e minták bármelyikéhez nyúlna, mérjen. A Delphi TStopwatch-ja a System.Diagnostics-ből becsomagolja a QueryPerformanceCounter-t, és elég pontos a fájl I/O profilozásához a falióra (wall-clock) szerint. Csomagolja be a LoadFromFile-t magát, és nézze meg, mennyi időt tesz ki. Ha a teljes idő 90%-át, akkor a javítás a Direct File API, vagy annak csökkentése, hogy hányszor elemzi ugyanazt a fájlt. Ha 20% alatt van, akkor a szűk keresztmetszet máshol van, és rossz dolgot üldöz
Az a kétperces kinyerés, amely ezt a bejegyzést elindította, teljes mértékben az ismételt betöltés (repeated-load) mintájának bizonyult. A dokumentum felépítése semmivel sem járult hozzá ehhez; egy lapos oldalfa is ugyanígy futott volna le. Ha áttérünk egyetlen LoadFromFile-ra, majd egy InsertPagesFromDocument hívásra, az 1,3 másodpercre csökkentette ugyanazon a hardveren, anélkül, hogy bármi máshoz hozzáérnénk
Az itt bemutatott oldal manipulációs API a Delphihez és C++Builderhez készült HotPDF Component része