HotPDF Delphi Component ob zaprtju ali ponovnem nalaganju dokumenta sprosti vsak objekt PDF, ki ga dokument ima: THotPDF.CloseIndirectObjects prehodi register objektov, vsako lastniško povezavo zbere v množico kazalcev, vse te povezave odklopi in šele nato vsako posamično vozlišče ter vsako koristno obremenitev toka sprosti natanko enkrat. Prav ta trifazni vrstni red omogoča, da se deljeni otroci, lastniški cikli, podvojene registracije in aliasi ovoj-telo podrejo brez dvojnega sproščanja in ne da bi karkoli ostalo za sabo. Pred različico v2.752.4 je ista rutina počela nekaj veliko preprostejšega in veliko slabšega: sprostila je lene vire datotečnega toka, poklicala Clear na seznamu IndirectObjects, sprostila vsebnik seznama, vsak resnični objekt PDF pa pustila, da ga ob izhodu iz procesa pobere sistem. Komentar v tisti kodi je bil glede tega tudi iskren. Sproščanje objektov posamično je povzročalo kršitve dostopa, zato je bil »varen pristop« tega ne početi sploh. Ta zapis govori o tem, zakaj je posamični pristop resnično rušil, in o tem, kako je videti podiranje, ki deluje, v jeziku z ročnim upravljanjem pomnilnika
Zakaj ne morete preprosto sprostiti vsakega registriranega objekta?
Ker se destruktorji razredov objektov ne strinjajo o tem, kdo ima kaj v lasti, register pa vsebuje vnose na več ravneh iste lastniške verige. Prehod po seznamu in klic Free na vsakem vnosu zato nekaj pomnilnika sprosti dvakrat in nekaj nikoli, odvisno od tega, kateri razredi slučajno stojijo drug ob drugem
Težavo ustvarijo tri asimetrije v HPDFObjs.pas in HPDFDoc.pas. THPDFDictionaryObject.Destroy prehodi svoje Items in vrednost sprosti le, kadar je IsIndirect enak False, ob predpostavki, da posredni otroci pripadajo registru in bodo sproščeni tam. THPDFArrayObject.Destroy take razlike ne dela in sprosti vsak element, ki ga ima. THPDFIndirectObject.Destroy, ovoj, ki nosi številko objekta, pa sprosti svoje telo InternalObject. Predstavljajte si register, v katerem so posredni slovar, niz, ki ta isti slovar navaja v enem od svojih mest, in ovoj, katerega telo je prav tako registrirano kot ločen koren, kar je natanko to, kar razčlenjevalnik izdela na resničnih datotekah. Sprostite najprej niz in slovarja ni več, preden register pride do njega. Sprostite ovoj in telo, v katerem koli vrstnem redu, in drugi klic požene destruktor na visečem kazalcu. Sprostite slovar sam in vsak posredni otrok, ki ga je izpustil, ostane alociran za vedno. Noben vrstni red registra tega ne popravi, ker je register raven seznam, lastniško razmerje pa je graf, in razmišljanje o grafu je edini izhod
Kaj šteje kot lastniška povezava v grafu objektov PDF?
Lastniška povezava je kazalec, katerega cilj je izvorni objekt dolžan uničiti; referenca je vse drugo in podiranje mora slediti prvi vrsti ter drugo prezreti. V HotPDF to da natanko štiri vrste povezav: Items razreda THPDFDictionaryObject, Items razreda THPDFArrayObject, InternalObject za razredom THPDFIndirectObject in obe polovici THPDFStreamObject, njegov Dictionary in njegovo koristno obremenitev Stream. Vrste referenc štejejo prav toliko, ker sledenje eni spremeni prehod po grafu v neskončno zanko ali v uporabo po sprostitvi. THPDFLink nosi številko objekta in generacijo, kar je način, kako ISO 32000-1 §7.3.10 definira posredni sklic: ime za objekt, ki živi drugje, in ne objekt sam. Razrešitev te številke skozi register da vozlišče, ki ga neka druga povezava že ima v lasti, zato CloseIndirectObjects povezav nikoli ne dereferencira. Kazalec nazaj FParent, ki ga hranijo slovarji in nizi, je ista zgodba v drugo smer; nadrejeni otroka že ima v lasti, zato bi sledenje kazalcu navzgor le znova obiskalo vozlišče, skozi katero je prehod že šel. Oba ostaneta pri miru in komentar v izvorni kodi to pove v eni vrstici: povezave in kazalci na nadrejene so reference, ne lastniške povezave
Kako deluje trifazno podiranje?
Prva faza je zbiranje v širino. Rutina delovni seznam zasadi z vsakim vnosom IndirectObjects, nato pa za vsako vozlišče doda cilje njegovih lastniških povezav in izpusti vse, kar je bilo že videno. Množica videnih je niz z odprtim naslavljanjem surovih kazalcev, zgoščenih s HPDFFastCacheHashInt64 po vrednosti kazalca, z linearnim preiskovanjem in podvojitvijo GrowSeen, ko doseže polno napolnjenost. Nič v tej strukturi ne alocira na vozlišče, kar šteje, kadar dokument nosi nekaj sto tisoč objektov. Koristne obremenitve tokov gredo na ločen seznam Streams, ker so potomci TStream in ne vozlišča THPDFObject, ter se sprostijo v svojem lastnem prehodu
procedure Collect(Value: TObject; Payload: boolean);
var
Slot: Integer;
begin
if Value = nil then Exit;
if (SeenCount + 1) * 2 >= Length(Seen) then GrowSeen;
Slot := PointerSlot(Pointer(Value), Length(Seen));
while Seen[Slot] <> nil do
begin
if Seen[Slot] = Pointer(Value) then Exit; // že zbrano
Slot := (Slot + 1) and (Length(Seen) - 1);
end;
Seen[Slot] := Pointer(Value);
Inc(SeenCount);
if Payload then Streams.Add(Value) else Nodes.Add(Value);
end;
// Prva faza: zasadi z registrom, nato sledi le lastniškim povezavam
for I := 0 to IndirectObjects.Count - 1 do
Collect(TObject(IndirectObjects[I]), False);
I := 0;
while I < Nodes.Count do
begin
Obj := THPDFObject(Nodes[I]);
if Obj is THPDFIndirectObject then
Collect(THPDFIndirectObject(Obj).InternalObject, False)
else if Obj is THPDFStreamObject then
begin
Collect(THPDFStreamObject(Obj).Dictionary, False);
Collect(THPDFStreamObject(Obj).Stream, True);
end
else if Obj is THPDFDictionaryObject then
for J := 0 to THPDFDictionaryObject(Obj).Items.Count - 1 do
Collect(PHPDFDictionaryItem(THPDFDictionaryObject(Obj).Items[J])^.Value, False)
else if Obj is THPDFArrayObject then
for J := 0 to THPDFArrayObject(Obj).Items.Count - 1 do
Collect(TObject(THPDFArrayObject(Obj).Items[J]), False);
Inc(I);
end;
Druga faza je tisti del, ki naredi destruktorje varne za izvajanje: vsaka lastniška povezava se nastavi na nil, preden se izvede kateri koli destruktor. Ovoj dobi MarkAsFreed, ki počisti FInternalObject in nastavi zastavico, ki jo njegov destruktor preveri najprej. Objekt toka ima Dictionary in Stream dodeljena na nil. Vsak element slovarja ima počiščen Item^.Value, vsako mesto v nizu pa prepisano z nil. Po tem prehodu grafu ne ostane nobena povezava več, zato ob tretji fazi, ko se Free pokliče na vsakem vozlišču v Nodes in nato na vsaki obremenitvi v Streams, vsak destruktor ne najde ničesar, v kar bi se spustil, in uniči le sebe
// Druga faza: odklopi vsako lastniško povezavo, preden karkoli sprostiš
for I := 0 to Nodes.Count - 1 do
begin
Obj := THPDFObject(Nodes[I]);
if Obj is THPDFIndirectObject then
THPDFIndirectObject(Obj).MarkAsFreed
else if Obj is THPDFStreamObject then
begin
THPDFStreamObject(Obj).Dictionary := nil;
THPDFStreamObject(Obj).Stream := nil;
end
else if Obj is THPDFDictionaryObject then
for J := 0 to THPDFDictionaryObject(Obj).Items.Count - 1 do
PHPDFDictionaryItem(THPDFDictionaryObject(Obj).Items[J])^.Value := nil
else if Obj is THPDFArrayObject then
for J := 0 to THPDFArrayObject(Obj).Items.Count - 1 do
THPDFArrayObject(Obj).Items[J] := nil;
end;
// Tretja faza: vsako posamično vozlišče in obremenitev se sprosti natanko enkrat
IndirectObjects.Clear;
for I := 0 to Nodes.Count - 1 do TObject(Nodes[I]).Free;
for I := 0 to Streams.Count - 1 do TObject(Streams[I]).Free;
FreeAndNil(IndirectObjects);
Poglejte, kaj ta razdelitev prinese. Slovar, ki si ga delita dva objekta toka, se zbere enkrat, odklopi od obeh in sprosti enkrat. Cikel, v katerem niz navaja svoj nadrejeni slovar, se zaključi, ker množica videnih zavrne drugi obisk. Ovoj in njegovo telo, oba registrirana kot korena, sta dva različna kazalca v množici, zato se sprostita oba, destruktor ovoja pa telesa ne poskuša več sprostiti, ker je MarkAsFreed to povezavo že odvzel. En sam TMemoryStream, dodeljen kot obremenitev dveh objektov toka, sedi na seznamu Streams natanko enkrat. Noben od teh primerov ne potrebuje posebne obravnave, in to je znak, da je model pravi
Kako ločite uhajanje od zadrževanja pri alokatorju?
Tako da preverite, ali se število živih alokacij upravljalnika pomnilnika premika skupaj z obremenitvijo, in ne le njegov rezervirani odtis. Upravljalnik pomnilnika Delphija sproščene velike bloke obdrži za ponovno uporabo, zato proces, ki po zaprtju dokumenta ostane pri 400 MiB, ni nujno puščal; proces, pri katerem število živih blokov na vsak zagon zraste za enega na stran, pa je. Sonda, ki je poganjala ta popravek, je bila namenoma majhna: en pisec THotPDF, ki izdela eno stran, nato trije bralniki, ki jo naložijo. Ko so bili vsi štirje sproščeni, je poročilo o kopici pokazalo natanko štiri žive alokacije po 512 KiB, eno na instanco, kar je koristna obremenitev toka vsebine, ki jo je vsaka imela in je ni nikoli sprostila. Povečanje je isti vzorec naredilo nedvoumnega. Dvakratni zagon vzporedne verige upodabljanja je številko dodeljenih velikih blokov premaknil s 384 MiB na 640 MiB, kar je porast sorazmeren s številom strani, ki ga zadrževanje pri alokatorju ne more pojasniti. Po prepisu je diagnoza z eno stranjo po izginotju instanc poročala nič dodeljenih velikih bajtov in nič rezerviranih. Če v svojem procesu lovite podobno rast, vam graf odvisnosti objektov z zadržanimi bajti pove, kateri objekti držijo pomnilnik, dokler je dokument odprt; ta zapis pa govori o tem, kako se sprostijo, ko se zapre
Pomnilniški pragovi naredijo regresijske teste krhke, zato izdani testi raje štejejo klice destruktorjev. Fiksna datoteka ročno zgradi patološki graf, z deljenim slovarjem pod dvema tokovoma, nizom, ki vsebuje tako deljeni slovar kot svoj lastni koren, eno obremenitvijo, dodeljeno obema tokovoma, dvakrat registriranim korenom in ovojem, katerega telo je registrirano ločeno, nato pa sprosti dokument in zahteva eno uničenje na vsak posamični objekt: eno obremenitev, dva toka, dva slovarja, en niz, en ovoj in eno število. Ob stari kodi so vsi trije testi življenjske dobe poročali nič uničenj, kar je najbolj neposredna možna izjava o tem, kaj pomeni »pusti za izhod iz procesa«
Kaj se mora zgoditi, preden se graf podre?
Vsako delo v ozadju, ki si objekte iz grafa izposoja, se mora najprej ustaviti, in vsak predpomnilnik, ki hrani sezname za prikaz ali bitne slike, izdelane iz teh objektov, se mora opustiti, sicer delovna nit ali predpomnjeni sklic bere sproščen pomnilnik. CloseIndirectObjects zato začne s CancelLoadedPagePrefetch in nato razveljavi predpomnilnik upodobljenih strani, preden se dotakne registra. Pot ponovnega nalaganja v LoadFromFile in LoadFromStream ter destruktor komponente gredo skozi to, zato isti vrstni red velja, ne glede na to, ali dokument zamenjujete ali instanco odstranjujete; pravila za ponovno uporabo enega THotPDF skozi več dokumentov se na to jamstvo opirajo. Dve podrobnosti iz tega uvoda sta se pokazali šele ob zagonu testov. Prvič, destruktor je frekvenčne skice za predpomnilnikoma upodabljanja in seznama za prikaz že odstranil, ko zapre graf, zato je razveljavitev varovana s tem, da ta polja niso nil, in se ne kliče brezpogojno. Drugič, InvalidateRenderedPageCache je rutina, ki sproži OnLoadedDocumentModified z indeksom strani -1, klicatelj, ki datoteko znova naloži, pa ne bi smel prejeti obvestila o urejanju zaradi notranjega podiranja starega dokumenta. Obravnavalnik se shrani, med klicem nastavi na nil in obnovi v bloku finally, regresija ponovnega nalaganja pa zahteva nič obvestil po drugem LoadFromStream. Popravek pomnilnika, ki tiho spremeni pogodbo dogodkov, je regresija z boljšim odnosom z javnostjo, zato dobi svojo trditev. Če nad dokumentom poženete vzporedno verigo upodabljanja in ga nato znova naložite, je prav korak preklica tisti, ki delovni nabor zadrži pred tekmo s podiranjem
Ponovna uporaba vzorca v vaši kodi Delphi
Tehnika ni značilna za PDF. Vsak objektni model v Delphiju, kjer destruktorji nedosledno posedujejo otroke, kjer je mogoče do istega otroka priti iz več nadrejenih ali kjer sobivajo kazalci nazaj in kazalci naprej, se ob naivnem Free po posameznih objektih zruši ali pušča. Popravek je vedno iste oblike: določite, katera polja kazalcev so lastniška in katera so reference, zberite zaprtje lastniških povezav skozi množico kazalcev, ki prenaša ponovne obiske, prerežite vsako povezavo in nato uničite ravni seznam. Korak prerezovanja je tisti, ki ga ljudje izpustijo, in prav on naredi obstoječe destruktorje varne za ponovno uporabo, namesto da bi zahteval prepis vsakega razreda v modelu. Meje pa velja povedati naravnost. Množica kazalcev kot identiteto uporablja naslov objekta, zato bi bil objekt, ki je bil že sproščen in katerega naslov je nova alokacija uporabila znova, nerazločljiv; vrstni red zagotavlja, da med zbiranjem ne steče noben destruktor, in prav to to izključi. Prehod vidi le štiri vrste povezav, ki jih pozna, zato bo nov razred, ki ima otroka v lasti prek polja, ki ga prehod ne pregleda, tega otroka puščal, dokler prehoda tega ne naučimo. In ker se povezave razrešujejo skozi register in jim ne sledimo, objekt, na katerega kaže le povezava in ki ni bil nikoli registriran, za to podiranje sploh ni dosegljiv; v HotPDF registracijo zagotavlja razčlenjevalnik, ročno zgrajen graf pa mora spoštovati isto pravilo
Vse to je znotraj komponente, zato je viden učinek za aplikacijo preprosto ta, da zaprtje ali ponovno nalaganje dokumenta vrne njegov pomnilnik, brez spremembe API. HotPDF je izvorna knjižnica PDF za VCL za Delphi in C++Builder s polno izvorno kodo; referenca API in poskusna različica sta na strani komponente HotPDF Delphi PDF