Teknik Makale

Delphi'de PDF Nesne Grafiğini Tam Bir Kez Serbest Bırakmak

HotPDF Delphi Component, bir belge kapandığında ya da yeniden yüklendiğinde o belgenin sahip olduğu her PDF nesnesini serbest bırakır: THotPDF.CloseIndirectObjects nesne kaydını gezer, her sahiplik kenarını bir gösterici kümesine toplar, o kenarların tümünü koparır ve ancak ondan sonra her benzersiz düğümü ve her akış yükünü tam olarak bir kez serbest bırakır. Paylaşılan çocukların, sahiplik döngülerinin, yinelenen kayıtların ve sarmalayıcı/gövde takma adlarının hepsinin çift serbest bırakma olmadan ve geride hiçbir şey kalmadan yıkılmasını sağlayan bu üç aşamalı sıradır. v2.752.4'ten önce aynı rutin çok daha basit ve çok daha kötü bir şey yapıyordu: tembel dosya akışı kaynaklarını bırakıyor, IndirectObjects listesi üzerinde Clear çağırıyor, liste kabını serbest bırakıyor ve gerçek PDF nesnelerinin her birini süreç kapanışının geri kazanmasına bırakıyordu. Koddaki yorum da bu konuda dürüsttü. Nesneleri tek tek serbest bırakmak erişim ihlallerine yol açıyordu, dolayısıyla “güvenli yol” onları hiç serbest bırakmamaktı. Bu yazı, tek tek yolun gerçekten neden çöktüğünü ve elle bellek yönetimi olan bir dilde çalışan bir yıkımın nasıl göründüğünü anlatıyor

Kayıtlı her nesneyi neden öylece Free ile bırakamazsınız?

Çünkü nesne sınıflarının yıkıcıları kimin neye sahip olduğu konusunda anlaşamaz ve kayıt, aynı sahiplik zincirinin birkaç düzeyinden girdi barındırır. Dolayısıyla listeyi gezip her girdi üzerinde Free çağırmak, hangi sınıfların yan yana geldiğine bağlı olarak bazı belleği iki kez, bazı belleği ise hiç serbest bırakmaz

HPDFObjs.pas ve HPDFDoc.pas içindeki üç asimetri sorunu yaratır. THPDFDictionaryObject.Destroy, Items içeriğini gezer ve bir değeri yalnızca IsIndirect False olduğunda serbest bırakır; dolaylı çocukların kayda ait olduğu ve orada serbest bırakılacağı varsayımıyla. THPDFArrayObject.Destroy böyle bir ayrım yapmaz ve tuttuğu her öğeyi serbest bırakır. THPDFIndirectObject.Destroy ise bir nesne numarası taşıyan sarmalayıcıdır ve InternalObject gövdesini serbest bırakır. Şimdi dolaylı bir sözlüğü, o aynı sözlüğü bir gözünde listeleyen bir diziyi ve gövdesi ayrı bir kök olarak da kayıtlı bir sarmalayıcıyı tutan bir kayıt düşünün; ayrıştırıcının gerçek dosyalarda ürettiği şey tam olarak budur. Önce diziyi serbest bırakın, sözlük kayıt ona ulaşmadan yok olur. Sarmalayıcı ile gövdeyi hangi sırayla serbest bırakırsanız bırakın, ikinci çağrı askıda kalan bir gösterici üzerinde yıkıcı çalıştırır. Yalnızca sözlüğü serbest bırakın, atladığı her dolaylı çocuk sonsuza dek ayrılmış kalır. Kaydın hiçbir sıralaması bunu düzeltmez, çünkü kayıt düz bir listedir, sahiplik ilişkisi ise bir graftır ve çıkış yolu yalnızca grafiği akıl yürütmekten geçer

HotPDF kaydındaki her girdiyi serbest bırakmanın neden çöktüğü: THPDFDictionaryObject.Destroy dolaylı çocukları atlar, THPDFArrayObject.Destroy tuttuğu her şeyi serbest bırakır ve THPDFIndirectObject.Destroy InternalObject gövdesini serbest bırakır; bu yüzden tek bir düz IndirectObjects listesinde bir sarmalayıcı, bir dizi ve paylaşılan bir sözlük varsa bazı bellek iki kez ölür, bazısı hiç ölmez
Yıkıcılar kimin neye sahip olduğu konusunda anlaşamaz ve kayıt, aynı sahiplik zincirinin birkaç düzeyinden girdi tutar; bu yüzden düz bir listenin hiçbir sıralaması saf bir nesne başına Free çağrısını doğru bir yıkıma çeviremez

PDF nesne grafiğinde ne sahiplik kenarı sayılır?

Sahiplik kenarı, hedefini kaynağın yok etmekle yükümlü olduğu bir göstericidir; başka her şey bir başvurudur ve yıkım birinci türü izlemeli, ikincisini yok saymalıdır. HotPDF'te bu tam olarak dört kenar türü verir: bir THPDFDictionaryObject nesnesinin Items alanı, bir THPDFArrayObject nesnesinin Items alanı, bir THPDFIndirectObject arkasındaki InternalObject ve bir THPDFStreamObject nesnesinin iki yarısı, yani Dictionary ile Stream yükü. Başvuru türleri de en az onlar kadar önemlidir, çünkü bunlardan birini izlemek bir grafik yürüyüşünü sonsuz döngüye ya da serbest bırakılmış bellek kullanımına çevirir. Bir THPDFLink bir nesne numarası ve nesil tutar; ISO 32000-1 §7.3.10'un dolaylı başvuruyu tanımladığı biçim de budur: nesnenin kendisi değil, başka yerde yaşayan bir nesnenin adı. Bu numarayı kayıt üzerinden çözmek, zaten başka bir kenarın sahip olduğu bir düğüm verir, dolayısıyla CloseIndirectObjects linkleri hiç çözmez. Sözlüklerin ve dizilerin tuttuğu FParent geri göstericisi de aynı hikâyenin diğer yönüdür; çocuğa zaten ebeveyn sahiptir, dolayısıyla göstericiyi yukarı doğru izlemek yürüyüşün çoktan geçtiği bir düğümü yeniden ziyaret etmek olur. İkisi de el değmemiş bırakılır ve kaynaktaki yorum bunu tek satırda söyler: linkler ve ebeveyn göstericileri başvurudur, sahiplik kenarı değil

HotPDF nesne grafiğinde sahiplik kenarları ve başvurular: DictionaryObject Items, ArrayObject Items, IndirectObject InternalObject ve bir StreamObject'ün iki yarısı izlenip koparılırken, bir THPDFLink nesne numarası ve FParent geri göstericisi başka yerde yaşayan nesnelerin adlarıdır, bu yüzden CloseIndirectObjects onları hiç çözmez
Sahiplik kenarı, hedefini kaynağın yok etmesi gereken bir göstericidir; bunun yerine bir başvuruyu izlemek genişlik öncelikli yürüyüşü sonsuz döngüye ya da serbest bırakılmış bellek kullanımına çevirirdi, bu yüzden linkler ve ebeveyn göstericileri el değmemiş bırakılır

Üç aşamalı yıkım nasıl çalışır?

Birinci aşama genişlik öncelikli bir toplamadır. Rutin, iş listesini IndirectObjects içindeki her girdiyle başlatır, sonra her düğüm için o düğümün sahiplik kenarlarının hedeflerini ekler ve görülmüş olanları atlar. Görülenler kümesi, gösterici değeri üzerinden HPDFFastCacheHashInt64 ile hash'lenen ham göstericilerden oluşan açık adreslemeli bir dizidir; doğrusal yoklama kullanır ve yarıya dolduğunda GrowSeen ile iki katına çıkar. Bu yapıda düğüm başına hiçbir ayırma yapılmaz ve bir belge birkaç yüz bin nesne taşıdığında bu önemlidir. Akış yükleri ayrı bir Streams listesine gider, çünkü onlar THPDFObject düğümü değil TStream torunlarıdır ve kendi geçişlerinde serbest bırakılırlar

HotPDF'te üç aşamalı CloseIndirectObjects yıkımı: genişlik öncelikli toplama, iş listesini IndirectObjects ile başlatır ve yalnızca sahiplik kenarlarını HPDFFastCacheHashInt64 ile hash'lenen açık adreslemeli bir görülenler kümesi üzerinden izler, ikinci aşama her kenarı MarkAsFreed ve nil atamasıyla koparır ve üçüncü aşama her düğümü ve akış yükünü tam olarak bir kez serbest bırakır
Kenarları herhangi bir yıkıcı çalışmadan önce kesmek, mevcut yıkıcıları yeniden kullanıma güvenli kılan şeydir: her biri o zaman özyineleyecek bir şey bulmaz, böylece paylaşılan çocuklar, döngüler ve sarmalayıcı-gövde takma adları çift serbest bırakma olmadan yıkılır
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;   // zaten toplandı
    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;

// Birinci aşama: kayıtla başlat, sonra yalnızca sahiplik kenarlarını izle
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;

İkinci aşama, yıkıcıları çalıştırmayı güvenli kılan kısımdır: herhangi bir yıkıcı çalışmadan önce her sahiplik kenarı nil yapılır. Bir sarmalayıcı MarkAsFreed alır; bu, FInternalObject alanını temizler ve yıkıcısının ilk baktığı bayrağı set eder. Bir akış nesnesinin Dictionary ve Stream alanları nil atanır. Her sözlük öğesinin Item^.Value alanı temizlenir ve her dizi gözünün üzerine nil yazılır. Bu geçişten sonra grafta hiç kenar kalmaz, dolayısıyla üçüncü aşama Nodes içindeki her düğüm ve ardından Streams içindeki her yük üzerinde Free çağırdığında her yıkıcı özyineleyecek bir şey bulmaz ve yalnızca kendini yok eder

// İkinci aşama: hiçbir şeyi serbest bırakmadan önce her sahiplik kenarını kopar
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;

// Üçüncü aşama: her benzersiz düğüm ve yük tam olarak bir kez serbest bırakılır
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);

Bu ayrımın ne kazandırdığına bakın. İki akış nesnesinin paylaştığı bir sözlük bir kez toplanır, ikisinden de koparılır ve bir kez serbest bırakılır. Bir dizinin kendi ebeveyn sözlüğünü listelediği bir döngü sonlanır, çünkü görülenler kümesi ikinci ziyareti reddeder. İkisi de kök olarak kayıtlı bir sarmalayıcı ve gövdesi kümede iki ayrı göstericidir, dolayısıyla ikisi de serbest bırakılır ve sarmalayıcının yıkıcısı artık gövdeyi serbest bırakmaya çalışmaz, çünkü MarkAsFreed o kenarı çoktan almıştır. İki akış nesnesinin yükü olarak atanmış tek bir TMemoryStream, Streams içinde tam olarak bir kez bulunur. Bu durumların hiçbiri özel işlem gerektirmez ve modelin doğru olduğunun işareti budur

Bir sızıntıyı ayırıcı alıkoymasından nasıl ayırt edersiniz?

Bellek yöneticisinin yalnızca ayrılmış ayak izine değil, canlı ayırma sayısının iş yüküyle birlikte hareket edip etmediğine bakarak. Bir Delphi bellek yöneticisi serbest bırakılan büyük blokları yeniden kullanım için elinde tutar, dolayısıyla bir belgeyi kapattıktan sonra 400 MiB'de kalan bir süreç illa sızıntı yapmış değildir; her çalıştırmada sayfa başına canlı blok sayısı bir artan süreç ise yapmıştır. Bu düzeltmeyi tetikleyen ölçüm bilinçli olarak küçüktü: tek sayfa üreten bir THotPDF yazıcı ve sonra onu yükleyen üç okuyucu. Dördü de serbest bırakıldıktan sonra yığın raporu tam olarak dört canlı 512 KiB ayırma gösteriyordu, örnek başına bir tane; yani her birinin sahip olduğu ve hiç bırakmadığı içerik akışı yükü. Ölçeği büyütmek aynı deseni yanılmaz kıldı. Paralel render hattını iki kez çalıştırmak büyük blok ayrılmış rakamını 384 MiB'den 640 MiB'ye taşıdı; sayfa sayısıyla orantılı bu artışı ayırıcı alıkoyması açıklayamaz. Yeniden yazımdan sonra tek sayfalık tanılama, örnekler yok olduğunda sıfır büyük bayt ayrılmış ve sıfır ayrılmış alan bildirdi. Kendi sürecinizde aynı türden bir büyümeyi kovalıyorsanız, tutulan baytlarla nesne bağımlılık grafiği belge açıkken belleği hangi nesnelerin tuttuğunu söyler; bu yazı ise kapandığında onların serbest bırakma davranışıyla ilgili

Bellek eşikleri kırılgan regresyon testleri yapar, bu yüzden sevk edilen testler bunun yerine yıkıcı çağrılarını sayar. Bir fikstür patolojik grafiği elle kurar: iki akış altında paylaşılan bir sözlük, hem paylaşılan sözlüğü hem kendi kökünü içeren bir dizi, iki akışa da atanmış tek bir yük, iki kez kaydedilmiş bir kök ve gövdesi ayrıca kayıtlı bir sarmalayıcı; sonra belgeyi serbest bırakır ve benzersiz nesne başına bir yıkım olduğunu doğrular: bir yük, iki akış, iki sözlük, bir dizi, bir sarmalayıcı, bir sayı. Eski kodda üç ömür testinin hepsi sıfır yıkım bildiriyordu ki bu, “süreç kapanışına bırak”ın anlamının mümkün olan en doğrudan ifadesidir

Grafik yıkılmadan önce ne olmalı?

Graftan nesne ödünç alan her arka plan işi önce durmalı ve o nesnelerden derlenmiş görüntüleme listelerini ya da bitmap'leri tutan her önbellek düşürülmelidir, aksi hâlde bir işçi iş parçacığı ya da önbellekteki bir başvuru serbest bırakılmış belleği okur. Bu yüzden CloseIndirectObjects açılışta CancelLoadedPagePrefetch çağırır, ardından kayda dokunmadan önce render edilmiş sayfa önbelleğini geçersiz kılar. LoadFromFile ve LoadFromStream içindeki yeniden yükleme yolu ile bileşen yıkıcısı da buradan geçer, dolayısıyla bir belgeyi değiştiriyor ya da örneği elden çıkarıyor olun aynı sıra geçerlidir; bir THotPDF örneğini belgeler arasında yeniden kullanma kuralları bu garantinin üzerine yaslanır. Bu giriş bölümündeki iki ayrıntı ancak testleri çalıştırmaktan çıktı. Birincisi, yıkıcı grafiği kapatana kadar render ve görüntüleme listesi önbelleklerinin arkasındaki frekans taslaklarını çoktan elden çıkarmıştır, bu yüzden geçersiz kılma koşulsuz çağrılmak yerine o alanların nil olmamasına bağlanır. İkincisi, InvalidateRenderedPageCache, OnLoadedDocumentModified olayını -1 sayfa indeksiyle tetikleyen rutindir ve bir dosyayı yeniden yükleyen bir çağıran, eski belgenin iç yıkımı için bir düzenleme bildirimi almamalıdır. İşleyici saklanır, çağrı çevresinde nil yapılır ve bir finally içinde geri yüklenir; yeniden yükleme regresyonu da ikinci LoadFromStream sonrasında sıfır bildirim sayısı doğrular. Sessizce bir olay sözleşmesini değiştiren bir bellek düzeltmesi, tanıtımı daha iyi bir regresyondur, bu yüzden kendi doğrulamasını alır. Bir belgeye karşı paralel render hattını çalıştırıp sonra yeniden yüklerseniz, işçi havuzunun yıkımla yarışmasını engelleyen şey iptal adımıdır

Bu deseni kendi Delphi kodunuzda yeniden kullanmak

Teknik PDF'e özgü değildir. Yıkıcıların çocuklara tutarsız biçimde sahip olduğu, aynı çocuğa birden fazla ebeveynden ulaşılabildiği ya da geri ve ileri göstericilerin bir arada bulunduğu her Delphi nesne modeli, saf bir nesne başına Free altında çöker ya da sızdırır. Düzeltme her zaman aynı biçimdedir: hangi gösterici alanlarının sahiplik, hangilerinin başvuru olduğuna karar ver, sahiplik kenarlarının kapanışını yeniden ziyaretlere dayanıklı bir gösterici kümesi üzerinden topla, her kenarı kes, sonra düz listeyi yok et. İnsanların atladığı adım kesme adımıdır ve modeldeki her sınıfı yeniden yazmaya zorlamak yerine mevcut yıkıcıları yeniden kullanıma güvenli kılan da odur. Sınırları yine de açıkça söylemek gerekir. Gösterici kümesi kimlik olarak nesne adresini kullanır, dolayısıyla çoktan serbest bırakılmış ve adresi taze bir ayırmayla yeniden kullanılmış bir nesne ayırt edilemezdi; sıralama, toplama sırasında hiçbir yıkıcının çalışmamasını garanti eder ve bunu olanaksız kılan da budur. Yürüyüş yalnızca bildiği dört kenar türünü görür, dolayısıyla bir çocuğa yürüyüşün incelemediği bir alan üzerinden sahip olan yeni bir sınıf, yürüyüş ona öğretilene kadar o çocuğu sızdırır. Ve linkler izlenmek yerine kayıt üzerinden çözüldüğü için, yalnızca bir linkle başvurulan ve hiç kaydedilmemiş bir nesneye bu yıkımdan ulaşılamaz; HotPDF'te kaydı ayrıştırıcı garanti eder, ama elle kurulmuş bir grafik de aynı kurala uymalıdır

Bütün bunlar bileşenin içindedir, dolayısıyla bir uygulama için görünür etki yalnızca şudur: bir belgeyi kapatmak ya da yeniden yüklemek belleğini geri verir ve hiçbir API değişmez. HotPDF, tam kaynak koduyla birlikte Delphi ve C++Builder için yerel bir VCL PDF kütüphanesidir; API referansı ve deneme derlemesi HotPDF Delphi PDF bileşeni sayfasındadır