HotPDF memuat PDF dari sumber random-access apa pun yang Anda implementasikan, dan THPDFCoalescingRandomAccessSource membungkus sumber itu sehingga pembacaan kecil yang tersebar dari parser berubah menjadi sekumpulan block range yang dicache dengan asynchronous prefetch. Pada dokumen yang disajikan lewat HTTP range request, ini adalah perbedaan antara beberapa ratus round trip dan hanya beberapa lusin
Tidak ada yang berubah pada parser. Anda tetap memanggil LoadFromRandomAccessSource, document object yang sama dikembalikan, dan page API yang sama tetap berfungsi. Yang berubah adalah traffic di baliknya
Mengapa PDF yang sama bisa dimuat seketika secara lokal tapi merayap di jaringan?
Karena PDF parser tidak membaca file, ia menavigasinya. Ia melakukan seek ke akhir untuk startxref, melompat kembali ke cross-reference table, meresolve trailer dictionary, mengikuti referensi ke Catalog, lalu ke akar page tree, lalu ke page node, lalu ke resource dictionary-nya. Setiap langkah itu membaca puluhan byte dari offset yang berbeda-beda
Pada file lokal pola seperti itu hampir tanpa biaya: sistem operasi sudah mencache 4 KiB halaman di sekitarnya, sehingga pembacaan kedua hanya menghabiskan biaya sebuah memcpy. Lewat transport jaringan, tidak ada locality semacam itu. Setiap pembacaan adalah sebuah request dengan latensinya sendiri, dan 300 request berurutan pada 40 ms masing-masing berarti dua belas detik yang hampir seluruhnya dihabiskan untuk menunggu. Solusinya bukan membaca lebih sedikit; parser memang membutuhkan persis apa yang diminta. Solusinya adalah membuat setiap pembacaan fisik mencakup lebih banyak dari apa yang akan diminta oleh pembacaan logis berikutnya
Apa yang diubah oleh coalescing
Sumber coalescing membulatkan setiap pembacaan ke atas menjadi satu block dan mencache block itu. BlockSize defaultnya 262.144 byte dan MaxCacheBytes 2.097.152, sehingga delapan block resident secara default dan dievict dengan urutan least-recently-used terhadap hard byte budget. Pembacaan 40 byte dari sebuah trailer key oleh parser menarik masuk 256 KiB di sekitarnya, dan selusin pembacaan berikutnya di lingkungan itu, yaitu tempat data cross-reference dan catalog berada, dilayani dari memori
Sumber Anda sendiri tetap sederhana. Implementasikan GetSize dan ReadAt, override ReadAtCancellable jika transport Anda bisa membatalkan permintaan yang sedang berjalan, dan biarkan wrapper menangani caching, coalescing, dan prefetch
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: wrapper membebaskan Raw bersama dirinya sendiri
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;
Seberapa jauh ke depan seharusnya ia membaca?
Adaptive read-ahead menjawab pertanyaan itu per dokumen, alih-alih memaksa Anda menebak. Dengan AdaptiveReadAheadEnabled diaktifkan, window tumbuh melalui 1, 2, 4, dan 8 block seiring terkumpulnya pembacaan maju yang berkelanjutan, dan tidak pernah melebihi MaxReadAheadBlocks atau kapasitas cache yang dikonfigurasi. Begitu ada pembacaan yang datang tidak kira-kira di titik akhir pembacaan sebelumnya, window langsung runtuh dan prefetch ditekan
SequentialReadToleranceBytes, default 4.096, mendefinisikan "kira-kira" itu. Pembacaan yang jatuh dalam jarak tersebut dari titik akhir pembacaan sebelumnya tetap dihitung sebagai sequential, yang penting karena PDF parser yang menyusuri content stream tidak menghasilkan offset yang benar-benar berurutan; ia melompati length field di sini, inline dictionary di sana. Atur tolerance terlalu rendah dan pemindaian maju normal akan diklasifikasikan sebagai random, sehingga read-ahead tidak pernah aktif. Atur terlalu tinggi dan random access yang sesungguhnya terlihat seperti sequential, sehingga Anda mengambil megabyte yang tidak diinginkan siapa pun. Nilai default dikalibrasi untuk penyusuran content-stream, dan statistiknya akan memberi tahu Anda jika transport Anda berbeda
Asimetri ini disengaja: pertumbuhan bersifat bertahap, keruntuhan bersifat langsung. Over-fetching pada beban kerja random-access menghabiskan bandwidth dan uang sungguhan pada transport bertarif meter, sehingga kesalahan yang murah lebih dipilih daripada yang mahal
Cancellation yang benar-benar menghentikan transfer
Base class mendeklarasikan ReadAtCancellable, dan sumber coalescing menghormatinya dari ujung ke ujung. Saat pembacaan foreground datang untuk sebuah range yang tidak sedang dilayani oleh prefetch yang sedang berjalan, prefetch itu dibatalkan alih-alih dibiarkan selesai, sehingga permintaan halaman pengguna tidak antre di belakang traffic spekulatif. Implementasi default pada THPDFRandomAccessSource jatuh kembali ke ReadAt biasa, yang berarti fitur ini opt-in per transport: HTTP client yang mendukung pembatalan request mendapatkan cancellation yang sesungguhnya, dan sumber yang lebih sederhana tetap berfungsi tanpa perubahan
Gabungkan itu dengan cancellation token yang dialirkan melalui UI Anda, dan pengguna yang menutup dokumen benar-benar menghentikan traffic jaringan alih-alih menunggu sampai habis. Model token yang sama mendasari queueing yang dijelaskan di background rendering dengan request queue, sehingga satu token bisa mencakup seluruh jalur dari viewport hingga socket
Membaca statistik range cache
GetStatistics mengisi record THPDFRangeCacheStatistics yang memisahkan apa yang dilakukan transport Anda dari apa yang dilakukan cache. SourceReadCount dan SourceBytesRead adalah traffic fisik. CacheHitCount dan CacheMissCount adalah traffic logis. SequentialReadCount dan RandomReadCount menunjukkan bagaimana pola akses diklasifikasikan, CurrentReadAheadBlocks dan PeakReadAheadBlocks menunjukkan seberapa jauh window terbuka, dan PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount, serta SuppressedPrefetchCount menunjukkan apakah spekulasi itu membuahkan hasil
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;
Tiga pembacaan angka ini memberi tahu apa yang perlu diubah. Banyak prefetch yang dibatalkan dengan random-read count tinggi berarti dokumen diakses tidak berurutan, jadi turunkan MaxReadAheadBlocks dan berhenti membayar bandwidth yang Anda buang. Banyak miss dengan peak window yang masih di angka 1 berarti tolerance menolak pola yang sebenarnya efektif sequential, jadi naikkan SequentialReadToleranceBytes. Dan byte yang terbaca jauh melebihi ukuran file berarti cache sedang thrashing, jadi naikkan MaxCacheBytes sebelum menyentuh yang lain
File linearized mengubah perhitungannya
Jika Anda mengendalikan producer-nya, melinearisasi dokumen mengubah masalahnya, bukan sekadar mengoptimalkannya. PDF linearized menempatkan object halaman pertama dan hint table di bagian depan file, sehingga viewer bisa merender halaman satu hanya dari megabyte pembuka tanpa perlu melihat sisanya. HotPDF mengekspos jalur itu secara langsung lewat GetProgressiveLinearizedLoadInfo dan ReadProgressiveLinearizedFirstPageSection, dan sisi penulisannya dibahas di membuat PDF linearized dengan hint table
Kedua teknik itu bisa dikombinasikan. Coalescing membuat dokumen apa pun bisa ditoleransi lewat koneksi lambat; linearization membuat halaman pertama tiba dengan cepat pada dokumen yang Anda hasilkan sendiri. Untuk file yang berada di disk lokal tapi terlalu besar untuk ditampung di memori, jalur mapped-file dan lazy-stream yang dijelaskan di direct file API workflow biasanya adalah alat yang lebih baik, karena sejak awal memang tidak ada latensi round-trip yang perlu diamortisasi
HotPDF adalah komponen PDF VCL native untuk Delphi dan C++Builder, tanpa DLL eksternal untuk parser dan dengan source lengkap tersedia. API sumber random-access, wrapper coalescing, dan entry point progressive loading didokumentasikan di halaman komponen PDF Delphi HotPDF