2 GB boyutunda taranmış bir arşiv bir S3 klasöründe duruyor ve kullanıcı 900. sayfayı istiyor. PDFlibPas dosyayı indirmeden o sayfayı sunabilir: LoadFromRangeSource, kendi bayt aralığı geri çağrınız üzerinde salt okunur ve konumlanabilir bir akış kurar ve bunu TPDFDocument nesnesine devreder; böylece ayrıştırıcı yalnızca çapraz başvuru tablolarını, tek bir sayfa ağacı dalını ve tek bir içerik akışını çeker
Bu işin taşıma tarafı eski ve sıradan. HTTP sunucuları on yıllardır bayt aralıklarını destekliyor; artık RFC 9110 §14 ile tanımlı ve her nesne deposu aynı dili konuşuyor. PDF tarafı da aynı derecede oturmuş durumda: ISO 32000-1 §7.5.8, doğrusallaştırmayı tam olarak bir okuyucu ilk sayfayı dosyanın başından işleyebilsin diye tanımlar. Delphi tarafında eksik olan ortadaki parçaydı: hangi aralıkların isteneceğine, kaç tanesinin tutulacağına ve aynı isteğin iki kez yapılmamasına karar veren bölüm
LoadFromRangeSource taşıma katmanınızdan ne ister?
İki şey ve ikisi de akış değil. PDFlibPas, kesin bir SourceSize ve TPDFlibRangeReadEvent tipinde, function(Sender: TObject; Offset: Int64; Buffer: Pointer; Count: LongInt): LongInt of object biçiminde tanımlanmış eşzamanlı bir okuma geri çağrısı ister. Bu ikili dahili olarak SourceSize ve ReadRange sunan bir TCallbackByteRangeSource haline gelir ve sahipliği belgeye geçen bir akışla sarılır. Geri çağrı hedefiniz ve arka ucu size ait kalır: belge kapatma, temizleme ya da yeniden yükleme sırasında sarmalayıcıyı serbest bırakır ama yöntem işaretçisinin arkasındaki taşıma nesnesine asla dokunmaz
Sözleşme bir yönde bilerek esnek, diğer yönde katıdır. Kısa okuma geçerlidir ve yalnızca ayrıştırıcının yeniden istemesi anlamına gelir. Hata fırlatan bir geri çağrı kısa okumaya dönüştürülür ve normal yükleme hatası yolundan sonuçlanır. Count değerinden fazla bayt yazdığını iddia eden bir geri çağrı kırpılır; çünkü hatalı bir sağlayıcı önbellek arabelleğini taşıramamalıdır. Parola yeniden denemeleri, aynı geri çağrı kaynağı üzerinde yeni bir aralık akışı ve yeni bir ayrıştırma durumu kurar; böylece başarısız bir deneme geride eski konum, pencere ya da şifre çözme durumu bırakamaz
type
TObjectStoreSource = class
private
FClient: TRangeHttpClient;
FSize: Int64;
public
function ReadRange(Sender: TObject; Offset: Int64;
Buffer: Pointer; Count: LongInt): LongInt;
function IsResident(Sender: TObject; Offset: Int64;
Count: LongInt): Integer;
property Size: Int64 read FSize;
end;
function TObjectStoreSource.ReadRange(Sender: TObject; Offset: Int64;
Buffer: Pointer; Count: LongInt): LongInt;
begin
{ Range: bytes=Offset-(Offset+Count-1) ile tek engelleyici GET }
Result := FClient.FetchInto(Offset, Count, Buffer);
end;
{ ... }
Lib := TPDFlib.Create;
Src := TObjectStoreSource.Create(BucketUrl);
try
if Lib.LoadFromRangeSource(Src.Size, Src.ReadRange, '',
65536, 8 * 1024 * 1024, 2, Src.IsResident) = 1 then
Lib.SelectPage(900);
finally
Lib.Free; { sarmalayıcı akışı serbest bırakır }
Src.Free; { taşıma katmanı sizin, yaşam süresi sizin }
end;
Aralık önbelleği gerçekte ne kadar tutar?
Varsayılan olarak 4 MiB; öbek hizalı pencerelere yayılır ve LRU ile atılır. Önceki tek pencereli tasarım, çağıranın istediği uzunluğa kadar büyüyordu; bu yüzden büyük bir sıralı okuma nominal öbek boyutunu aşabiliyor, rastgele bir atlama ise önceki pencereyi hemen atıyordu. Mevcut önbellek her kaynak ofsetini ChunkSize değerine hizalar, her ıskalamada tam olarak bir öbek getirir ve birkaç pencere arasında katı bir bayt bütçesi uygular. Verdiğiniz herhangi bir açık bütçe en az bir tam öbeğe yükseltilir; böylece tek bir okuma her zaman öbek öbek ilerler ve en yüksek önbellek yükü öngörülebilir kalır. 4096 altındaki bir ChunkSize, 64 KiB varsayılanına geri döner
Yeniden okuma muhasebesi, telemetrinize bağlamaya değer bölümdür. PDFlibPas bir tekrarı hizalı öbek başlangıcıyla tanımlar ve sıralı bitişik aralıklar tutar; bu, gerçek bir ilk getirmeyi atılmadan sonraki yeniden getirmeden ayırırken kayıt tutmanın dosya boyutuyla doğrusal büyümesini engeller. GetRangeSourceCacheInfo tüm resmi JSON olarak döndürür, SetRangeSourceCacheLimit bütçeyi çalışma zamanında yeniden boyutlandırır, ClearRangeSourceCache ise pencereleri düşürür ve istatistikleri birlikte sıfırlar. Çalışma zamanında bütçeyi küçültmek geçmişi korur ve bütçe kaynaklı serbest bırakmaları atılma olarak sayar; bu yüzden yatay seyreden hits karşısında yükselen repeatedReads, çalışma kümesinin artık sığmadığının sinyalidir
var
Info: WideString;
begin
Lib.SetRangeSourceCacheLimit(16 * 1024 * 1024);
Lib.SelectPage(900);
if Lib.GetRangeSourceCacheInfo(Info) = 1 then
{ "windowCount", "cacheLimitBytes", "cachedBytes", "hits", "misses",
"evictions", "sourceReads", "sourceBytes", "repeatedReads",
"coalescedRequests", "coalescedSourceReads" }
LogRangeStats(Info);
end;
Birkaç iş parçacığı aynı öbeği istediğinde ne olur?
Tek bir isteği beklerler, birkaç isteği değil. Klasik bir TStream tek bir konum imlecine sahiptir ve her biri doğru şekilde kilitlenen iki iş parçacığı bile Seek ile Read arasında o konumun yeniden yazıldığını görebilir; bu yüzden PDFlibPas içindeki tembel nesneler ve bölümlenmiş okumalar, imleci asla oynatmayan mutlak bir ReadAt kullanır. Her hizalı öbek, o öbeği isteyen tüm çağıranların paylaştığı tek bir uçuş içi istek alır, kuyruktaki bitişik öbekler kaynak okuması başlamadan birleştirilir ve tek bir fiziksel okuma 16 MiB ile sınırlıdır; böylece paralel sayfa işi patlaması ne yinelenen küçük isteklere ne de tek bir absürt büyük isteğe dönüşür. Birleştirme penceresi varsayılan olarak 2 ms değerindedir ve yalnızca her ReadAt çağrısının ilk eksik öbeğine uygulanır; konumsal Read bunu asla beklemez ve sıfır geçmek başlangıç toplama gecikmesini tamamen kaldırır ki bu, aksi halde beklemeyi öbek öbek biriktirecek uzun sıralı taramalar için önemlidir. Konum, önbellek meta verisi ve kaynak okumaları üç ayrı kilidin arkasında durur ve kaynak geri çağrısının kendisi serileştirilmiştir; dahili iş parçacığı koruması olmayan bir veritabanı ya da nesne deposu bağdaştırıcısının değiştirilmeden kullanılabilmesini sağlayan da budur. Bekleyenler verinin kendi kopyasını alır; böylece sonraki bir LRU atılması, zaten teslim edilmiş bir arabelleği geçersiz kılamaz
900. sayfanın hazır olup olmadığını onu getirmeden sorabilir misiniz?
Evet ve isteğe bağlı kullanılabilirlik geri çağrısı tam olarak bunun içindir. Düz bir okuma geri çağrısı, çoktan inmiş baytları engelleyici bir gidiş dönüş gerektiren baytlardan ayıramaz ve deneme okumasıyla yoklama, tam da kaçınmaya çalıştığınız indirmeyi tetikler. TPDFlibRangeAvailabilityEvent yalnızca tek bir soruyu yanıtlar: tam bir aralık hemen okunabilir mi; hiçbir şey getirmesi yasaktır ve önbelleğin zaten kapsadığı baytlar her zaman kullanılabilir sayılır. GetRangeSourceDataAvailability, dolaylı nesneleri çapraz başvuru girdilerinde kayıtlı fiziksel depolama aralıklarına eşler, sıkıştırılmış nesneleri nesne akışı kapsayıcısına çözümler, kaymış bir PDF başlığını düzeltir ve bir nesneyi ancak tam aralık getirmesiz yoklamayı geçtikten sonra ayrıştırır; böylece eksik yol okuma geri çağrınızı asla çağırmaz
Gezinme kapsamlı değil sınırlıdır. Bir sayfa sorgusu, sayfa ağacının yalnızca hedef sayfayı içeren dalında yürür; ardından sayfa içeriğini, kaynakları, ek açıklamaları ve devralınan sayfa özniteliklerini eklerken Parent ve P geri kenarlarını atlar, böylece tek bir sayfa ya da widget geriye doğru tüm belgeye genişleyemez. Nesne grafiği 100000 istenen nesne ve 256 derinlikle sınırlıdır, akış nesneleri önce sözlük esasıyla ayrıştırılır ve tam ayrıştırma yedeğine yalnızca 4 MiB boyutuna kadar depolanmış nesneler için izin verilir. JSON raporu saymadan önce çakışan ve bitişik aralıkları birleştirir; bu yüzden requiredBytes ve missingBytes, end değeri dahil uç nokta olan birleştirilmiş requiredRanges ve missingRanges dizilerinden hesaplanır. Zaten kullanılabilir bir nesneyi sorgulamak aralık önbelleğini doldurabilir; eksik bir nesneyi sorgulamak ise okuma istatistiklerine dokunmaz
var
Report: WideString;
Status: Integer;
begin
Status := Lib.GetRangeSourceDataAvailability(PDF_RANGE_DATA_PAGE, 900,
Report);
if Status = PDF_RANGE_DATA_AVAILABLE then
RenderPageNow
else if Status = PDF_RANGE_DATA_NOT_AVAILABLE then
{ Report, birleştirilmiş "missingRanges" ile birlikte "missingBytes" taşır }
ShowProgress(Report)
else if Status = PDF_RANGE_DATA_NOT_PRESENT then
ShowMissingFeature; { örn. dosyada hiç AcroForm yok }
end;
Önden yükleme neden yinelemeli olmak zorunda
Çünkü geçerli missingRanges değerini bir kez okumak sayfayı kullanılabilir yapmaz. Eksik bir sayfa ağacı düğümü ya da nesne akışı, ancak geldikten sonra bir sonraki bağımlılık katmanını açığa çıkarır; bu yüzden bir PDFlibPas önden yükleme işi, sayfa, form ya da nesne grafiği tamamen kullanılabilir olana ya da bir bayt ya da geçiş sınırı onu durdurana dek sorgula, getir, yeniden sorgula döngüsü çalıştırır. İş kendi okuyucusunu ve mutlak okumaları özgün aralık akışına ileten veri kaynaklı küçük bir ikincil önbellek kullanır; bu, ayrıştırma durumunu ön plandaki TSmartPDFReader nesnesinden yalıtılmış tutarken gerçekten indirilen baytlar yine de paylaşılan ana önbelleğe düşer. Her aralık akışı için tek bir çalışan iş parçacığı vardır; bu, kaynak geri çağrısının zaten gerektirdiği serileştirmeyle eşleşir ve kuyruk önce dört öncelik düzeyine, sonra düzey içinde gönderim sırasına göre seçim yapar. MaxBytes fiziksel öbek baytlarıyla ücretlendirilir; bu yüzden önbelleğe alınmamış bir öbeğin içinde tek bir bayt isteyen ayrıştırıcı yine de tüm öbeğin bedelini öder, paylaşılan önbellekteki öbekler ise işe hiçbir maliyet getirmez. Kuyruktaki bir işi iptal etmek sıfır kaynak okumasıyla son duruma ulaşır; çalışan bir iş her bağımlılık geçişinden ve her kaynak öbeğinden önce denetlenir ve aralık akışını serbest bırakmak, uçuş içi geri çağrıyı kesmeye çalışmak yerine onun dönmesini bekler
var
Job: Integer;
Info: WideString;
begin
Job := Lib.StartRangeSourcePrefetch(PDF_RANGE_DATA_PAGE, 901,
PDF_RANGE_PREFETCH_PRIORITY_HIGH, 8 * 1024 * 1024, 65536);
if Lib.WaitForRangeSourcePrefetch(Job, 5000) =
PDF_RANGE_PREFETCH_STATE_COMPLETED then
PrepareNextPage
else
Lib.CancelRangeSourcePrefetch(Job);
{ "passes", "plannedRanges", "sourceReads", "fetchedBytes" ve son tam
kullanılabilirlik raporu; böylece LIMIT_REACHED, FAILED durumundan
ayırt edilebilir kalır }
Lib.GetRangeSourcePrefetchInfo(Job, Info);
end;
Bunun tüm dosya indirmesine dönüştüğü yer
Aralık yükleme dosya düzenine oynanan bir bahistir ve bazı dosyalar buna uymaz. ISO 32000-1 §7.5.8 uyarınca doğrusallaştırılmış bir dosya iyi durumdur: ilk sayfa bölümü açılışta ısıtılır; hem mevcut 4 MiB güvenlik eşiği hem de geçerli önbellek bütçesiyle sınırlıdır, böylece ısınma kendisinin büyük bölümünü hemen atamaz. Doğrusallaştırılmamış bir dosya yine de sondaki kuyruk ve çapraz başvuru zinciri üzerinden çözümlenir ki bu, felaket yerine birkaç ek gidiş dönüşe mal olur. Gerçek uçurum, onarım yolunu zorlayan hasarlı bir dosyadır; çünkü çapraz başvuru tablosunu yeniden kurmak, tüm belge boyunca nesne başlıklarını taramak demektir ve bu da öbek öbek gelen tam bir indirmedir. Gecikme diğer dürüst sınırdır: istek başına 60 ms sürede, kırk önbelleksiz öbek gerektiren rastgele erişimli bir ayrıştırma, önbellek ne kadar iyi olursa olsun iki saniyeden fazla süreyi yolda harcar; ileri okuma bağımsız değişkeni ve öncelik kuyruğu tam da bunu gizlemek için vardır. Aynı disiplin büyük PDF dosyalarını birleştirme ve bölmeye doğrudan erişim yaklaşımında da görülür ve bu önbellek hem paralel sayfa işlemenin hem de görüntüleyici disk sayfa önbelleğinin altında durur
Aralık kaynağı API, kullanılabilirlik sorgusu ve önden yükleme zamanlayıcısı; Delphi, C++Builder ve Free Pascal için standart PDFlibPas Delphi PDF Library paketinin parçasıdır; ürün sayfası, önden yükleme önceliği ve durum sabitleriyle birlikte LoadFromRangeSource için tam parametre başvurusunu taşır