Teknik Makale

HotPDF: Delphi ile Uzak PDF Akışı ve Aralık Birleştirme

HotPDF, uyguladığınız herhangi bir rastgele erişim kaynağından bir PDF yükler ve THPDFCoalescingRandomAccessSource bu kaynağı sararak ayrıştırıcının dağınık küçük okumalarını eşzamansız ön getirmeli, sınırlı bir önbelleğe alınmış blok aralıkları kümesine dönüştürür. HTTP aralık istekleriyle sunulan bir belgede bu, birkaç yüz gidiş-dönüş ile birkaç düzine arasındaki farktır

Ayrıştırıcıda hiçbir şey değişmez. Yine LoadFromRandomAccessSource'u çağırırsınız, aynı belge nesnesi geri gelir ve aynı sayfa API'si çalışır. Değişen şey altındaki trafiktir

Aynı PDF neden yerelde anında yükleniyor ama ağ üzerinden sürünüyor?

Çünkü bir PDF ayrıştırıcısı bir dosyayı okumaz, onu gezer. startxref için sona atlar, çapraz referans tablosuna geri döner, sonek sözlüğünü çözer, Catalog'a bir başvuruyu izler, ardından sayfa ağacı köküne, ardından bir sayfa düğümüne, ardından onun kaynak sözlüğüne gider. Bu adımların her biri farklı bir konumdan onlarca bayt okur

Yerel bir dosyada bu örüntü neredeyse ücretsizdir: işletim sistemi çevredeki 4 KiB sayfayı zaten önbelleğe almıştır, bu yüzden ikinci okuma bir memcpy'ye mal olur. Bir ağ taşımasında böyle bir yerellik yoktur. Her okuma kendi gecikmesine sahip bir istektir ve her biri 40 ms'lik 300 ardışık istek, neredeyse tamamen beklemekle geçen on iki saniyedir. Çözüm daha az okumak değildir; ayrıştırıcının tam olarak istediğine ihtiyacı vardır. Çözüm, her fiziksel okumayı bir sonraki mantıksal okumanın isteyeceğinin daha fazlasını kapsayacak şekilde yapmaktır

Birleştirme neyi değiştirir?

Birleştirme kaynağı, her okumayı bir bloğa yuvarlar ve bloğu önbelleğe alır. BlockSize varsayılan olarak 262.144 bayt, MaxCacheBytes ise 2.097.152'dir; bu yüzden varsayılan olarak sekiz blok bellekte tutulur ve sabit bir bayt bütçesine karşı en uzun süredir kullanılmayan sırayla tahliye edilir. Ayrıştırıcının bir sonek anahtarına yönelik 40 baytlık okuması, çevresindeki 256 KiB'ı çeker ve çapraz referans ile Catalog verisinin bulunduğu o komşuluktaki sonraki düzine okuma bellekten sunulur

Kendi kaynağınız basit kalır. GetSize ve ReadAt'ı uygulayın, taşımanız uçuşta iptal edebiliyorsa ReadAtCancellable'ı geçersiz kılın ve önbelleğe alma, birleştirme ve ön getirmeyi sarmalayıcıya bırakın

type
  THttpRangeSource = class(THPDFRandomAccessSource)
  private
    FClient: TMyHttpClient;
    FUrl: string;
    FSize: Int64;
  public
    function GetSize: Int64; override;
    function ReadAt(Offset: Int64; var Buffer; Count: Longint): Longint; override;
    function ReadAtCancellable(Offset: Int64; var Buffer; Count: Longint;
      CancellationToken: THPDFCancellationToken): Longint; override;
  end;

var
  Raw: THttpRangeSource;
  Cached: THPDFCoalescingRandomAccessSource;
  Pdf: THotPDF;
begin
  Raw := THttpRangeSource.Create('https://files.example.com/contract.pdf');
  // OwnsSource=True: sarmalayıcı Raw'ı kendisiyle birlikte serbest bırakır
  Cached := THPDFCoalescingRandomAccessSource.Create(Raw, True, 262144, 8388608);
  Pdf := THotPDF.Create(nil);
  try
    Cached.AsyncPrefetchEnabled := True;
    Cached.AdaptiveReadAheadEnabled := True;
    Cached.MaxReadAheadBlocks := 8;

    if Pdf.LoadFromRandomAccessSource(Cached, True) = 1 then
      RenderFirstPage(Pdf);
  finally
    Pdf.Free;
  end;
end;

Ne kadar ileriyi okumalı?

Uyarlanabilir ön okuma bu soruyu tahmin etmenizi gerektirmeden belge başına yanıtlar. AdaptiveReadAheadEnabled ayarlandığında, sürdürülen ileri okumalar biriktikçe pencere 1, 2, 4 ve 8 blok üzerinden büyür ve MaxReadAheadBlocks'u ya da yapılandırılmış önbellek kapasitesini asla aşmaz. Öncekinin bittiği yere yaklaşık olarak denk gelmeyen bir okuma geldiği anda pencere çöker ve ön getirme baskılanır

Varsayılan olarak 4.096 olan SequentialReadToleranceBytes, "yaklaşık olarak"ı tanımlar. Önceki okumanın bitişine bu mesafe içinde inen okumalar yine ardışık sayılır; bu önemlidir çünkü bir içerik akışını gezen bir PDF ayrıştırıcısı mükemmel bitişik konumlar üretmez, burada bir uzunluk alanını, orada satır içi bir sözlüğü atlar. Toleransı çok düşük ayarlarsanız normal bir ileri tarama rastgele sınıflandırılır ve ön okuma hiç devreye girmez. Çok yüksek ayarlarsanız gerçek rastgele erişim ardışık görünür ve kimsenin istemediği megabaytlar çekersiniz. Varsayılan, içerik akışı gezinmesi için kalibre edilmiştir ve taşımanız aksini düşünüyorsa istatistikler size söyleyecektir

Bu asimetri bilinçlidir: büyüme kademelidir, çöküş ise anındadır. Rastgele erişimli bir iş yükünde aşırı getirme, ölçülü taşımalarda gerçek bant genişliğine ve gerçek paraya mal olur; bu yüzden ucuz hata pahalı olana tercih edilir

Aktarımı gerçekten durduran iptal

Temel sınıf ReadAtCancellable'ı bildirir ve birleştirme kaynağı bunu uçtan uca onurlandırır. Uçuştaki bir ön getirmenin karşılamadığı bir aralık için bir ön plan okuması geldiğinde, ön getirme bitmesi için bırakılmak yerine iptal edilir; bu yüzden kullanıcının sayfa isteği spekülatif trafiğin arkasında kuyruğa alınmaz. THPDFRandomAccessSource'taki varsayılan uygulama sade bir ReadAt'a geri düşer; bu, özelliğin taşıma başına isteğe bağlı olduğu anlamına gelir: istek iptalini destekleyen HTTP istemcileri gerçek bir iptal alır ve daha basit kaynaklar değişmeden çalışmaya devam eder

Bunu arayüzünüz boyunca iletilen bir iptal belirteciyle birleştirin ve bir belgeyi kapatan bir kullanıcı, ağ trafiğinin boşalmasını beklemek yerine onu gerçekten durdursun. Bir istek kuyruğuyla arka plan oluşturmada anlatılan kuyruklamanın altında da aynı belirteç modeli yatar; bu yüzden tek bir belirteç görüntü alanından sokete tüm yolu kapsayabilir

Aralık önbelleği istatistiklerini okumak

GetStatistics, taşımanızın ne yaptığını önbelleğin ne yaptığından ayıran bir THPDFRangeCacheStatistics kaydını doldurur. SourceReadCount ve SourceBytesRead fiziksel trafiktir. CacheHitCount ve CacheMissCount mantıksal trafiktir. SequentialReadCount ve RandomReadCount, erişim örüntüsünün nasıl sınıflandırıldığını gösterir; CurrentReadAheadBlocks ve PeakReadAheadBlocks pencerenin ne kadar açıldığını gösterir ve PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount ile SuppressedPrefetchCount spekülasyonun karşılığını verip vermediğini gösterir

var
  S: THPDFRangeCacheStatistics;
begin
  Cached.GetStatistics(S);
  Log(Format('physical %d reads / %d bytes, hits %d, misses %d',
    [S.SourceReadCount, S.SourceBytesRead, S.CacheHitCount, S.CacheMissCount]));
  Log(Format('pattern: %d sequential, %d random, peak window %d blocks',
    [S.SequentialReadCount, S.RandomReadCount, S.PeakReadAheadBlocks]));
  Log(Format('prefetch: %d issued, %d completed, %d cancelled, %d suppressed',
    [S.PrefetchRequestCount, S.PrefetchCompletedCount,
     S.PrefetchCancelledCount, S.SuppressedPrefetchCount]));
end;

Üç okuma neyi değiştirmeniz gerektiğini söyler. Yüksek bir rastgele okuma sayısıyla birlikte çok sayıda iptal edilmiş ön getirme, belgenin sırasız erişildiği anlamına gelir; bu yüzden MaxReadAheadBlocks'u düşürün ve attığınız bant genişliği için ödeme yapmayı bırakın. Tepe pencerenin hâlâ 1'de olduğu çok sayıda kaçırma, toleransın etkin biçimde ardışık olan bir örüntüyü reddettiği anlamına gelir; bu yüzden SequentialReadToleranceBytes'ı yükseltin. Ve dosya boyutunu büyük ölçüde aşan okunan baytlar, önbelleğin çırpındığı anlamına gelir; bu yüzden başka bir şeye dokunmadan önce MaxCacheBytes'ı yükseltin

Doğrusallaştırılmış dosyalar aritmetiği değiştirir

Üreticiyi kontrol ediyorsanız, belgeyi doğrusallaştırmak sorunu optimize etmek yerine değiştirir. Doğrusallaştırılmış bir PDF, ilk sayfanın nesnelerini ve bir ipucu tablosunu dosyanın önüne yerleştirir; bu yüzden bir görüntüleyici gerisini görmeden ilk sayfayı açılış megabaytından oluşturabilir. HotPDF bu yolu GetProgressiveLinearizedLoadInfo ve ReadProgressiveLinearizedFirstPageSection üzerinden doğrudan açar; yazma tarafı ise ipucu tablolarıyla doğrusallaştırılmış PDF üretmede ele alınmıştır

Her iki teknik de birleşir. Birleştirme, herhangi bir belgeyi yavaş bir bağlantı üzerinden tolere edilebilir kılar; doğrusallaştırma ise kendi ürettiğiniz belgelerde ilk sayfanın hızlı gelmesini sağlar. Yerel bir diskte duran ama belleğe sığmayacak kadar büyük dosyalar için, doğrudan dosya API'sinde anlatılan eşlemeli dosya ve tembel akış yolları genellikle daha iyi araçtır, çünkü baştan itibaren amorti edilecek bir gidiş-dönüş gecikmesi yoktur

HotPDF, ayrıştırıcı için harici bir DLL olmadan ve tam kaynak kodu mevcut olarak Delphi ve C++Builder için yerel bir VCL PDF bileşenidir. Rastgele erişim kaynağı API'si, birleştirme sarmalayıcısı ve aşamalı yükleme giriş noktaları HotPDF Delphi PDF bileşeni sayfasında belgelenmiştir