Artikel Teknis

Cache Render Disk HotPDF: Identitas Sumber dan Invalidasi

HotPDF RenderCacheFolder mengubah cache halaman hasil render di memori milik komponen Delphi HotPDF menjadi page cache disk yang persisten: halaman hasil render ditulis sebagai file PNG di bawah folder pilihan Anda, dan saat PDF sumber yang sama dibuka lagi, RenderLoadedPageToBitmapCached membacanya kembali alih-alih merasterisasi ulang. Urutan lookup-nya memori, lalu disk, lalu renderer

Tier disk sudah ada di API sejak v2.416.0, tapi sampai v2.770.140 ia tak pernah benar-benar menyajikan satu halaman pun untuk panggilan LoadFromFile atau LoadFromStream normal. Perbaikannya memaksa satu pertanyaan yang harus dijawab setiap cache persisten: bagaimana Anda tahu bahwa file yang Anda buka hari ini adalah dokumen yang Anda render kemarin, dan apa yang terjadi pada halaman ter-cache ketika bukan? Di bawah ini jawaban-jawaban yang dipilih HotPDF, termasuk di mana ia dengan sengaja menolak men-cache

Bagaimana disk render cache HotPDF bekerja?

Disk render cache HotPDF adalah tier kedua di belakang raster cache di memori, dan ia hanya berpartisipasi ketika RenderCacheFolder adalah path non-kosong. Panggilan RenderLoadedPageToBitmapCached(PageIndex, DPI) lebih dulu memindai entri di memori, yang di-key oleh indeks halaman, DPI, dan varian render-settings. Kalau miss, ia bertanya ke tier disk; hit disk men-decode PNG-nya, mem-promote-nya kembali ke memori, dan mengembalikan salinan milik caller. Hanya ketika kedua tier miss halaman melalui content-stream interpreter yang dijelaskan di merender halaman PDF loaded menjadi TBitmap, dan bitmap barunya juga ditulis ke disk

Diagram HotPDF atas lookup render cache untuk RenderLoadedPageToBitmapCached: tier di memori yang di-key oleh halaman, DPI dan varian render dicek lebih dulu, lalu tier disk RenderCacheFolder berupa file PNG dengan atomic replace, lalu content-stream interpreter, dan setiap hit mengembalikan salinan milik caller
HotPDF melirik memori lebih dulu, lalu disk, dan baru kemudian merasterisasi; hit disk di-promote kembali ke memori dan setiap jalur menyerahkan salinan yang Anda miliki dan harus Anda free

Di disk, layoutnya sengaja membosankan. Tiap dokumen mendapat subfolder yang dinamai dari key dokumen 16 karakter hex plus varian render 16 karakter hex, tiap halaman disimpan sebagai <page>@<dpi>.png, dan index.txt di root menjaga dokumen dalam urutan most-recently-used di balik satu schema tag. Ketidakcocokan schema membersihkan folder itu pada pemakaian pertama. Penulisan menuju file sementara lebih dulu lalu ditukar ke tempatnya dengan atomic replace, sehingga crash di tengah penulisan menyisakan halaman lama atau tidak sama sekali, tak pernah setengah PNG. PNG yang gagal di-decode dihapus dan dihitung sebagai miss

Tiga limit membatasi folder itu:

  • RenderCacheMaxDocuments (default 20) membatasi jumlah subfolder dokumen; folder least recently used di-evict lebih dulu
  • RenderCacheMaxBytes (default 524288000, yaitu 500 MB) membatasi ukuran total semua file PNG di bawah root
  • Tiap folder dokumen menyimpan paling banyak 200 image halaman; batas per dokumen itu tetap oleh THotPDF dan bukan property yang dipublikasikan

RenderCacheCapacity (default 8) adalah kenop terpisah: ia menyetel berapa halaman hasil render yang disimpan tier memori, dan tak ada hubungannya dengan jejak disk

uses
  SysUtils, Graphics, HPDFDoc;

procedure WarmThumbnails(const FileName: string);
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Konfigurasi disk tier sebelum render cache pertama:
    // folder dan kedua limit dibaca saat tier pertama kali dipakai
    Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
      GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
    Pdf.RenderCacheMaxDocuments := 50;
    Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
    Pdf.RenderCacheCapacity := 16;                        // halaman di memori

    if Pdf.LoadFromFile(FileName) > 0 then
      for I := 0 to Pdf.LoadedPageCount - 1 do
      begin
        Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 96);
        if Bmp <> nil then
        try
          // Serahkan salinannya ke thumbnail strip di sini
        finally
          Bmp.Free; // panggilan cache selalu mengembalikan salinan milik caller
        end;
      end;
  finally
    Pdf.Free; // sejak v2.770.140 ini tak lagi menghapus entri disk
  end;
end;

Jalankan prosedur yang sama dua kali dan run kedua tak pernah merasterisasi halaman yang muat di cache. Objek disk cache dibuat lazily pada render cache pertama dan hidup sampai instance THotPDF di-free, jadi mengubah RenderCacheFolder, RenderCacheMaxDocuments, atau RenderCacheMaxBytes setelah titik itu tak memindahkan atau mengubah ukuran cache yang sudah terbuka. Halaman yang terlalu besar untuk admission policy memori (secara default satu entri tak boleh melebihi 64 MiB piksel 32-bit) juga tak dipersistenkan, dan tier disk hanya dikonsultasikan selama RenderFallbackPolicy menjaga default rfpIgnore-nya, karena diagnostik fallback tidak disimpan bersama PNG

Kenapa RenderCacheFolder tak pernah bekerja sebelum v2.770.140?

RenderCacheFolder tak berpengaruh sebelum v2.770.140 karena tier disk meng-key dokumen pada hash byte sumber yang tidak pernah disimpan oleh load biasa. Key dokumen datang dari SHA-256 atas salinan internal byte PDF mentah, tapi LoadFromFile dan LoadFromStream mem-parse sumbernya di tempat dan tidak menyimpan salinan semacam itu; field itu hanya terisi sementara di jalur encrypted recovery dan dikosongkan lagi tepat setelahnya. Tanpa byte, key selalu kosong, dan key kosong berarti tier disk dilewati. Tanpa error, tanpa warning, cuma folder yang tetap kosong

Membuat key itu non-kosong membuka bug kedua yang sembunyi di balik yang pertama. InvalidateRenderedPageCache yang lama menghapus folder disk dokumen, dan InvalidateRenderedPageCache jalan di awal setiap load, pada setiap edit, dan di dalam Free. Jadi begitu key-nya bekerja, setiap sesi viewer akan menghancurkan cache-nya sendiri saat keluar, dan sesi berikutnya tetap mulai dingin. Lebih buruk, key dihitung ulang dari sumber yang sama setelah edit, sehingga render dokumen yang sudah diedit akan tersimpan di bawah key file asli dan disajikan ke sesi berikutnya yang membuka PDF yang belum dimodifikasi. v2.770.140 memperbaiki identitas dan invalidasi sekaligus; memperbaiki hanya salah satunya akan mengirim cache yang mati atau yang bohong

Bagaimana HotPDF mengidentifikasi PDF tanpa membaca seluruh file

HotPDF mengidentifikasi PDF yang dimuat dari file lokal lewat fingerprint dari ukurannya, last-write time-nya, serta 64 KiB pertama dan terakhirnya, dan mengidentifikasi stream atau sumber random-access lewat SHA-256 atas seluruh kontennya. Keduanya ditangkap sekali, saat load sukses, dan 16 karakter hex pertama dari digest SHA-256 (64 bit) menjadi key dokumennya

SumberIdentitasBiayaDitangkap kapan
LoadFromFileSize + LastWriteTime + 64 KiB awal dan akhir, di-hash dengan SHA-256Paling banyak baca 128 KiB, independen dari ukuran fileSetiap load yang sukses, meski RenderCacheFolder diset belakangan
LoadFromStreamSHA-256 atas seluruh streamSatu pass penuh atas sumberHanya jika RenderCacheFolder diset sebelum load
LoadFromRandomAccessSourceSHA-256 atas seluruh sumberSatu pass penuh atas sumberHanya jika folder diset lebih dulu dan seluruh range tersedia
Sumber apa pun dengan entri /EncryptTidak adaTidak adaTak pernah; tier disk dilewati
Peta identitas sumber HotPDF untuk disk render cache: LoadFromFile meng-hash ukuran, LastWriteTime dan 64 KiB pertama serta terakhir, LoadFromStream dan LoadFromRandomAccessSource meng-hash seluruh konten hanya ketika RenderCacheFolder diset lebih dulu, dan trailer /Encrypt apa pun tidak menangkap identitas sama sekali
File di-fingerprint dari ujung-ujungnya karena header, xref, dan trailer tinggal di sana, stream hanya membayar hash penuh kalau Anda minta cache lebih dulu, dan dokumen terenkripsi tak pernah ditulis ke disk

Fingerprint file adalah trade-off yang disengaja. Meng-hash arsip hasil scan 400 MB secara penuh di setiap pembukaan bisa lebih mahal daripada merender dua halaman yang benar-benar dilihat pengguna. Region yang disampelkan tak arbitrary: header berada di awal file, dan trailer serta section cross-reference terakhir berada di ujungnya (ISO 32000-1 §7.5). Incremental update menambahkan body baru, section cross-reference, dan trailer baru (§7.5.6), jadi ia mengubah ukuran dan ekor sekaligus. Rewrite penuh oleh tool normal mana pun mengubah last-write time. Untuk file sampai 128 KiB, kedua sampel menutupi setiap byte, jadi dokumen kecil efektifnya ter-hash penuh

Risiko residunya adalah perubahan di tempat berukuran sama di tengah file besar yang writernya lalu mengembalikan timestamp asli. Itu butuh tool yang sengaja melestarikan modification time sambil mengedit konten, jarang tapi tak mustahil, dan dalam kasus itu cache menyajikan halaman basi. Sisi baiknya: menyalin file di Windows biasanya melestarikan last-write time, jadi salinan dokumen yang sudah di cache meng-hit entri yang sama, dan itu benar karena byte-nya identik

Stream tak punya modification time sama sekali, jadi satu-satunya identitas yang jujur adalah kontennya. HotPDF hanya membayar pass SHA-256 penuh itu ketika Anda meminta disk cache sebelum load; caller LoadFromStream lainnya tak melihat biaya ekstra. Itu membuat urutan assignment property menjadi load-bearing:

procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
  const CacheRoot: string);
begin
  // Urutan salah untuk stream: hash konten hanya dihitung ketika
  // foldernya sudah diset, jadi dokumen ini akan melewati disk tier
  //   Pdf.LoadFromStream(Data);
  //   Pdf.RenderCacheFolder := CacheRoot;

  Pdf.RenderCacheFolder := CacheRoot; // set lebih dulu
  Data.Position := 0;
  if Pdf.LoadFromStream(Data) <= 0 then
    raise Exception.Create('The stream is not a loadable PDF');
end;

Sumber random-access yang masih mengunduh (beberapa range belum tersedia) tak mendapat identitas alih-alih hash atas konten parsial, dan kalau penghitungan identitas gagal karena alasan apa pun, load-nya tetap sukses; dokumennya cukup dirender tanpa tier disk

Apa yang meng-invalidasi entri disk cache HotPDF?

Entri disk cache HotPDF tak pernah di-invalidasi dengan menghapusnya saat edit; sebagai gantinya, mengedit dokumen yang dimuat membuang identitas dokumen itu, sehingga tier disk dilewati untuk sisa load tersebut dan halaman tersimpan tetap valid untuk sumber yang belum dimodifikasi. Entri meninggalkan disk hanya lewat limit LRU dan byte, PNG korup, atau perubahan schema

Key itu mendeskripsikan sumber di disk, bukan object graph di memori. Begitu Anda men-stamp sebuah halaman atau mengubah annotation, dokumen tak lagi cocok dengan sumber itu, jadi membaca maupun menulis di bawah key-nya sama-sama tak benar. Sejak v2.770.140, invalidasi level dokumen maupun level halaman mengosongkan identitas alih-alih menyentuh folder, dan ada pengaman kedua untuk edit yang tak memanggil InvalidateRenderedPageCache: sebelum memakai tier disk, THotPDF mengecek apakah ada objek yang dimuat berstatus dirty dan memperlakukan dokumen dirty sebagai tanpa identitas

Setting render bekerja sebaliknya. Mengganti PageRenderBackend (atau memanggil UseNativeGDIRenderBackend), serta memanggil ConfigureRenderICCWorkflow atau ClearRenderICCWorkflow, menyiram halaman di memori tapi menjaga identitas, karena dokumen masih cocok dengan sumbernya. Setting-setting itu mengubah piksel tanpa menjadi bagian varian di memori, jadi key disk melipat masuk nama backend, flag black-point compensation, dan digest SHA-256 dari profil ICC proof dan output. Varian itu sendiri sudah mencakup color intent, output dithering, overprint preview, luminosity mask mode, fallback policy, dan visibilitas setiap optional content group, jadi men-toggle layer merender ke folder berbeda alih-alih menimpa view default

Semantik invalidasi HotPDF untuk disk cache RenderCacheFolder: mengedit dokumen yang dimuat atau objek dirty mana pun membuang identitas sumber sehingga tier dilewati, mengganti render backend atau ICC workflow menjaga identitas di bawah key varian baru, dan save plus reload meng-key ulang dokumen
Edit tak pernah menghapus folder tersimpan, perubahan setting merender di bawah key berbeda, dan hanya save plus reload yang memberi dokumen hasil edit identitas baru

Untuk mengembalikan dokumen hasil edit ke tier disk, berikan identitas sumber baru dengan menyimpannya lalu memuat hasilnya:

procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
  // Setelah mengedit dokumen yang dimuat: refresh halaman di memori.
  // Identitas sumber sudah hilang, jadi tak ada yang dibaca dari atau
  // ditulis ke folder disk dokumen aslinya
  Pdf.InvalidateRenderedPageCache;

  // File tersimpan punya ukuran dan last-write time baru, karenanya
  // identitas baru; render setelah load ini di-cache di bawah key baru
  Pdf.SaveLoadedDocument(EditedFile);
  if Pdf.LoadFromFile(EditedFile) <= 0 then
    raise Exception.Create('Could not reload the edited document');
end;

Folder dokumen aslinya dibiarkan dan menua lewat RenderCacheMaxDocuments dan RenderCacheMaxBytes seperti entri lainnya. Kalau pengguna membuka ulang asli yang belum diedit, halaman-halamannya masih di sana

Batas keamanan: sumber terenkripsi dan folder terhubung

Disk render cache HotPDF menolak dua jenis input dengan sengaja: ia tak pernah menulis halaman PDF terenkripsi ke disk, dan tak pernah mengikuti subfolder dokumen yang berupa junction atau reparse point lain. Kedua aturan itu menukar cache hit dengan tak bocornya data dan tak terhapusnya file yang salah

PDF terenkripsi tak pernah di-cache di disk

Halaman hasil render adalah konten yang sudah di-decrypt. Menulisnya sebagai PNG polos ke folder cache akan menyisakan salinan yang bisa dibaca dari dokumen terlindungi kata sandi di disk, di luar perlindungan yang dipilih penulisnya (ISO 32000-1 §7.6). HotPDF karenanya tak menangkap identitas untuk sumber apa pun yang trailernya membawa entri /Encrypt, termasuk file yang dibuka dengan password atau dengan user password kosong. Dokumen-dokumen itu tetap memakai tier memori, yang mati bersama prosesnya

Subfolder junction ditolak sejak v2.770.173

Root cache adalah pilihan Anda, dan mengarahkannya ke junction diizinkan. Subfolder dokumen di bawahnya urusan berbeda: cache menciptakan, membaca, menyentuh, dan menghapusnya sendiri, selama recovery saat startup (yang menghapus file sementara tersisa), lookup (yang memperbarui timestamp), store, invalidasi, dan ketiga limit eviction. Kalau seseorang dengan akses tulis ke root cache mengganti folder dokumen dengan junction ke direktori lain, setiap jalur itu akan mengikutinya, dan eviction akan menghapus file di tempat yang tak pernah dimiliki cache. Sejak v2.770.173 tiap entry point itu mengecek atribut reparse-point dan melewatkan folder dokumen yang terhubung: lookup menghitung miss, store menghitung kegagalan tulis, dan eviction membiarkannya

Path Unicode dan root bersama

Dua perbaikan terkait penting kalau Anda deploy ke profil pengguna. Sebelum v2.770.135, RenderCacheFolder adalah AnsiString, sehingga folder di luar system code page (nama pengguna Tionghoa di instalasi Windows English misalnya) terkonversi secara lossy sebelum cache melihatnya; property itu kini string Unicode, dan atomic replace-nya memakai Windows API versi wide. Sejak v2.770.52, beberapa instance THotPDF dalam satu proses yang menunjuk root yang sama (setelah ekspansi path, dibandingkan case-insensitive) berbagi satu index dan lock yang reference-counted. Sebelumnya tiap instance menimpa index.txt dengan salinannya sendiri dan menegakkan limit terhadap view parsialnya, sehingga folder bisa tumbuh beberapa kali melewati anggarannya

Pembagian itu berhenti di batas proses. Dua proses terpisah di root yang sama tetap memegang index di memori yang terpisah, jadi berikan tiap aplikasi yang jalan bersamaan root cache miliknya sendiri. Viewer yang merender di worker thread aman dalam satu proses: PrefetchLoadedPages dan antrean yang dibahas di rendering latar belakang dengan request queue sama-sama lewat jalur cache yang sama dan lock yang sama

Referensi cepat: checklist RenderCacheFolder

  • Set RenderCacheFolder, RenderCacheMaxDocuments, dan RenderCacheMaxBytes sebelum panggilan pertama ke RenderLoadedPageToBitmapCached; untuk load stream dan random-access, set foldernya sebelum load
  • Upgrade ke v2.770.140 atau lebih baru jika Anda mengandalkan tier disk; versi lebih lama menerima property itu tapi tak pernah menyajikan satu halaman pun dari disk untuk load normal
  • Jangan berharap disk cache untuk PDF terenkripsi, untuk dokumen yang diedit setelah load, atau selama RenderFallbackPolicy bukan rfpIgnore
  • Free instance THotPDF secara normal; sejak v2.770.140 tidak ada Free maupun InvalidateRenderedPageCache yang menghapus entri disk
  • Mengganti PageRenderBackend atau ICC workflow menjaga dokumen di tier disk di bawah key yang berbeda
  • Pakai satu root cache per aplikasi yang berjalan; instance di dalam satu proses berbagi index sejak v2.770.52
  • Simpan root cache di lokasi per pengguna; subfolder dokumen yang berupa junction dilewati sejak v2.770.173

Page cache persisten paling terasa di viewer yang membuka ulang dokumen yang sama sepanjang hari, yang persis bentuk arsitektur PDF viewer custom di Delphi yang dibahas di tempat lain di blog ini. RenderCacheFolder, raster cache di memori, dan page renderer dikirim bersama komponen PDF Delphi HotPDF untuk Delphi dan C++Builder