To minutter på å kopiere tre sider ut av en 40-siders PDF er ikke et problem med ytelsesjustering. Det er et signal om at feil API-vei blir brukt. Første gang jeg så denne tidsbruken på et HotPDF Component sidekopieringseksempel, var instinktet mitt å se på dokumentstrukturen først og koden etterpå. Den rekkefølgen viste seg å spille en rolle
Hva som faktisk var tregt
PDF-en det var snakk om, var et referansedokument på 40 sider med et ikke-trivielt sidetre: flere mellomliggende /Pages-noder i stedet for en enkel, flat array (liste). Den opprinnelige eksempelkoden kalte LoadFromFile, bygget deretter et nytt dokument med BeginDoc, loopet over valgte sidetall, og på hver iterasjon lastet den kildedokumentet fra disk igjen for å hente én side. Det betyr den fulle parse-kostnaden multiplisert med hvor mange sider du vil ha. En fil på 12 MB traff disken seks ganger for et tre-siders uttrekk, fordi ingen så på hvorvidt filen behøvde å forbli åpen på tvers av iterasjonene
Den andre bidragsyteren var usynlig i koden: HotPDFs LoadFromFile løser opp (resolves) hele kryssreferansetabellen, og dekomprimerer hvert objekt-strøm ved innlasting. Det er riktig atferd for et dokument du er i ferd med å endre, men det er mer arbeid enn du trenger hvis du kun er ute etter sideantall og et delsett av sider. For skrivebeskyttet (read-only) tilgang til struktur unngår DAOpenFileReadOnly å deserialisere (deserialize) hele objekt-treet, noe som utgjør en stor forskjell på komprimerte filer med store bilderessurser
Ingen av disse er en bibliotekfeil. Begge er kallere (callers) som velger API-et designet for én jobb og bruker det til en annen
Bruke InsertPagesFromDocument for sideuttrekk
Den rette veien for å kopiere en rekke sider fra ett HotPDF-dokument og inn i et annet, er InsertPagesFromDocument, som kalles etter LoadFromFile på kilden. Du laster kilden én gang, laster eller oppretter destinasjonen én gang, flytter sidene, og lagrer. Kilden forblir i minnet gjennom alle side-innsettingene:
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-parameteren godtar samme format som kommandolinjeeksempelet: en kommadelt liste med sidenumre eller serier som '1-3' eller '1,5,7-9'. Sider er 1-basert. InsertPagesFromDocument kopierer innholdsstrømmer, ressursordbøker og sidegeometri uten å røre metadata, bokmerker eller innbygde filvedlegg, med mindre de er referert fra de kopierte sidene. For et tre-siders uttrekk fra et 40-siders dokument er det et ganske lite arbeidssett
Tidtaking på den samme 12 MB-filen som tidligere kjørte i to minutter: under 1,5 sekunder med dette mønsteret. Det meste av den tiden er selve LoadFromFile-kallet. Dokumentstrukturen er irrelevant når først objekttabellen er løst opp (resolved) første gang
Når LoadFromFile er for mye: Direct File API-et
Hvis du bare trenger å telle sider, inspisere dokumentinformasjon, eller kopiere en fil uten å røre innholdet, unngår Direct File API-et det fulle parset i sin helhet. DAOpenFileReadOnly kartlegger (maps) kryssreferansetabellen uten å dekomprimere objekt-strømmer, slik at sidetelling er O(xref size) fremfor O(file size):
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;
Haken (the caveat): DAOpenFileReadOnly godtar en passordparameter, men faller tilbake til en full parse for krypterte inndata (inputs), fordi dekryptering krever at objekttreet løser (resolves) krypteringsordboken. Hvis kildefilene dine er kryptert, dekrypter dem først med DecryptFile for å få en ukryptert kopi, og åpne deretter den med Direct File API-et. DecryptFile-funksjonen på filnivå tar en direkte AES-256 omskrivingsvei for standard kryptering og er raskere enn LoadFromFile fulgt av SaveLoadedDocument for store filer, fordi den ikke bygger den fulle minnebaserte objektmodellen (in-memory object model)
Minne under batchbehandling i stor skala
Batchjobber som behandler dusinvis av filer i en løkke (loop) har et mønster som ser korrekt ut, men akkumulerer minne: man oppretter THotPDF inne i løkken, kaller LoadFromFile, gjør arbeid, og kaller Free. Strukturelt sett er det helt fint. Problemet oppstår når det indre arbeidet allokerer midlertidige (scratch) objekter, fanger opp unntak (exceptions), og lar de midlertidige objektene leve på feil-veier (error paths). Delphis minnehåndterer (memory manager) komprimerer ikke (compacts), så hundre feil-vei lekkasjer på tvers av en batch-kjøring kan skyve minnet høyt nok til å bremse allokering for alt mulig annet
Løsningen er ikke eksotisk. Hver THotPDF og enhver mellomliggende TStream eller TBitmap som tar del i PDF-arbeid, hører hjemme i en try/finally-blokk, hvor Free er siste instruksjon. Sett lokale pekere til nil før try, slik at finally-grenen kan bruke if Assigned(x) then x.Free trygt, når initialisering feiler underveis. Dette er standard eierskapsdisiplin i Delphi, og det er hele historien for denne klassen av problemer
En ting til å sjekke i batch-sammenhenger: AddImage registrerer bilder i en intern liste som vedvarer gjennom levetiden til THotPDF-instansen. Hvis du gjenbruker én enkelt instans på tvers av mange dokumenter ved å kalle LoadFromFile gjentatte ganger, blir bilderegistreringer fra tidligere dokumenter værende i listen. Enten må du opprette en fersk instans per dokument, eller kalle ryddeprosedyren for bildelisten mellom dokumentene
Måle før man endrer noe
Før du griper etter noen av disse mønstrene, mål opp. Delphis TStopwatch fra System.Diagnostics pakker inn QueryPerformanceCounter, og er nøyaktig nok for vegg-klokke-profilering (wall-clock profiling) av fil-I/O. Legg den rundt LoadFromFile alene, og se hvor lang tid dette tar. Hvis det utgjør 90% av totaltiden, er løsningen Direct File API-et eller å redusere hvor mange ganger du parser den samme filen. Hvis det er under 20%, er flaskehalsen et annet sted, og du jakter på feil ting
De to minuttene med uttrekk som startet dette innlegget, viste seg utelukkende å være det gjentatte laste-mønsteret (repeated-load pattern). Dokumentstrukturen bidro ikke med noe; et flatt sidetre ville kjørt på samme måte. Å bytte til én enkelt LoadFromFile etterfulgt av ett InsertPagesFromDocument-kall tok det ned til 1,3 sekunder på den samme maskinvaren uten å røre noe annet
Siden-manipulerings API-et vist her er en del av HotPDF Component for Delphi og C++Builder