Две минути за копиране на три страници от 40-страничен PDF не е проблем с настройката на производителността. Това е сигнал, че се използва грешен API път. Когато за пръв път видях това измерване на пример за копиране на страници от HotPDF Component, инстинктът ми беше да разгледам структурата на документа преди кода. Оказа се, че редът има значение
Какво всъщност беше бавно
PDF файлът беше 40-страничен референтен документ с нетривиално дърво на страниците: множество междинни /Pages възли вместо единичен плосък масив. Оригиналният примерен код извикваше LoadFromFile, след това изграждаше нов документ с BeginDoc, обхождаше избраните номера на страниците и при всяка итерация зареждаше изходния документ отново от диска, за да извлече страница. Това е пълната стойност на парсинга, умножена по броя на страниците, които искате. 12 MB файл ударяше диска шест пъти за извличане на три страници, защото никой не проверяваше дали файлът трябва да остане отворен между итерациите
Вторият принос беше невидим в кода: LoadFromFile на HotPDF разрешава цялата таблица с кръстосани препратки и декомпресира всеки поток от обекти при зареждане. Това е правилното поведение за документ, който ще модифицирате, но е повече работа от необходимото, ако искате само броя на страниците и подмножество от тях. За достъп само за четене до структурата, DAOpenFileReadOnly избягва десериализирането на пълното дърво от обекти, което има значение при компресирани файлове с големи графични ресурси
Нито едното от двете не е грешка в библиотеката. И двете са извикващи, избиращи API, проектиран за една задача, и го използващи за друга
Използване на InsertPagesFromDocument за извличане на страници
Правилният начин за копиране на диапазон от страници от един HotPDF документ в друг е InsertPagesFromDocument, извикан след LoadFromFile за изходния документ. Зареждате изходния файл веднъж, зареждате или създавате дестинацията веднъж, премествате страниците и записвате. Изходният файл остава в паметта по време на всички вмъквания на страници:
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 приема същия формат като примера от командния ред: разделен със запетаи списък от номера на страници или диапазони, като '1-3' или '1,5,7-9'. Страниците са с номерация от 1. InsertPagesFromDocument копира потоци от съдържание, речници с ресурси и геометрия на страниците, без да докосва метаданни, отметки или прикачени файлове, освен ако не са препратени от копираните страници. За извличане на три страници от 40-страничен документ, това е малък работен набор
Времето за същия 12 MB файл, който преди е работил две минути: под 1,5 секунди с тази схема. По-голямата част от времето е единственото извикване на LoadFromFile. Структурата на документа е без значение, след като таблицата с обекти е разрешена за пръв път
Когато LoadFromFile е твърде много: Direct File API
Ако трябва само да броите страници, да инспектирате информацията за документа или да копирате файл без да докосвате съдържанието му, Direct File API избягва изцяло пълния парсинг. DAOpenFileReadOnly картографира таблицата с кръстосани препратки без декомпресиране на потоци от обекти, така че броят на страниците е O(размер на xref) вместо O(размер на файла):
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;
Предупреждението: DAOpenFileReadOnly приема параметър за парола, но прибягва до пълен парсинг за криптирани входни данни, тъй като декриптирането изисква дървото от обекти за разрешаване на речника за криптиране. Ако изходните файлове са криптирани, първо ги декриптирайте с DecryptFile, за да получите некриптирано копие, след което отворете него с Direct File API. Функцията DecryptFile на ниво файл приема директен AES-256 път за презаписване за стандартно криптиране и е по-бърза от LoadFromFile, последвано от SaveLoadedDocument за големи файлове, тъй като не изгражда пълния модел на обекти в паметта
Памет при мащабна пакетна обработка
Пакетните задачи, обработващи десетки файлове в цикъл, имат схема, изглеждаща правилна, но натрупваща памет: създаване на THotPDF в цикъла, извикване на LoadFromFile, извършване на работа, извикване на Free. Структурно е добре. Проблемът е когато вътрешната работа заделя временни обекти, улавя изключения и оставя тези временни обекти живи по пътя на грешките. Мениджърът на паметта на Delphi не компактира, така че сто изтичания по пътя на грешките в рамките на пакетно изпълнение могат да повишат паметта достатъчно, за да забавят заделянето за всичко останало
Поправката не е екзотична. Всеки THotPDF и всеки междинен TStream или TBitmap, участващ в PDF работата, трябва да е в блок try/finally, където Free е последният оператор. Задайте локалните указатели на nil преди try, за да може клонът finally да използва безопасно if Assigned(x) then x.Free, когато инициализацията се провали наполовина. Това е стандартната дисциплина за собственост в Delphi и е пълната история за този клас проблеми
Още едно нещо за проверка в пакетни контексти: AddImage регистрира изображения в вътрешен списък, който продължава да съществува за времето на живот на инстанцията THotPDF. Ако използвате повторно единична инстанция за много документи, като многократно извиквате LoadFromFile, регистрациите на изображения от по-ранни документи остават в списъка. Или създайте нова инстанция за всеки документ, или извикайте пътя за изчистване на списъка с изображения между документите
Измерване преди промяна на каквото и да е
Преди да прибягвате към някоя от тези схеми, измерете. TStopwatch на Delphi от System.Diagnostics обвива QueryPerformanceCounter и е достатъчно точен за профилиране на стенния часовник при файлов I/O. Обвийте само LoadFromFile и вижте колко от общото време заема. Ако е 90%, поправката е Direct File API или намаляване на броя на парсинганията на един и същ файл. Ако е под 20%, тесното място е някъде другаде и преследвате грешното нещо
Двуминутното извличане, послужило като начало на тази статия, се оказа изцяло схемата за многократно зареждане. Структурата на документа нямаше принос; плоско дърво на страниците щеше да работи по същия начин. Преминаването към единично LoadFromFile, последвано от едно извикване на InsertPagesFromDocument, го доведе до 1,3 секунди на същия хардуер без докосване на нищо друго
Показаният тук API за манипулация на страниците е част от HotPDF Component за Delphi и C++Builder