Techninis straipsnis

HotPDF puslapių išgavimo našumas Delphi aplinkoje

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 TStopwatchSystem.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