HotPDF gengiver mange PDF-sider samtidigt via to indgangspunkter: RenderLoadedPagesParallel, som returnerer et array af bitmaps, og RenderLoadedPagesParallelOrdered, som leverer hver side til et callback i inputrækkefølge, så snart den er klar. Den ordnede form er den, der skalerer, fordi den aldrig holder mere end et begrænset antal bitmaps i hukommelsen, uanset hvor mange sider du bad om
Denne skelnen er hele artiklens pointe. At gengive 10.000 sider ved 300 DPI ind i et array betyder 10.000 levende bitmaps, hvilket ikke er et gennemløbsproblem, men et out-of-memory-nedbrud. At gengive dem via et ordnet callback med en kødybde på to betyder to levende bitmaps, og jobbet kører med et fast fodaftryk, uanset hvor langt dokumentet er
Hvorfor kræver parallel gengivelse to trin?
At gengive en PDF-side er reelt to forskellige jobs under ét navn. Først tokeniseres og kompileres content stream'en til en display list, hvilket er CPU-bundet og rører ved delte caches. Dernæst afspilles display listen ind i et bitmap, hvilket også er CPU-bundet, men allokerer kraftigt. At behandle dem som én udelelig enhed tvinger et valg mellem at serialisere den cache-berørende halvdel og at duplikere arbejde
HotPDF opdeler dem. Låsen på display list-cachen beskytter kun frekvensoptællingen, LRU-rækkefølgen, brugstællere og optagelsesbeslutninger; selve kompileringen kører uden for låsen. Efter kompilering forespørger workeren cachen igen: hvis en anden worker i mellemtiden har publiceret den samme side, frigiver den sin egen kopi og tager en brugstæller på den publicerede post. Det holder duplikeret arbejde begrænset og forhindrer to poster for den samme side, uden nogensinde at serialisere kompilering på tværs af sider
Den målte effekt på en kompleks 32-siders testfixture er det, der er værd at huske. Den samlede tid på Win64 faldt fra 1.565 ms med én worker til 696 ms med fire, cirka en faktor 2,25. Tiden til det første resultat faldt fra 1.255 ms til 63 ms, omkring 95 procent, fordi workere begynder at afspille færdige display lists, mens andre stadig kompilerer, i stedet for at vente ved en fuld kompileringsbarriere
Ordnet levering og den reserverede plads
Det ordnede API garanterer, at dit callback ser sider i den rækkefølge, du bad om dem, mens workere bliver færdige i hvilken rækkefølge de nu bliver færdige. Færdige bitmaps publiceres i en output-plads indekseret efter inputposition, og callbacket kører serielt på den kaldende tråd
To regler gør dette begrænset og ikke bare ordnet. Output-køens optælling inkluderer bitmaps, der aktuelt bruges af callbacket, så et langsomt callback ikke kan lade producenter glide forbi kødybden. Og senere sider må højst optage queueDepth - 1 pladser, fordi én plads permanent er reserveret til den næste side, der skal leveres. Uden den reservation kan en langsom første side blive låst ude af færdige senere sider, der fylder køen, og pipelinen går i dødvande foran i køen, mens alle workere er inaktive
uses
HPDFDoc;
procedure TExportJob.PageReady(Sender: TObject; InputIndex,
PageIndex: Integer; Bitmap: TBitmap; var Cancel: boolean);
begin
// Bitmappet er lånt: kun gyldigt for dette kald. Forbrug det her
// (skriv til disk, kod, hash), og behold ikke referencen
Bitmap.SaveToFile(Format('page-%.4d.bmp', [InputIndex + 1]));
Cancel := FUserCancelled;
end;
procedure TExportJob.Run;
var
Pdf: THotPDF;
Pages: array of Integer;
Info: THPDFParallelRenderPipelineInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('manual.pdf') <= 0 then
Exit;
SetLength(Pages, Pdf.LoadedPageCount);
for I := 0 to High(Pages) do
Pages[I] := I;
Pdf.RenderLoadedPagesParallelOrdered(Pages, 200, 4, 2,
PageReady, Info);
Writeln(Format('workers=%d peak queued=%d peak bytes=%d',
[Info.WorkerCount, Info.PeakQueuedPageCount, Info.PeakQueuedBytes]));
Writeln(Format('first result=%d ms total=%d ms backpressure waits=%d',
[Info.FirstResultMilliseconds, Info.TotalMilliseconds,
Info.BackpressureWaitCount]));
finally
Pdf.Free;
end;
end;
Kontrakten om det lånte bitmap er grunden til, at fodaftrykket forbliver fladt. Biblioteket frigiver bitmappet, så snart dit callback returnerer, så en 10.000-siders testfixture ved kødybde 2 topper på præcis to bitmaps på både Win32 og Win64. Hvis du har brug for bitmappet efter callbacket, skal du kopiere det, og så ejer du selv hukommelsen og regnskabet
Sådan vælger hukommelsesbudgettet et antal workere
Flere workere er ikke altid hurtigere og er ofte fatalt. Seks workere, der gengiver A3-sider ved 600 DPI, har brug for flere hundrede megabyte levende bitmap uanset hvor mange kerner der er til rådighed, og maskinen med otte kerner har måske ikke så meget ledigt adresserum
HotPDF normaliserer derfor det ønskede antal workere mod anmodningens størrelse, sideantallet og et hårdt loft, og dividerer så det aktuelt tilgængelige globale budget med det største konservative enkeltside-estimat i anmodningen. Mindst én worker overlever altid, så en overdimensioneret side stadig gengives, eksklusivt. I referencemålingen kørte et seks-siders 600 DPI-job under et budget på 512 MiB med tre workere i stedet for seks, med en reservationstop på 504.583.296 bytes
Hver worker tager en hukommelsesreservation, efter den har taget en side, og før den kompilerer, og reservationen dækker kompilering og afspilning samlet. Kun at reservere omkring rasterfasen ville overse display listen og content stream'ens arbejdssæt, hvilket på grafiktunge sider er den største halvdel. De to API'er adskiller sig herefter: array-formen frigiver sin reservation, så snart bitmappet er færdigt, fordi den kaldende part ejer resultatet, og planlæggeren ikke længere kan holde regnskab med det, mens den ordnede form overfører reservationen sammen med bitmappet ind i output-pladsen og holder den, indtil callbacket returnerer. Det er endnu en grund til at foretrække det ordnede API, når outputmængden er stor
Annullering uden dødvande
At sætte Cancel i callbacket stopper nyt arbejde og vækker enhver worker, der venter på backpressure. Sider, der allerede er inde i rendereren, bliver færdige og ryddet op i stedet for forladt, og kun den første fejldiagnose bevares, som kastes, efter at alle workere er sluttet sig sammen igen
Rækkefølgen her er bærende. Annulleringstilstand sættes først, upublicerede output-reservationer frigives dernæst, og først derefter sluttes workerne sammen igen. Ombytning af de sidste to trin skaber en lukket løkke: en worker venter på adgang til et budget, der ikke frigives, før forbrugeren er færdig, mens forbrugeren venter på, at den worker afslutter
Interaktive applikationer bør også kende til foreground-preemption. Sideforudhentning kører på sine egne annulleringstokens, og foreground-gengivelseskaldene annullerer og slutter sig til prefetch-workerne, før de udfører deres eget arbejde, så baggrundsgennemløb aldrig lægger latenstid til den side, en bruger venter på. Annullering kontrolleres med intervaller begrænset til 4 KiB byte-scanning eller 256 tokens, selvom et atomart codec- eller OS-kald ikke kan afbrydes midt i flyvningen — responsivitetsgarantien dækker bibliotekets egne løkker, ikke tredjeparts-dekodere. Den kø-baserede tilgang til interaktiv gengivelse er beskrevet i baggrundsgengivelse med en anmodningskø
Aflæsning af pipeline-statistikken
THPDFParallelRenderPipelineInfo adskiller tal, der let sammenblandes. RequestedWorkerCount sammenholdt med WorkerCount og MemoryBudgetWorkerLimit fortæller dig, om budgettet reducerede din samtidighed. CompiledPageCount sammenholdt med CacheHitPageCount fortæller dig, hvor meget display list-cachen sparede. CompiledPagesAtFirstResult viser, om pipelinen reelt overlappede kompilering og afspilning, eller om den degenererede til en barriere
Til finjustering er de to mest nyttige BackpressureWaitCount og MemorySchedulerWaitMilliseconds. Høje backpressure-ventetider betyder, at dit callback er flaskehalsen, så enten gør det hurtigere eller hæv kødybden, hvis hukommelsen tillader det. Høje scheduler-ventetider betyder, at budgettet er flaskehalsen, så reducér DPI, reducér samtidighed, eller hæv budgettet. At hæve antallet af workere i begge tilfælde gør gennemløbet dårligere, hvilket er den kontraintuitive del
Et praktisk udgangspunkt for batch-eksport: samtidighed lig med fysiske kerner minus én, kødybde to eller tre, og DPI valgt ud fra det faktiske outputkrav frem for vanetænkning. Aflæs derefter statistikken fra et rigtigt dokument og justér én gang i stedet for at gætte to gange. For jobs, hvor selve dokumentet er for stort til at holde i hukommelsen, kombinér dette med streaming-adgangsmodellen i det direkte fil-API til store PDF-workflows
Parallel gengivelse, forudhentning og display list-cachen er alle en del af den samme gengivelsesstak til Delphi og C++Builder; den komplette funktionsliste findes på HotPDF Delphi PDF component-siden