Dve minuti za kopiranje treh strani iz 40-stranskega dokumenta PDF ni težava z optimizacijo delovanja. To je znak, da se uporablja napačna pot API. Ko sem prvič videl ta čas v primeru kopiranja strani komponente HotPDF, je bil moj instinkt najprej preveriti strukturo dokumenta in šele nato kodo. Ta vrstni red se je izkazal za pomembnega.
Kaj je bilo dejansko počasno
Zadevni PDF je bil 40-stranski referenčni dokument z netrivialnim drevesom strani: več vmesnimi vozlišči /Pages namesto enega samega ravnega polja. Prvotna vzorčna koda je klicala LoadFromFile, nato zgradila nov dokument z BeginDoc, se pomikala skozi izbrane številke strani in pri vsaki iteraciji znova naložila izvorni dokument z diska, da bi izvlekla stran. To pomeni polne stroške razčlenjevanja, pomnožene s številom strani, ki jih želite. 12-megabajtna datoteka je nesmiselno šestkrat dostopala do diska za ekstrakcijo treh strani, ker nihče ni preveril, ali mora datoteka med iteracijami ostati odprta.
Drugi dejavnik je bil v kodi neviden: klic LoadFromFile komponente HotPDF ob nalaganju razreši celotno navzkrižno tabelo in dekomprimira vsak tok objekta. To je pravilno vedenje za dokument, ki ga nameravate spremeniti, vendar je to več dela, kot ga potrebujete, če želite le število strani in podmnožico strani. Za dostop do strukture samo za branje klic DAOpenFileReadOnly prepreči deserializacijo celotnega drevesa objektov, kar je pomembno pri stisnjenih datotekah z velikimi slikovnimi viri.
Nobena od teh stvari ni hrošč v knjižnici. V obeh primerih so klicatelji izbrali API, zasnovan za eno delo, in ga uporabili za nekaj drugega.
Uporaba InsertPagesFromDocument za ekstrakcijo strani
Prava pot za kopiranje obsega strani iz enega dokumenta HotPDF v drugega je InsertPagesFromDocument, ki se pokliče po klicu LoadFromFile na viru. Vir naložite enkrat, cilj naložite ali ustvarite enkrat, premaknete strani in shranite. Vir ostane v pomnilniku skozi vse vstavitve strani:
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;
Parameter PageRange sprejema enako obliko kot primer v ukazni vrstici: z vejico ločen seznam številk strani ali obsegov, kot sta '1-3' ali '1,5,7-9'. Strani se štejejo od 1 naprej. InsertPagesFromDocument prekopira tokove vsebine, slovarje virov in geometrijo strani, ne da bi se dotaknil metapodatkov, zaznamkov ali priloženih datotek, razen če se kopirane strani nanje izrecno sklicujejo. Za kopiranje treh strani iz 40-stranskega dokumenta je to majhen delovni nabor.
Čas izvajanja na isti 12-megabajtni datoteki, ki je prej trajal dve minuti: pod 1,5 sekunde s tem vzorcem. Večina tega časa odpade na en sam klic LoadFromFile. Ko se tabela objektov prvič razreši, je struktura dokumenta nepomembna.
Ko je LoadFromFile preveč: Direct File API
Če morate le prešteti strani, pregledati podatke o dokumentu ali kopirati datoteko brez spreminjanja njene vsebine, se Direct File API popolnoma izogne celotnemu razčlenjevanju. Klic DAOpenFileReadOnly preslika navzkrižno tabelo brez dekompresije tokov objektov, zato je štetje strani O(velikost xref) in ne O(velikost datoteke):
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;
Opozorilo: DAOpenFileReadOnly sicer sprejme parameter gesla, vendar se pri šifriranih vhodih vrne k celotnemu razčlenjevanju, saj dešifriranje zahteva objektno drevo za razrešitev slovarja šifriranja. Če so vaše izvorne datoteke šifrirane, jih najprej dešifrirajte z DecryptFile, da dobite nešifrirano kopijo, nato pa jo odprite z Direct File API. Funkcija DecryptFile na ravni datoteke uporablja neposredno pot prepisovanja AES-256 za standardno šifriranje in je pri velikih datotekah hitrejša od zaporedja LoadFromFile in SaveLoadedDocument, saj ne gradi celotnega objektnega modela v pomnilniku.
Pomnilnik med obdelavo velikih serij
Serijska opravila, ki v zanki obdelujejo na desetine datotek, imajo vzorec, ki se zdi pravilen, a kopiči pomnilnik: ustvarjanje THotPDF znotraj zanke, klic LoadFromFile, izvedba dela in klic Free. To je strukturno v redu. Težava nastane, ko delo znotraj zanke dodeljuje pomožne objekte, lovi izjeme in te objekte pusti aktivne na poteh napak. Delphijev upravljalnik pomnilnika ne izvaja stiskanja (compaction), zato lahko sto uhajanj na poti napak med serijskim zagonom dvigne porabo pomnilnika dovolj visoko, da upočasni dodeljevanje za vse ostalo.
Popravek ni zapleten. Vsak THotPDF in vsak vmesni TStream ali TBitmap, ki sodeluje pri delu s PDF-jem, sodi v blok try/finally, kjer je Free zadnji stavek. Nastavite lokalne kazalce na nil pred try, da lahko veja finally varno uporabi if Assigned(x) then x.Free, ko inicializacija delno ne uspe. To je standardna Delphijeva disciplina lastništva in v celoti rešuje ta razred težav.
Še ena stvar, ki jo je treba preveriti v serijskih kontekstih: klic AddImage registrira slike v interni seznam, ki obstaja ves čas življenjske dobe instance THotPDF. Če isto instanco znova uporabite v več dokumentih z večkratnim klicanjem LoadFromFile, registracije slik iz prejšnjih dokumentov ostanejo na seznamu. Za vsak dokument ustvarite novo instanco ali pa med dokumenti počistite seznam slik.
Merjenje pred kakršnimi koli spremembami
Preden posežete po katerem koli od teh vzorcev, opravite meritve. Delphijev TStopwatch iz enote System.Diagnostics ovija QueryPerformanceCounter in je dovolj natančen za časovno profiliranje datotečnega I/O. Izmerite sam klic LoadFromFile in preverite, kolikšen del časa predstavlja. Če predstavlja 90 % celotnega časa, je rešitev Direct File API ali zmanjšanje števila razčlenjevanj iste datoteke. Če je pod 20 %, je ozko grlo drugje in iščete napačno stvar.
Dveminutna ekstrakcija, s katero se je začel ta prispevek, je bila v celoti posledica vzorca večkratnega nalaganja. Struktura dokumenta k temu ni prispevala ničesar; ravno drevo strani bi delovalo enako. Preklop na en sam LoadFromFile, ki mu je sledil en klic InsertPagesFromDocument, je čas izvajanja na isti strojni opremi zmanjšal na 1,3 sekunde brez kakršnih koli drugih sprememb.
API za manipulacijo strani, prikazan tukaj, je del komponente HotPDF za Delphi in C++Builder.