Dvi minutės nukopijuoti tris puslapius iš 40 puslapių PDF dokumento nėra našumo derinimo problema. Tai signalas, kad naudojamas neteisingas API kelias. Kai pirmą kartą pamačiau šį laiką HotPDF Component puslapių kopijavimo pavyzdyje, mano instinktas buvo pirmiausia pažvelgti į dokumento struktūrą, o tik tada į kodą. Paaiškėjo, kad ši tvarka buvo svarbi
Kas iš tikrųjų buvo lėta
Minėtas PDF buvo 40 puslapių informacinis dokumentas su netrivialiu puslapių medžiu: keli tarpiniai /Pages mazgai vietoje vieno plokščio masyvo. Pradiniame pavyzdiniame kode buvo iškviečiama LoadFromFile, tada kuriamas naujas dokumentas su BeginDoc, iteruojama per pasirinktus puslapių numerius ir kiekvienos iteracijos metu šaltinio dokumentas vėl įkeliamas iš disko, kad būtų ištrauktas puslapis. Tai yra visa analizės kaina, padauginta iš norimo puslapių skaičiaus. 12 MB failas į diską kreipėsi šešis kartus dėl trijų puslapių išgavimo, nes niekas nepažiūrėjo, ar failas turi likti atidarytas per visas iteracijas
Antrasis veiksnys kode buvo nematomas: HotPDF LoadFromFile įkeliant išsprendžia visą kryžminių nuorodų lentelę (cross-reference table) ir išskleidžia kiekvieną objektų srautą. Tai yra tinkamas elgesys dokumentui, kurį ketinate keisti, bet tai yra daugiau darbo nei reikia, jei norite tik puslapių skaičiaus ir dalies puslapių. Tik skaitymo (read-only) prieigai prie struktūros DAOpenFileReadOnly išvengia viso objektų medžio deserializavimo, o tai svarbu suspaustiems failams su dideliais vaizdų ištekliais
Nė vienas iš šių dalykų nėra bibliotekos klaida. Abiem atvejais tai iškviečiančiojo kodo problema, kai pasirenkamas vienam darbui skirtas API, bet naudojamas kitam
InsertPagesFromDocument naudojimas puslapių išgavimui
Tinkamas būdas nukopijuoti puslapių diapazoną iš vieno HotPDF dokumento į kitą yra InsertPagesFromDocument, iškviečiamas po LoadFromFile šaltinyje. Šaltinį įkeliate vieną kartą, paskirties vietą įkeliate arba sukuriate vieną kartą, perkeliate puslapius ir išsaugote. Šaltinis lieka atmintyje visų puslapių įterpimų metu:
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;
Parametras PageRange priima tokį patį formatą kaip ir komandinės eilutės pavyzdys: kableliais atskirtą puslapių numerių ar diapazonų sąrašą, pvz., '1-3' arba '1,5,7-9'. Puslapių numeracija prasideda nuo 1. InsertPagesFromDocument nukopijuoja turinio srautus, išteklių žodynus ir puslapio geometriją neliesdamas metaduomenų, žymių (bookmarks) ar įterptų failų priedų, nebent į juos yra nuorodos iš nukopijuotų puslapių. Išgaunant tris puslapius iš 40 puslapių dokumento, tai yra mažas darbinis rinkinys
To paties 12 MB failo, kuris anksčiau buvo apdorojamas dvi minutes, laikas: mažiau nei 1,5 sekundės naudojant šį šabloną. Didžiąją to laiko dalį užima vienas LoadFromFile iškvietimas. Dokumento struktūra tampa nesvarbi, kai objektų lentelė išsprendžiama pirmą kartą
Kai LoadFromFile yra per daug: tiesioginio failo (Direct File) API
Jei jums reikia tik suskaičiuoti puslapius, patikrinti dokumento informaciją ar nukopijuoti failą neliečiant jo turinio, tiesioginio failo API visiškai išvengia pilnos analizės. DAOpenFileReadOnly susieja kryžminių nuorodų lentelę neišskleidžiant objektų srautų, todėl puslapių skaičiavimas yra O(xref dydis), o ne O(failo dydis):
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;
Išlyga: DAOpenFileReadOnly priima slaptažodžio parametrą, bet grįžta prie pilnos analizės užšifruotiems įvesties failams, nes iššifravimui reikia objektų medžio, kad būtų galima išspręsti šifravimo žodyną. Jei jūsų šaltinio failai yra užšifruoti, pirmiausia iššifruokite juos naudodami DecryptFile, kad gautumėte neužšifruotą kopiją, tada atidarykite ją su tiesioginio failo API. Failo lygio funkcija DecryptFile naudoja tiesioginį AES-256 perrašymo kelią standartiniam šifravimui ir yra greitesnė nei LoadFromFile po kurio seka SaveLoadedDocument dideliems failams, nes ji nesukuria pilno objektų modelio atmintyje
Atmintis apdorojant dideles partijas
Paketinės (batch) užduotys, kurios cikle apdoroja dešimtis failų, turi šabloną, kuris atrodo teisingas, bet kaupia atmintį: THotPDF kūrimas cikle, LoadFromFile iškvietimas, darbo atlikimas, Free iškvietimas. Struktūriškai tai yra gerai. Problema kyla, kai vidinis darbas paskirsto laikinus (scratch) objektus, sugauna išimtis ir palieka tuos laikinus objektus gyvus klaidų keliuose. Delphi atminties tvarkyklė neatlieka tankinimo (compact), todėl šimtas klaidų kelių nutekėjimų per paketinį vykdymą gali pakelti atminties naudojimą tiek, kad sulėtintų paskirstymą viskam kitam
Sprendimas nėra egzotiškas. Kiekvienas THotPDF ir kiekvienas tarpinis TStream ar TBitmap, kuris dalyvauja PDF darbe, priklauso try/finally blokui, kur Free yra paskutinis teiginys. Prieš try nustatykite vietines rodykles į nil, kad finally šaka galėtų saugiai naudoti if Assigned(x) then x.Free, kai inicijavimas nepavyksta įpusėjus. Tai yra standartinė Delphi nuosavybės disciplina ir tai yra visa istorija šios klasės problemoms spręsti
Dar vienas dalykas, kurį reikia patikrinti paketiniame kontekste: AddImage užregistruoja vaizdus vidiniame sąraše, kuris išlieka visą THotPDF egzemplioriaus gyvavimo laiką. Jei pakartotinai naudojate vieną egzempliorių daugeliui dokumentų pakartotinai iškviesdami LoadFromFile, vaizdų registracijos iš ankstesnių dokumentų lieka sąraše. Arba sukurkite naują egzempliorių kiekvienam dokumentui, arba iškvieskite vaizdų sąrašo išvalymo kelią tarp dokumentų
Matavimas prieš ką nors keičiant
Prieš imdamiesi bet kurio iš šių šablonų, išmatuokite. Delphi TStopwatch iš System.Diagnostics apgaubia QueryPerformanceCounter ir yra pakankamai tikslus realaus laiko failų I/O profiliavimui. Apgaubkite tik LoadFromFile ir pažiūrėkite, kiek laiko jis užima. Jei tai sudaro 90% viso laiko, sprendimas yra tiesioginio failo API arba to paties failo analizės kartų skaičiaus sumažinimas. Jei tai mažiau nei 20%, kliūtis yra kažkur kitur ir jūs ieškote ne ten, kur reikia
Dviejų minučių išgavimas, nuo kurio prasidėjo šis įrašas, pasirodė esąs visiškai pakartotinio įkėlimo šablonas. Dokumento struktūra prie to niekaip neprisidėjo; plokščias puslapių medis būtų veikęs taip pat. Perėjimas prie vieno LoadFromFile, po kurio seka vienas InsertPagesFromDocument iškvietimas, sumažino laiką iki 1,3 sekundės toje pačioje techninėje įrangoje nieko kito nekeičiant
Čia parodytas puslapių manipuliavimo API yra HotPDF Component, skirto Delphi ir C++Builder, dalis