Dvi minutės trijų puslapių nukopijavimui iš 40 puslapių PDF failo nėra našumo derinimo problema. Tai ženklas, kad naudojamas netinkamas API kelias. Kai pirmą kartą pamačiau tokį rezultatą HotPDF Component puslapių kopijavimo pavyzdyje, mano intuicija pasiūlė pirmiausia patikrinti dokumento struktūrą, o tik po to kodą. Pasirodė, kad šis eiliškumas buvo svarbus.
Kas iš tikrųjų lėtino procesą
Nagrinėjamas PDF buvo 40 puslapių informacinis dokumentas su sudėtingu puslapių medžiu: keliais tarpiniais /Pages mazgais, o ne vienu plokščiu masyvu. Pradiniame pavyzdiniame kode buvo iškviečiamas LoadFromFile, tuomet kuriamas naujas dokumentas su BeginDoc, einama per pasirinktus puslapių numerius ir kiekvienoje iteracijoje šaltinio dokumentas vėl buvo nuskaitomas iš disko puslapiui paimti. Tai reiškia, kad pilno analizavimo sąnaudos dauginamos iš puslapių skaičiaus. Dėl to 12 MB failas buvo nuskaitomas iš disko šešis kartus trijų puslapių išgavimui, nes niekas nepasirūpino, kad failas liktų atidarytas iteracijų metu.
Antrasis veiksnys kode buvo nematomas: „HotPDF“ funkcija LoadFromFile įkėlimo metu išsprendžia visą kryžminių nuorodų lentelę (cross-reference table) ir išspaudžia kiekvieno objekto srautą. Tai tinkama elgsena dokumentui, kurį ketinate keisti, tačiau tai yra per didelis darbas, jei norite tik sužinoti puslapių skaičių ar nukopijuoti dalį puslapių. Tik struktūros skaitymui skirta funkcija DAOpenFileReadOnly padeda išvengti pilno objektų medžio deserializavimo, kas yra itin svarbu suspaustiems failams su dideliais paveikslėlių ištekliais.
Nei vienas iš šių atvejų nėra bibliotekos klaida. Abiem atvejais programuotojai tiesiog pasiriko vienam darbui skirtą API ir panaudojo jį kitam.
InsertPagesFromDocument naudojimas puslapių išgavimui
Tinkamas būdas kopijuoti puslapių rėžį iš vieno „HotPDF“ dokumento į kitą yra naudoti InsertPagesFromDocument, iškviečiamą po LoadFromFile šaltinio objekte. Vieną kartą įkeliate šaltinį, vieną kartą įkeliate arba sukuriate tikslą, perkeliate puslapius ir išsaugote. Šaltinis lieka atmintyje visų puslapių įterpimo 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 pavyzdyje: kableliais atskirtą puslapių numerių arba rėžių sąrašą, pavyzdžiui, '1-3' arba '1,5,7-9'. Puslapių indeksavimas prasideda nuo 1. Metodas InsertPagesFromDocument nukopijuoja turinio srautus, išteklių žodynus ir puslapio geometriją, neliesdamas metaduomenų, skirtukų (bookmarks) ar prisegtų failų, nebent jie yra susieti su nukopijuotais puslapiais. Trijų puslapių išgavimui iš 40 puslapių dokumento tai yra labai mažas operacijų rinkinys.
To paties 12 MB failo, kurio apdorojimas anksčiau truko dvi minutes, apdorojimo laikas su šiuo šablonu sutrumpėjo iki mažiau nei 1,5 sekundės. Didžiąją laiko dalį užima vienintelis LoadFromFile iškvietimas. Dokumento struktūra nebeturi reikšmės, kai objektų lentelė išsprendžiama pirmąjį kartą.
Kai LoadFromFile yra perteklinis: Direct File API
Jeigu jums reikia tik suskaičiuoti puslapius, patikrinti dokumento informaciją arba nukopijuoti failą neliečiant jo turinio, „Direct File API“ padeda visiškai išvengti pilno analizavimo. Metodas DAOpenFileReadOnly sukuria kryžminių nuorodų lentelės atvaizdą neišspausdamas objektų srautų, zodžiu, puslapių skaičiavimo sudėtingumas 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;
Svarbi pastaba: DAOpenFileReadOnly priima slaptažodžio parametrą, tačiau šifruotiems failams taiko pilną analizavimą, nes dešifravimui reikalingas objektų medis šifravimo žodynui išspręsti. Jei jūsų šaltinio failai yra šifruoti, pirmiausia iššifruokite juos su DecryptFile, kad gautumėte nešifruotą kopiją, ir tik tada atidarykite ją su „Direct File API“. Failo lygio funkcija DecryptFile naudoja tiesioginį AES-256 perrašymo kelią standartiniam šifravimui ir veikia sparčiau nei LoadFromFile kartu su SaveLoadedDocument dideliems failams, nes ji nesukuria pilno objektų modelio atmintyje.
Atminties valdymas vykdant didelės apimties paketinius darbus
Paketiniai darbai, apdorojantys dešimtis failų cikle, naudoja šabloną, kuris atrodo teisingas, tačiau kaupia atmintį: cikle sukuriamas THotPDF, iškviečiamas LoadFromFile, atliekamas darbas, iškviečiamas Free. Struktūriškai tai tvarkinga. Problema kyla tuomet, kai vidinis darbas išskiria atmintį laikiniesiems objektams, įvyksta klaidos ir tie laikinieji objektai lieka klaidos keliuose. „Delphi“ atminties tvarkyklė neatlieka sutankinimo (compaction), todėl šimtas atminties nutekėjimų paketo vykdymo metu gali padidinti atminties suvartojimą tiek, kad sulėtės visų kitų objektų kūrimas.
Sprendimas nėra sudėtingas. Kiekvienas THotPDF ir kiekvienas tarpinis TStream arba TBitmap, dalyvaujantis PDF darbuose, turi būti patalpintas į try/finally bloką, kur Free yra paskutinė instrukcija. Prieš try bloką priskirkite vietinėms rodyklėms nil reikšmę, kad finally šakoje galėtumėte saugiai naudoti if Assigned(x) then x.Free, jei inicijavimas nepavyktų įpusėjus procesui. Tai standartinė „Delphi“ objektų valdymo praktika, išsprendžianti šios klasės problemas.
Dar vienas dalykas, kurį verta patikrinti paketiniuose procesuose: metodas AddImage užregistruoja paveikslėlius vidiniame sąraše, kuris išlieka visą THotPDF egzemplioriaus gyvavimo laikotarpį. Jei pakartotinai naudojate tą patį egzempliorių keliems dokumentams iš eilės kviesdami LoadFromFile, ankstesnių dokumentų paveikslėlių registracijos lieka sąraše. Tokiu atveju sukurkite naują egzempliorių kiekvienam dokumentui arba išvalykite paveikslėlių sąrašą tarp dokumentų apdorojimo.
Matavimas prieš atliekant bet kokius pakeitimus
Prieš taikydami bet kurį iš šių šablonų, atlikite matavimus. „Delphi“ programinės įrangos bibliotekoje esanti TStopwatch struktūra (iš System.Diagnostics) apgaubia QueryPerformanceCounter ir yra pakankamai tiksli failų įvesties bei išvesties (I/O) profiliavimui. Pamatuokite vien LoadFromFile vykdymo laiką ir pažiūrėkite, kokią dalį bendro laiko jis užima. Jei tai sudaro 90% laiko, sprendimas yra naudoti „Direct File API“ arba sumažinti to paties failo analizavimo kartus. Jei tai užima mažiau nei 20%, problema slypi kitur.
Šio įrašo pradžioje minėtas dviejų minučių puslapių išgavimo atvejis buvo susijęs tik su pakartotinio įkėlimo šablonu. Dokumento struktūra tam įtakos neturėjo; plokščias puslapių medis būtų veikęs taip pat. Perėjimas prie vieno LoadFromFile iškvietimo ir vienos InsertPagesFromDocument operacijos sutrumpino laiką iki 1,3 sekundės toje pačioje aparatinėje įrangoje be jokių kitų pakeitimų.
Čia aprašyta puslapių manipuliavimo sąsaja (API) yra dalis HotPDF Component komponento, skirto „Delphi“ ir „C++Builder“.