Technical Article

Performanse ekstrakcije stranica HotPDF-om u Delphi-ju

Dva minuta za kopiranje tri stranice iz PDF-a od 40 stranica nije problem podesavanja performansi. To je signal da se koristi pogresan API put. Kada sam prvi put video ovo merenje vremena na primeru kopiranja stranica HotPDF komponente, moj instinkt bio je da prvo pogledam strukturu dokumenta, a tek onda kod. Taj redosled se pokazao vaznim.

Sta je zapravo bilo sporo

PDF u pitanju bio je referentni dokument od 40 stranica s netrivijalnim stablom stranica: vise intermedijarnih /Pages cvorova umesto jednog ravnog niza. Originalni uzorak koda pozivao je LoadFromFile, zatim gradio novi dokument s BeginDoc, prolazio petljom kroz odabrane brojeve stranica, i na svakoj iteraciji ponovo ucitavao izvorni dokument s diska da bi izvukao stranicu. To je pun trosak parsiranja pomnоzen brojem stranica koje zelite. Datoteka od 12 MB pogodila je disk sest puta za ekstrakciju tri stranice jer niko nije proverio da li datoteka treba da ostane otvorena tokom iteracija.

Drugi doprinosec bio je nevidljiv u kodu: HotPDF-ov LoadFromFile razresava celu tabelu unakrsnih referenci i dekompresuje sve tokove objekata pri ucitavanju. To je ispravno ponasanje za dokument koji cete modifikovati, ali je vise posla nego sto vam treba ako zelite samo broj stranica i podskup stranica. Za pristup strukturi samo za citanje, DAOpenFileReadOnly izbegava deserijalizaciju punog stabla objekata, sto je vazno na kompresovanim datotekama s velikim resursima slika.

Nijedno od ovoga nije bug biblioteke. Oba slucaja su pozivaoci koji biraju API dizajniran za jedan posao i koriste ga za drugi.

Koristenje InsertPagesFromDocument za ekstrakciju stranica

Pravi put za kopiranje opsega stranica iz jednog HotPDF dokumenta u drugi je InsertPagesFromDocument, pozvan posle LoadFromFile na izvoru. Ucitavate izvor jednom, ucitavate ili kreirate odrediste jednom, premestite stranice i sacuvate. Izvor ostaje u memoriji tokom svih umetanja stranica:

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;

Parametar PageRange prihvata isti format kao uzorak komandne linije: listu brojeva stranica ili opsega razdvojenih zarezima, poput '1-3' ili '1,5,7-9'. Stranice su zasnovane na indeksu 1. InsertPagesFromDocument kopira tokove sadrzaja, recnike resursa i geometriju stranice bez dodirivanja metapodataka, oznaka ili ugradenih priloga datoteka osim ako se na njih ne pozivaju kopirane stranice. Za ekstrakciju tri stranice iz dokumenta od 40 stranica, to je mali radni skup.

Merenje vremena na istoj datoteci od 12 MB koja je prethodno trosila dva minuta: manje od 1,5 sekundi s ovim obrascem. Vecina tog vremena je jedan poziv LoadFromFile. Struktura dokumenta je irеlevantna kada se tabela objekata razresi prvi put.

Kada je LoadFromFile previse: direktni API datoteka

Ako trebate samo da prebrojite stranice, pregledate informacije dokumenta ili kopirate datoteku bez dodirivanja njenog sadrzaja, direktni API datoteka u potpunosti izbegava puno parsiranje. DAOpenFileReadOnly mapira tabelu unakrsnih referenci bez dekompresovanja tokova objekata, pa je broj stranica O(velicina xref) umesto O(velicina 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;

Upozorenje: DAOpenFileReadOnly prihvata parametar lozinke, ali se vraca na puno parsiranje za sifrovane ulaze, jer dekriptovanje zahteva stablo objekata da bi razresilo recnik sifrovanja. Ako su vase izvorne datoteke sifrovane, najpre ih desifrisite s DecryptFile da biste dobili nesifrovanu kopiju, a zatim otvorite tu kopiju s direktnim API-jem datoteka. Funkcija DecryptFile na nivou datoteke uzima direktan AES-256 put prepisivanja za standardno sifrovanje i brza je od LoadFromFile pracenog SaveLoadedDocument za velike datoteke, jer ne gradi pun model objekata u memoriji.

Memorija tokom obrade velikih serija

Serijski poslovi koji obradjuju desetine datoteka u petlji imaju obrazac koji izgleda ispravno ali akumulira memoriju: kreiranje THotPDF unutar petlje, poziv LoadFromFile, rad, poziv Free. Strukturno je u redu. Problem je kada unutrasnji rad alocira privremene objekte, hvata izuzetke i ostavlja te privremene objekte zivima na putanjama grеsaka. Delphi-jev menadzер memorije ne kompaktira, pa sto curenja na putanjama gresaka tokom serijskog pokretanja moze gurnuti memoriju dovoljno visoko da uspori alokaciju za sve ostalo.

Resenje nije egzoticno. Svaki THotPDF i svaki intermedijarni TStream ili TBitmap koji ucestvuje u PDF radu pripada bloku try/finally gde je Free poslednja izjava. Postavite lokalne pokazivace na nil pre try kako bi grana finally mogla bezbedno koristiti if Assigned(x) then x.Free kada inicijalizacija delimicno ne uspe. Ovo je standardna Delphi disciplina vlasnistva i to je potpuna prica za ovu klasu problema.

Jos jedna stvar za proveru u serijskim kontekstima: AddImage registruje slike u internoj listi koja perzistira za zivotni vek instance THotPDF. Ako ponovo koristite jednu instancu kroz mnoge dokumente ponavljajuci poziv LoadFromFile, registracije slika iz ranijih dokumenata ostaju u listi. Ili kreirajte novu instancu po dokumentu ili pozovite put brisanja liste slika izmedju dokumenata.

Merenje pre bilo kakvih promena

Pre nego sto posegnete za bilo kojim od ovih obrazaca, izmerите. Delphi-jev TStopwatch iz System.Diagnostics obmotava QueryPerformanceCounter i dovoljno je tacan za profilisanje sata zida ulaza/izlaza datoteka. Obmotajte samo LoadFromFile i vidite koliko vremena zauzima. Ako je 90% ukupnog vremena, resenje je direktni API datoteka ili smanjenje broja puta parsiranja iste datoteke. Ako je ispod 20%, usko grlo je negde drugo i jurите pogresnu stvar.

Ekstrakcija od dva minuta koja je pokrenula ovaj tekst pokazala se kao iskljucivo obrazac ponovljenog ucitavanja. Struktura dokumenta nije doprinela nicemu; ravno stablo stranica bi radilo na isti nacin. Prelaz na jedan LoadFromFile pracen jednim pozivom InsertPagesFromDocument doveo je do 1,3 sekunde na istom hardveru bez dodirivanja iceg drugog.

API za manipulaciju stranicama prikazan ovde deo je HotPDF komponente za Delphi i C++Builder.