Dvije minute za kopiranje tri stranice iz PDF-a od 40 stranica nije problem ugađanja performansi. To je signal da se koristi pogrešan API put. Kada sam prvi put vidio ovo vrijeme na primjeru kopiranja stranica komponente HotPDF Component, moj je instinkt bio prvo pogledati strukturu dokumenta, a tek onda kod. Pokazalo se da je taj redoslijed bitan
Što je zapravo bilo sporo
Predmetni PDF bio je referentni dokument od 40 stranica s netrivijalnim stablom stranica: više prijelaznih /Pages čvorova umjesto jednog ravnog niza. Izvorni kod primjera pozivao je LoadFromFile, a zatim gradio novi dokument s BeginDoc, petljao po odabranim brojevima stranica i u svakoj iteraciji ponovno učitavao izvorni dokument s diska da bi izvukao stranicu. To je puni trošak parsiranja pomnožen s onoliko stranica koliko želite. Datoteka od 12 MB pristupila je disku šest puta za ekstrakciju od tri stranice jer nitko nije provjerio treba li datoteka ostati otvorena tijekom iteracija
Drugi čimbenik bio je nevidljiv u kodu: LoadFromFile komponente HotPDF rješava cijelu tablicu unakrsnih referenci i dekomprimira svaki tok objekata prilikom učitavanja. To je ispravno ponašanje za dokument koji se spremate izmijeniti, ali to je više posla nego što trebate ako želite samo broj stranica i podskup stranica. Za pristup strukturi samo za čitanje, DAOpenFileReadOnly izbjegava deserijalizaciju cijelog stabla objekata, što je važno kod komprimiranih datoteka s velikim slikovnim resursima
Nijedno od ovoga nije bug u biblioteci. U oba slučaja pozivatelji biraju API dizajniran za jedan posao i koriste ga za drugi
Korištenje InsertPagesFromDocument za ekstrakciju stranica
Pravi put za kopiranje niza stranica iz jednog HotPDF dokumenta u drugi je InsertPagesFromDocument, pozvan nakon LoadFromFile na izvoru. Izvor učitavate jednom, odredište učitavate ili kreirate jednom, premještate stranice i spremate. Izvor ostaje u memoriji tijekom 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
// Učitaj izvor jednom: potpuno parsiranje događa se ovdje i samo ovdje
Source.LoadFromFile(SourceFile);
// Izradi minimalni odredišni dokument
Dest.FileName := DestFile;
Dest.BeginDoc;
// Kopiraj traženi raspon; '1-3' umeće stranice od 1 do 3
// počevši od pozicije 1 u odredištu
Dest.InsertPagesFromDocument(Source, PageRange, 1);
Dest.EndDoc;
finally
Source.Free;
Dest.Free;
end;
end;
Parametar PageRange prihvaća isti format kao i primjer iz komandne linije: zarezima odvojen popis brojeva stranica ili raspona kao što su '1-3' ili '1,5,7-9'. Stranice su indeksirane od 1. InsertPagesFromDocument kopira tokove sadržaja, rječnike resursa i geometriju stranice bez diranja metapodataka, knjižnih oznaka ili ugrađenih privitaka datoteka osim ako se ne referenciraju s kopiranih stranica. Za ekstrakciju tri stranice iz dokumenta od 40 stranica, to je mali radni skup
Vrijeme izvođenja na istoj datoteci od 12 MB koja je prije trajala dvije minute: manje od 1,5 sekunde s ovim obrascem. Većina tog vremena odlazi na jedan poziv LoadFromFile. Struktura dokumenta je irelevantna nakon što se tablica objekata prvi put riješi
Kada je LoadFromFile previše: Direct File API
Ako trebate samo izbrojati stranice, pregledati informacije o dokumentu ili kopirati datoteku bez diranja njezinog sadržaja, Direct File API u potpunosti izbjegava cjelokupno parsiranje. DAOpenFileReadOnly mapira tablicu unakrsnih referenci bez dekompresije tokova objekata, tako da je broj stranica O(veličina xref-a) umjesto O(veličina 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 je kopija koja čuva bajtove, nema ponovne serijalizacije
Pdf.DACopyFile(FileName, 'archive-copy.pdf');
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
Upozorenje: DAOpenFileReadOnly prihvaća parametar lozinke, ali se vraća na puno parsiranje za kriptirane ulaze, jer dekripcija zahtijeva stablo objekata kako bi riješila rječnik enkripcije. Ako su vaše izvorne datoteke kriptirane, prvo ih dekriptirajte pomoću DecryptFile kako biste dobili nekriptiranu kopiju, a zatim je otvorite pomoću Direct File API-ja. Funkcija DecryptFile na razini datoteke koristi izravni AES-256 put ponovnog pisanja za standardnu enkripciju i brža je od LoadFromFile praćenog sa SaveLoadedDocument za velike datoteke, jer ne gradi cijeli objektni model u memoriji
Memorija tijekom obrade velikih serija (large-batch)
Skupni poslovi (batch jobs) koji obrađuju desetke datoteka u petlji imaju obrazac koji izgleda točno, ali nakuplja memoriju: kreiranje THotPDF unutar petlje, pozivanje LoadFromFile, odrađivanje posla, pozivanje Free. To je strukturno u redu. Problem nastaje kada unutarnji rad alocira privremene objekte, hvata iznimke i ostavlja te privremene objekte živima na putanjama pogreške. Delphijev upravitelj memorijom se ne sažima, tako da stotinu curenja memorije zbog grešaka u serijskom pokretanju može gurnuti memoriju dovoljno visoko da uspori alokaciju za sve ostalo
Rješenje nije egzotično. Svaki THotPDF i svaki posredni TStream ili TBitmap koji sudjeluje u PDF radu pripada u try/finally blok gdje je Free posljednja naredba. Postavite lokalne pokazivače na nil prije try tako da grana finally može sigurno koristiti if Assigned(x) then x.Free kada inicijalizacija djelomično ne uspije. Ovo je standardna Delphi disciplina vlasništva i to je cijela priča za ovu klasu problema
Još jedna stvar koju treba provjeriti u serijskim kontekstima: AddImage registrira slike u internoj listi koja postoji tijekom životnog vijeka THotPDF instance. Ako ponovno koristite jednu instancu kroz mnogo dokumenata višekratnim pozivanjem LoadFromFile, registracije slika iz ranijih dokumenata ostaju u popisu. Ili stvorite svježu instancu za svaki dokument ili pozovite putanju za brisanje liste slika između dokumenata
Mjerenje prije bilo kakvih promjena
Prije nego posegnete za bilo kojim od ovih obrazaca, mjerite. Delphijev TStopwatch iz System.Diagnostics omotava QueryPerformanceCounter i dovoljno je precizan za profiliranje u stvarnom vremenu ulaza/izlaza datoteka. Omotajte samo LoadFromFile i pogledajte koliko mu je vremena potrebno. Ako je to 90% ukupnog vremena, rješenje je Direct File API ili smanjenje broja parsiranja iste datoteke. Ako je ispod 20%, usko grlo je negdje drugdje i lovite pogrešnu stvar
Ekstrakcija od dvije minute s početka ovog posta pokazala se u potpunosti kao obrazac ponovljenog učitavanja. Struktura dokumenta nije pridonijela ničemu; ravno stablo stranica izvodilo bi se na isti način. Prelazak na jedan LoadFromFile praćen jednim pozivom InsertPagesFromDocument smanjio je to na 1,3 sekunde na istom hardveru bez diranja ičega drugog
API za manipulaciju stranicama prikazan ovdje dio je komponente HotPDF Component za Delphi i C++Builder