Dvě minuty na zkopírování tří stránek ze 40stránkového PDF není problém ladění výkonu. Je to signál, že se používá nesprávná cesta API. Když jsem toto načasování poprvé viděl na ukázce kopírování stránek v komponentě HotPDF, mým instinktem bylo podívat se nejprve na strukturu dokumentu a na kód až jako druhý. Ukázalo se, že na tomto pořadí záleží
Co bylo ve skutečnosti pomalé
Dotyčné PDF byl 40stránkový referenční dokument s netriviálním stromem stránek: více mezilehlých uzlů /Pages namísto jediného plochého pole. Původní ukázkový kód volal LoadFromFile, pak vytvářel nový dokument pomocí BeginDoc, cyklil přes vybraná čísla stránek a v každé iteraci znovu načítal zdrojový dokument z disku, aby vytáhl stránku. To je plná cena zpracování (parse) vynásobená libovolným počtem stránek. Soubor o velikosti 12 MB zasáhl disk šestkrát pro třístránkovou extrakci, protože se nikdo nepodíval, jestli soubor potřebuje zůstat otevřený napříč iteracemi
Druhý viník byl v kódu neviditelný: HotPDF LoadFromFile vyřeší celou křížovou referenční tabulku a při načítání dekomprimuje každý proud objektů. To je správné chování pro dokument, který se chystáte upravovat, ale je to víc práce, než potřebujete, pokud chcete jen počet stránek a podmnožinu stránek. Pro přístup ke struktuře pouze pro čtení se DAOpenFileReadOnly vyhne deserializaci celého stromu objektů, na čemž záleží u komprimovaných souborů s rozsáhlými obrázkovými prostředky
Nic z toho není chyba knihovny. V obou případech volající vybírají API navržené pro jeden úkol a používají ho pro jiný
Použití InsertPagesFromDocument pro extrakci stránek
Správná cesta pro kopírování rozsahu stránek z jednoho dokumentu HotPDF do jiného je InsertPagesFromDocument, volaná po LoadFromFile na zdroji. Zdroj načtete jednou, cíl načtete nebo vytvoříte jednou, přesunete stránky a uložíte. Zdroj zůstává v paměti po celou dobu vkládání stránek:
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;
Parametr PageRange přijímá stejný formát jako ukázka příkazového řádku: čárkou oddělený seznam čísel stránek nebo rozsahů, jako je '1-3' nebo '1,5,7-9'. Stránky jsou indexovány od jedničky. InsertPagesFromDocument kopíruje datové proudy obsahu, slovníky prostředků a geometrii stránek, aniž by se dotýkal metadat, záložek nebo vložených příloh souborů, pokud na ně neexistují odkazy z kopírovaných stránek. Pro třístránkovou extrakci ze 40stránkového dokumentu se jedná o malou pracovní sadu
Načasování u stejného 12 MB souboru, který dříve běžel dvě minuty: pod 1,5 sekundy s tímto vzorem. Většinu tohoto času tvoří jediné volání LoadFromFile. Jakmile je tabulka objektů vyřešena poprvé, je struktura dokumentu irelevantní
Když je LoadFromFile příliš mnoho: Direct File API
Pokud potřebujete pouze spočítat stránky, prozkoumat informace o dokumentu nebo zkopírovat soubor bez zásahu do jeho obsahu, rozhraní Direct File API se úplně vyhne plnému zpracování. DAOpenFileReadOnly mapuje křížovou referenční tabulku bez dekomprimování datových proudů objektů, takže počet stránek je O(velikost xref) namísto O(velikost souboru):
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;
Upozornění: DAOpenFileReadOnly přijímá parametr hesla, ale u šifrovaných vstupů se vrací k plnému zpracování, protože dešifrování vyžaduje strom objektů k vyřešení šifrovacího slovníku. Jsou-li vaše zdrojové soubory zašifrovány, nejprve je dešifrujte pomocí DecryptFile, abyste získali nezašifrovanou kopii, a poté ji otevřete pomocí rozhraní Direct File API. Funkce DecryptFile na úrovni souboru používá pro standardní šifrování přímou cestu k přepisu AES-256 a pro velké soubory je rychlejší než LoadFromFile následovaný SaveLoadedDocument, protože nevytváří plný paměťový objektový model
Paměť během zpracování velkých dávek
Dávkové úlohy, které zpracovávají desítky souborů ve smyčce, mají vzorec, který vypadá správně, ale hromadí paměť: vytvoření THotPDF uvnitř smyčky, volání LoadFromFile, provedení práce, volání Free. To je strukturálně v pořádku. Problém nastává ve chvíli, kdy vnitřní práce alokuje pomocné objekty, zachycuje výjimky a nechává tyto pomocné objekty žít na chybových cestách. Správce paměti v Delphi nekompaktuje, takže stovka úniků na chybových cestách během dávkového běhu může posunout paměť dostatečně vysoko na to, aby to zpomalilo alokaci pro cokoli dalšího
Oprava není nijak exotická. Každé THotPDF a každý průběžný TStream nebo TBitmap, který se podílí na práci s PDF, patří do bloku try/finally, kde je Free tím posledním příkazem. Před try nastavte lokální ukazatele na nil, aby větev finally mohla bezpečně použít if Assigned(x) then x.Free, když se inicializace v polovině nezdaří. Toto je standardní disciplína vlastnictví v Delphi a je to celý příběh této třídy problémů
Ještě jedna věc ke kontrole v dávkovém kontextu: AddImage registruje obrázky v interním seznamu, který přetrvává po celou dobu životnosti instance THotPDF. Pokud znovu použijete jednu instanci napříč mnoha dokumenty tak, že opakovaně zavoláte LoadFromFile, registrace obrázků ze starších dokumentů zůstanou v seznamu. Buď vytvořte pro každý dokument čerstvou instanci, nebo mezi dokumenty vyvolejte smazání seznamu obrázků
Měření před jakoukoliv změnou
Než sáhnete po některém z těchto vzorů, měřte. TStopwatch ze System.Diagnostics v Delphi obaluje QueryPerformanceCounter a je dostatečně přesný pro profilování I/O operací se soubory podle času. Obalte samotný LoadFromFile a uvidíte, jak dlouho to potrvá. Pokud je to 90 % celkového času, řešením je Direct File API nebo snížení počtu případů, kdy zpracováváte stejný soubor. Pokud je to pod 20 %, úzké hrdlo je někde jinde a vy se honíte za špatnou věcí
Dvouminutová extrakce, která tento článek odstartovala, se ukázala být celá pouze vzorem opakovaného načítání. Struktura dokumentu k tomu ničím nepřispěla; plochý strom stránek by proběhl stejně. Přechod na jedno jediné volání LoadFromFile, následované jedním voláním InsertPagesFromDocument, zkrátil čas na 1,3 sekundy na stejném hardwaru, aniž by se sázelo na cokoliv jiného
Rozhraní API pro manipulaci se stránkami zobrazené zde je součástí komponenty HotPDF pro Delphi a C++Builder