HotPDF tải một PDF từ bất kỳ nguồn truy cập ngẫu nhiên nào bạn tự triển khai, và THPDFCoalescingRandomAccessSource bọc nguồn đó lại để các lượt đọc nhỏ, rải rác của trình phân tích cú pháp trở thành một tập hợp có giới hạn các dải khối đã cache kèm tải trước bất đồng bộ. Trên một tài liệu phục vụ qua các yêu cầu HTTP range, đây là khác biệt giữa vài trăm lượt round trip và vài chục lượt
Không có gì trong trình phân tích cú pháp thay đổi cả. Bạn vẫn gọi LoadFromRandomAccessSource, cùng một đối tượng tài liệu trả về, và cùng một API trang hoạt động. Điều thay đổi là lưu lượng bên dưới
Vì sao cùng một PDF tải tức thì khi ở máy nội bộ nhưng lại ì ạch qua mạng?
Vì một trình phân tích cú pháp PDF không đọc một tệp, nó dò dẫm qua tệp đó. Nó seek đến cuối tệp để tìm startxref, nhảy ngược lại bảng tham chiếu chéo, giải quyết từ điển trailer, theo một tham chiếu đến Catalog, rồi đến gốc cây trang, rồi đến một node trang, rồi đến từ điển tài nguyên của nó. Mỗi bước trong số đó đọc vài chục byte từ một offset khác nhau
Trên một tệp cục bộ, kiểu truy cập đó gần như miễn phí: hệ điều hành đã cache sẵn trang 4 KiB xung quanh, nên lượt đọc thứ hai chỉ tốn một phép memcpy. Qua một giao vận mạng thì không có tính cục bộ như vậy. Mỗi lượt đọc là một yêu cầu có độ trễ riêng, và 300 yêu cầu tuần tự với 40 ms mỗi yêu cầu là mười hai giây gần như hoàn toàn dành cho việc chờ đợi. Cách sửa không phải là đọc ít đi; trình phân tích cú pháp cần chính xác những gì nó yêu cầu. Cách sửa là làm cho mỗi lượt đọc vật lý bao phủ nhiều hơn phần mà lượt đọc logic tiếp theo sẽ cần
Việc gộp thay đổi điều gì
Nguồn gộp làm tròn mọi lượt đọc lên thành một khối và cache khối đó. BlockSize mặc định là 262.144 byte và MaxCacheBytes mặc định là 2.097.152, nên tám khối thường trú theo mặc định và bị loại bỏ theo thứ tự dùng gần đây nhất so với một ngân sách byte cứng. Lượt đọc 40 byte của trình phân tích cú pháp cho một khóa trailer sẽ kéo theo 256 KiB xung quanh nó, và hàng chục lượt đọc tiếp theo trong vùng lân cận đó, chính là nơi dữ liệu tham chiếu chéo và catalog nằm, sẽ được phục vụ từ bộ nhớ
Nguồn của riêng bạn vẫn đơn giản. Hãy triển khai GetSize và ReadAt, ghi đè ReadAtCancellable nếu giao vận của bạn có thể hủy giữa chừng, và để lớp bọc xử lý việc cache, gộp và tải trước
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: lớp bọc sẽ giải phóng Raw cùng với chính nó
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;
Nên đọc trước bao xa?
Đọc trước thích ứng trả lời câu hỏi đó theo từng tài liệu thay vì buộc bạn phải đoán. Khi bật AdaptiveReadAheadEnabled, cửa sổ tăng dần qua 1, 2, 4 và 8 khối khi các lượt đọc tiến tới liên tục tích lũy, và không bao giờ vượt quá MaxReadAheadBlocks hoặc dung lượng cache đã cấu hình. Ngay khi có một lượt đọc đến không nằm gần chỗ lượt đọc trước đó kết thúc, cửa sổ sẽ sụp xuống và việc tải trước bị ngừng lại
SequentialReadToleranceBytes, mặc định 4.096, định nghĩa thế nào là "gần". Các lượt đọc nằm trong khoảng cách đó tính từ điểm kết thúc của lượt đọc trước vẫn được tính là tuần tự, điều này quan trọng vì một trình phân tích cú pháp PDF duyệt qua một luồng nội dung không tạo ra các offset liền kề hoàn hảo; nó bỏ qua một trường độ dài ở đây, một từ điển inline ở kia. Đặt ngưỡng quá thấp thì một lượt quét tiến tới bình thường bị phân loại là ngẫu nhiên, nên đọc trước không bao giờ được kích hoạt. Đặt quá cao thì truy cập ngẫu nhiên thực sự lại trông như tuần tự, nên bạn tải về hàng megabyte không ai cần. Giá trị mặc định được hiệu chỉnh cho việc duyệt luồng nội dung, và số liệu thống kê sẽ cho bạn biết nếu giao vận của bạn không đồng ý
Sự bất đối xứng này là chủ ý: tăng dần từ từ, sụp đổ ngay lập tức. Việc tải quá mức trên một khối lượng công việc truy cập ngẫu nhiên tốn băng thông và tiền bạc thực trên các giao vận tính phí theo dung lượng, nên sai lầm rẻ tiền được ưu tiên hơn sai lầm đắt đỏ
Hủy bỏ thực sự dừng được việc truyền tải
Lớp cơ sở khai báo ReadAtCancellable, và nguồn gộp tôn trọng nó từ đầu đến cuối. Khi một lượt đọc tiền cảnh đến cho một dải mà một lượt tải trước đang chạy không phục vụ, lượt tải trước đó bị hủy thay vì để nó chạy xong, nên yêu cầu trang của người dùng không bị xếp hàng đợi phía sau lưu lượng suy đoán. Cách triển khai mặc định trên THPDFRandomAccessSource quay về dùng ReadAt thuần túy, nghĩa là tính năng này là tùy chọn theo từng giao vận: các client HTTP hỗ trợ hủy yêu cầu có được khả năng hủy thực sự, còn các nguồn đơn giản hơn vẫn hoạt động bình thường không đổi
Kết hợp điều đó với một token hủy được luồn qua giao diện người dùng của bạn, và một người dùng đóng tài liệu sẽ thực sự dừng lưu lượng mạng thay vì chờ nó chảy hết. Cùng mô hình token đó cũng là nền tảng cho hàng đợi được mô tả trong render nền với hàng đợi yêu cầu, nên một token duy nhất có thể bao phủ toàn bộ đường đi từ viewport đến socket
Đọc số liệu thống kê cache dải
GetStatistics điền vào một bản ghi THPDFRangeCacheStatistics tách biệt những gì giao vận của bạn đã làm với những gì cache đã làm. SourceReadCount và SourceBytesRead là lưu lượng vật lý. CacheHitCount và CacheMissCount là lưu lượng logic. SequentialReadCount và RandomReadCount cho thấy kiểu truy cập được phân loại ra sao, CurrentReadAheadBlocks và PeakReadAheadBlocks cho thấy cửa sổ đã mở rộng đến đâu, và PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount cùng SuppressedPrefetchCount cho thấy việc suy đoán có mang lại hiệu quả hay không
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;
Ba chỉ số cho bạn biết cần thay đổi gì. Nhiều lượt tải trước bị hủy cùng số lượt đọc ngẫu nhiên cao nghĩa là tài liệu đang được truy cập không theo thứ tự, nên hãy giảm MaxReadAheadBlocks và ngừng trả tiền cho băng thông bị bỏ phí. Nhiều lượt trượt cache trong khi cửa sổ đỉnh vẫn ở mức 1 nghĩa là ngưỡng đang từ chối một kiểu truy cập vốn dĩ gần như tuần tự, nên hãy tăng SequentialReadToleranceBytes. Và số byte đọc được vượt xa kích thước tệp nghĩa là cache đang thrash, nên hãy tăng MaxCacheBytes trước khi động vào bất cứ thứ gì khác
Tệp linearized làm thay đổi bài toán
Nếu bạn kiểm soát trình tạo, việc linearize tài liệu thay đổi hẳn vấn đề thay vì chỉ tối ưu nó. Một PDF linearized đặt các đối tượng của trang đầu tiên cùng một bảng gợi ý ở đầu tệp, nên một trình xem có thể render trang một từ megabyte mở đầu mà không cần thấy phần còn lại. HotPDF cung cấp đường đó trực tiếp qua GetProgressiveLinearizedLoadInfo và ReadProgressiveLinearizedFirstPageSection, còn phía ghi được trình bày trong tạo PDF linearized kèm bảng gợi ý
Cả hai kỹ thuật kết hợp được với nhau. Gộp khiến bất kỳ tài liệu nào cũng chấp nhận được qua một đường truyền chậm; linearize khiến trang đầu tiên đến nhanh trên các tài liệu bạn tự tạo. Với các tệp nằm trên đĩa cục bộ nhưng quá lớn để chứa trong bộ nhớ, các đường xử lý mapped-file và lazy-stream được mô tả trong quy trình API tệp trực tiếp thường là công cụ tốt hơn, vì ở đó không hề có độ trễ round-trip nào để khấu hao ngay từ đầu
HotPDF là một thành phần VCL PDF gốc cho Delphi và C++Builder, không cần DLL bên ngoài cho trình phân tích cú pháp và có đầy đủ mã nguồn. API nguồn truy cập ngẫu nhiên, lớp bọc gộp và các điểm vào tải tiến triển được tài liệu hóa tại trang thành phần PDF HotPDF cho Delphi