Teknik Makale

Delphi'de Bellek Eşlemeli PDF Okuma: Kayan Pencere

PDFlibPas, yerel bir PDF'i sınırlı bir salt okunur memory-mapped görünüm üzerinden açabilir: LoadFromMappedFile ve DAOpenMappedFile dosya üzerinde tam olarak tek bir kayan pencere tutar, gerektiğinde yeniden eşler ve her nesne dilimini mutlak ofsetli okumalarla sunar. Delphi PDF kütüphanesi kaynak dosyanın tamamını bellekte tutmadığı için dosya büyüdükçe adres alanı kullanımı sabit kalır. Tasarım tek bir iş yükü için vardır: ayrıştırıcı yüklemeyi bitirdiği halde nesne nesne, akış parçası akış parçası diske dönmeye devam eden gigabaytlık PDF'ler

PDF yüklendikten sonra seyrek okumalar neden hâlâ pahalı?

Bir PDF'i yüklemek okumayı bitirmez ve çok gigabaytlı bir dosyada zamanın gittiği yer bu aralıktır. Bir çapraz başvuru tablosu veya çapraz başvuru akışı (ISO 32000-1 §7.5.4 ve §7.5.8) yalnızca her dolaylı nesnenin nerede başladığını kaydeder. Baytlar daha sonra, bir sayfa render edildiğinde, bir font programı çözüldüğünde veya gömülü dosya akışı (ISO 32000-1 §7.11.4) çıkarıldığında gelir. On binlerce nesne içeren 2 GB'lık bir arşiv, on binlerce küçük ve sırasız okumaya dönüşür ve bunların hiçbiri yükleme anında bilinmez

Bu okumaların eskiden izlediği yol, tek bir konumlu akışta paylaşılan bir Seek ardından Read çağrısıydı ve iki yönden birden sorunluydu. Sayfa işletim sisteminin önbelleğinde zaten yerleşik olsa bile her parça bir dosya okuması için ödeme yapar; ayrıca imleç paylaşılan değişebilir durumdur, bu nedenle yerel dosya ile önceden getirmeli aşamalı PDF aralık yüklemesinin arkasındaki bayt aralığı kaynağı konum üzerinde yarışmadan aynı ayrıştırıcı kodunu çalıştıramazdı. PDFlibPas mutlak ofsetli okumayı bir optimizasyondan sözleşmeye yükselterek ikisini de düzeltir

TPDFReadAtStream neyi garanti eder?

TPDFReadAtStream, mantıksal akış imlecine bağlı olmayan ve onu bozmayan, mutlak ofsette okuma garantisi verir. Tam olarak tek bir virtual metoda sahip soyut bir TStream alt sınıfıdır ve kütüphanedeki imleçten bağımsız iki kaynak da ondan türemiştir: yerel dosyalar için TReadOnlyMappedFileStream, aralıkla sunulan uzak dosyalar için TByteRangeStream. Nesne dilimi okuyucusu kaynağının bir TPDFReadAtStream olup olmadığını bir kez sorar; değilse eski seek-then-read dizisine geri döner, böylece sıradan bir dosya akışı veya bellek akışı değişmeden çalışmaya devam eder

type
  // Mutlak okumaları paylaşılan Seek ve Read'den kaçınan salt okunur akışlar
  TPDFReadAtStream = class(TStream)
  public
    function ReadAt(Offset: Int64; var Buffer;
      Count: LongInt): LongInt; virtual; abstract;
  end;

  // Tek bir yerel dosyaya pencereli salt okunur erişim
  TReadOnlyMappedFileStream = class(TPDFReadAtStream)
  private
    FMemoryMapped: Boolean;
  public
    constructor Create(const FileName: WideString; WindowSize: Int64 = 0);
    function GetStats: TPDFMappedFileStats;
    function ReadAt(Offset: Int64; var Buffer;
      Count: LongInt): LongInt; override;
    property MemoryMapped: Boolean read FMemoryMapped;
  end;

İmzanın düşündürdüğünden daha önemli olan ayrım budur. ReadAt kendisine verilen ofseti kullanır ve Position değerini tam olarak olduğu yerde bırakır; iç içe ayrıştırıcı düzeylerinin her çağrının çevresinde konumu kaydetme ve geri yükleme dansı yapmadan okuma yapmasını sağlayan şey budur. TReadOnlyMappedFileStream yine her TStream gibi Read, Seek ve Size uygular, Seek mantıksal konumu dosya içine sıkıştırır ve kaynak salt okunur açıldığı için Write her zaman 0 döndürür

Delphi'de eşlenmiş görünüm üzerinden PDF açma

Eşlenmiş bir kaynağı açan iki açık giriş noktası vardır ve ikisi de zaten kullandığınız giriş noktalarının davranışını değiştirmez. LoadFromMappedFile bir belgeyi yükler ve seçer; DAOpenMappedFile aynı dosya üzerinde bir Direct Access tutamacı döndürür, bu da gigabaytlık PDF'leri Direct Access üzerinden birleştirip bölmek istediğiniz kipte kullanılır. LoadFromFile ve DAOpenFile dosya paylaşımı, hata ve uyumluluk semantiklerini korur; dolayısıyla tercih etmeyen çağıranların altında hiçbir şey değişmez. Her iki eşlenmiş giriş noktası da istenen WindowSize değerini bayt cinsinden ve bir Options bit maskesini alır; ikisi için de 0 kabul edilir

var
  Pdf: TPDFlib;
  Payload: AnsiString;
  Info: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    // WindowSize 0, 64 MiB varsayılanını seçer; eşleme burada zorunludur
    if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
      PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
      raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
        [Pdf.LastErrorCode]);

    // Ertelenmiş çıkarma artık seek yerine eşlenmiş pencereler üzerinde ilerler
    Payload := Pdf.GetEmbeddedFileContentToString(1);
    if Pdf.GetMappedFileInfo(Info) = 1 then
      Writeln(Info);
  finally
    Pdf.Free;
  end;
end;

PDF_MAPPED_FILE_REQUIRE_MAPPING gerçekte neyi zorlar?

PDF_MAPPED_FILE_REQUIRE_MAPPING, sessiz geri dönüşü açılış zamanında hemen teşhis edilebilir bir hataya çevirir. Options 0 bırakıldığında her iki giriş noktası salt okunur dosya akışı geri dönüşünü kabul eder: platformda eşleme kodu yoksa veya eşleme çağrısı başarısız olursa belge yine açılır ve her okuma normal dosya akışından geçer. Bayrak ayarlandığında PDFlibPas girdiyi yalnızca ilk görünüm kurulmuşsa kabul eder ve sessizce eski yolla aynı şekilde çalışan bir belgeyi yüklemek yerine reddi LastErrorCode 401 üzerinden bildirir

Windows'ta eşlenmiş akış, FILE_SHARE_READ, FILE_SHARE_WRITE ve FILE_SHARE_DELETE ile FILE_FLAG_RANDOM_ACCESS bayrağını kullanarak ikinci bir salt okunur tutamaç açar, bunun üzerinde bir PAGE_READONLY eşlemesi oluşturur ve ilk pencereyi constructor içinde eşler. Eşlemeyi erkenden yapmak bütün amacın kendisidir: "mapping required" hatası ilk tembel nesne okumasında, render işinin ortasında değil, LoadFromMappedFile çağrısında görünür. Yine de garantinin nerede bittiğini bilin. Eşleme kodu yalnızca Windows hedefleri için derlenir ve sıfır baytlık bir dosya hiç eşleme denemez; dolayısıyla PDF_MAPPED_FILE_REQUIRE_MAPPING, taşınabilir bir söz değil, meşru biçimde başarısız olabilecek bir istektir. Negatif bir WindowSize veya belgelenen tek değer dışındaki herhangi bir Options biti aynı hata 401 ile doğrudan reddedilir

Tek pencere, allocation granularity'ye göre yeniden eşlenir

Adres alanı kullanımını dosya boyutundan bağımsız tutan şey, her zaman yalnızca tek görünümün tutulmasıdır. WindowSize 0 değeri 64 MiB seçer; sistem allocation granularity'sinden küçük bir değer ona yükseltilir; 1 GiB'den büyük bir değer sınırlandırılır ve sonuç granularity birimlerinin tam sayısına yukarı yuvarlanır; Windows'ta bu değer 65536 bayttır, ancak GetSystemInfo farklı bir dwAllocationGranularity bildirirse o kullanılır. Bir okuma mevcut görünümün dışına çıktığında PDFlibPas görünümü kaldırır, istenen ofseti bir granularity sınırına aşağı hizalar ve orada yeni bir pencere eşler. Son pencere fiziksel dosya boyutuna sıkıştırılır; böylece görünüm dosyanın sonunu aşmaz

Tek bir okuma istediği kadar pencereyi aşabilir: döngü mevcut görünümün sağlayabildiği şeyi kopyalar, yeniden eşler ve devam eder; dosya sonuna taşan bir istek hata vermek yerine kısa bir sayı döndürür. PDFlibPas'ın özellikle yapmadığı şey, size görünüm içine bir pointer vermektir; çünkü bir sonraki pencereler arası okuma onu geçersiz kılar ve hiçbir çağıran buna makul biçimde karşı koyamaz. Eşlenmiş baytlar doğrudan ayrıştırıcının sahibi olduğu hedef buffer'lara kopyalanır; bu, ek dosya giriş buffer'ını ve konum değiştirmeyi ortadan kaldırır, ancak kütüphane son ayrıştırıcı depolaması için zero-copy iddiasında bulunmaz. Okuma tarafındaki pencereleme yazma tarafıyla da birleşir; hızlı PDF birleştirme sırasında bayt düzeyinde başvuru kaydırma nesne baytlarını dışarı akıtırken eşlenmiş kaynak onları içeri akıtır. Pencere boyutu ödünü açıktır: daha küçük pencere daha az adres alanı tutar ve daha sık yeniden eşlenir; bu da genellikle 32 bitlik bir süreç içinde doğru tercihtir

Kilit neyi korur ve GetMappedFileInfo ne bildirir?

Tek bir critical section eşlenmiş görünümü, geri dönüş dosya imlecini, mantıksal konumu ve istatistikleri kapsar; iki okuma metodunun ayrımı da doğrudan bundan çıkar. ReadAt kilidi alır ve lock-free dahili okuyucuyu çağırır; Read aynı kilidi alır, aynı dahili okuyucuyu mevcut mantıksal konumda çağırır ve ardından konumu ilerletir. Public ReadAt yerine dahili işlevi yeniden kullanmak recursive locking'i önler; kilidi tüm kopyalama döngüsü boyunca tutmak ise eşzamanlı çağrılar altında tek pencerenin yeniden eşlenmesini doğru tutar. Free Pascal'a port etmeden önce bilinmesi gereken bir ayrıntı var: FPC Windows birimi kendi TCriticalSection adlı record'unu bildirir, bu nedenle alan ve oluşturulması SyncObjs.TCriticalSection olarak yazılmalıdır. Delphi niteliksiz biçimi sorunsuz derler; FPC ise onu Create, Enter veya Leave içermeyen bir record olarak çözer

var
  Pdf: TPDFlib;
  Handle, PageRef: Integer;
  Info: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    Handle := Pdf.DAOpenMappedFile('archive-2026.pdf', '',
      16 * 1024 * 1024, PDF_MAPPED_FILE_REQUIRE_MAPPING);
    if Handle = 0 then
      Exit;
    try
      PageRef := Pdf.DAFindPage(Handle, 1);
      Writeln(Pdf.DAExtractPageText(Handle, PageRef, 0));

      // {"memoryMapped":true,"fileSize":...,"remapCount":...}
      if Pdf.DAGetMappedFileInfo(Handle, Info) = 1 then
        Writeln(Info);
    finally
      Pdf.DACloseFile(Handle);
    end;
  finally
    Pdf.Free;
  end;
end;
  • memoryMapped, taşınabilir dosya akışı geri dönüşü etkin olduğunda false olur; eşlemenin hiç kurulmadığını kanıtlayan tek alan budur
  • windowSize, istediğiniz değer değil etkin hizalanmış penceredir; kuyruk penceresinde mappedBytes bundan küçüktür
  • mappedOffset, tutulan görünümün allocation hizalı başlangıcıdır veya o anda etkin görünüm yoksa -1'dir
  • readCalls başarılı ve dosya içindeki okuma isteklerini, bytesRead çağıranlara kopyalanan baytları, remapCount ise ilk görünüm dahil tüm yeniden eşlemeleri sayar

Hedefli regresyonlar pencereler arası mutlak okumaları, mantıksal imlecin korunmasını, kuyrukta kısa okumaları, geçersiz ofsetleri, reddedilen yazmaları, birbirinden ayrık pencereler arasında yeniden eşlemeyi, sıkıştırılamayan 220 KB'lık bir ekin ertelenmiş çıkarılmasını ve DACloseFile sonrasında geçersizleşen istatistikleri kapsar; Win32 ve Win64 headless paketlerinin her biri 1467 test keşfeder ve yok sayılan, başarısız, hatalı veya sızdırılmış sonuç olmadan hepsini geçer. Delphi veya C++Builder'da gigabaytlık PDF'lerle çalışıyor ve profiler sürekli ayrıştırma yerine dosya okumalarını gösteriyorsa, eşlenmiş dosya giriş noktaları bir öğleden sonra ölçüm yapmaya değer; GetMappedFileInfo gerçekten eşleme alıp almadığınızı söyler. Tam API başvurusu ve deneme sürümü PDFlibPas Delphi PDF library sayfasındadır