Mất hai phút để sao chép ba trang ra khỏi một tệp PDF 40 trang không phải là một vấn đề về tinh chỉnh hiệu năng. Đó là một tín hiệu cho thấy đường dẫn API sai đang được sử dụng. Khi lần đầu tiên tôi nhìn thấy khoảng thời gian này trên một đoạn mã mẫu sao chép trang HotPDF Component, bản năng của tôi là xem xét cấu trúc tài liệu trước và mã thứ hai. Hóa ra thứ tự đó lại có ý nghĩa quan trọng
Điều gì thực sự chậm
Tệp PDF được đề cập là một tài liệu tham khảo 40 trang với một cây trang không hề đơn giản: nhiều nút /Pages trung gian thay vì một mảng phẳng duy nhất. Đoạn mã mẫu ban đầu đã gọi LoadFromFile, sau đó xây dựng một tài liệu mới với BeginDoc, lặp qua các số trang đã chọn, và ở mỗi vòng lặp lại tải lại tài liệu nguồn từ đĩa để kéo ra một trang. Đó là chi phí phân tích cú pháp toàn bộ nhân với số lượng trang bạn muốn. Một tệp 12 MB đã truy xuất đĩa sáu lần cho một thao tác trích xuất ba trang vì không ai để ý xem liệu tệp có cần duy trì trạng thái mở qua các vòng lặp hay không
Tác nhân thứ hai vô hình trong mã: LoadFromFile của HotPDF phân giải toàn bộ bảng tham chiếu chéo và giải nén mọi luồng đối tượng khi tải. Đó là hành vi đúng đối với một tài liệu mà bạn sắp sửa đổi, nhưng nó lại là nhiều công việc hơn mức bạn cần nếu bạn chỉ muốn lấy số lượng trang và một tập con các trang. Đối với quyền truy cập chỉ-đọc vào cấu trúc, DAOpenFileReadOnly tránh được việc giải tuần tự (deserializing) toàn bộ cây đối tượng, điều này có ý nghĩa quan trọng đối với các tệp bị nén chứa các tài nguyên hình ảnh lớn
Cả hai điều này đều không phải là lỗi thư viện. Cả hai đều là việc người gọi chọn API được thiết kế cho công việc này và sử dụng nó cho một công việc khác
Sử dụng InsertPagesFromDocument để trích xuất trang
Đường dẫn đúng để sao chép một dải trang từ một tài liệu HotPDF sang một tài liệu khác là InsertPagesFromDocument, được gọi sau khi LoadFromFile nguồn. Bạn tải nguồn một lần, tải hoặc tạo đích một lần, di chuyển các trang, và lưu. Nguồn sẽ nằm trong bộ nhớ xuyên suốt tất cả các thao tác chèn trang:
procedure ExtractPages(const SourceFile, DestFile: string;
const PageRange: string);
var
Source, Dest: THotPDF;
begin
Source := THotPDF.Create(nil);
Dest := THotPDF.Create(nil);
try
// Load source once: full parse happens here and only here
Source.LoadFromFile(SourceFile);
// Build a minimal destination document
Dest.FileName := DestFile;
Dest.BeginDoc;
// Copy the requested range; '1-3' inserts pages 1 through 3
// starting at position 1 in the destination
Dest.InsertPagesFromDocument(Source, PageRange, 1);
Dest.EndDoc;
finally
Source.Free;
Dest.Free;
end;
end;
Tham số PageRange chấp nhận định dạng tương tự như mẫu dòng lệnh: một danh sách các số hoặc dải trang phân tách bằng dấu phẩy như '1-3' hoặc '1,5,7-9'. Các trang được tính theo cơ số 1 (1-based). InsertPagesFromDocument sao chép các luồng nội dung, từ điển tài nguyên, và đặc tính hình học của trang mà không đụng tới siêu dữ liệu, dấu trang (bookmarks), hoặc các tệp đính kèm nhúng trừ khi chúng được tham chiếu từ các trang được sao chép. Đối với một thao tác trích xuất ba trang từ một tài liệu 40 trang, đó là một tập hợp làm việc (working set) nhỏ
Thời gian xử lý trên cùng một tệp 12 MB trước đó chạy mất hai phút: dưới 1,5 giây với mô hình này. Hầu hết thời gian đó là lệnh gọi LoadFromFile duy nhất. Cấu trúc tài liệu không còn liên quan nữa một khi bảng đối tượng được phân giải lần đầu tiên
Khi LoadFromFile là quá mức cần thiết: Direct File API
Nếu bạn chỉ cần đếm trang, kiểm tra thông tin tài liệu, hoặc sao chép một tệp mà không chạm vào nội dung của nó, thì Direct File API sẽ tránh hoàn toàn quá trình phân tích cú pháp đầy đủ. DAOpenFileReadOnly ánh xạ bảng tham chiếu chéo mà không giải nén các luồng đối tượng, vì vậy số lượng trang là O(kích thước xref) thay vì O(kích thước tệp):
procedure InspectPDF(const FileName: string);
var
Pdf: THotPDF;
Handle, PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Handle := Pdf.DAOpenFileReadOnly(FileName, '');
if Handle <= 0 then
Exit;
try
PageCount := Pdf.DAGetPageCount(Handle);
Writeln('Pages: ', PageCount);
// DACopyFile is a byte-preserving copy, no re-serialization
Pdf.DACopyFile(FileName, 'archive-copy.pdf');
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
Lời cảnh báo: DAOpenFileReadOnly chấp nhận một tham số mật khẩu nhưng lại lùi về (fall back) một bản phân tích toàn bộ đối với các đầu vào được mã hóa, bởi vì việc giải mã yêu cầu cây đối tượng phải phân giải được từ điển mã hóa. Nếu các tệp nguồn của bạn được mã hóa, hãy giải mã chúng trước với DecryptFile để có được một bản sao không mã hóa, sau đó mở bản đó bằng Direct File API. Hàm DecryptFile cấp độ tệp lấy một đường dẫn viết lại trực tiếp bằng AES-256 đối với mã hóa tiêu chuẩn và nó nhanh hơn LoadFromFile theo sau bởi SaveLoadedDocument đối với các tệp lớn, bởi vì nó không xây dựng mô hình đối tượng đầy đủ trong bộ nhớ
Bộ nhớ trong quá trình xử lý hàng loạt lô lớn
Các công việc hàng loạt (batch jobs) xử lý hàng chục tệp trong một vòng lặp có một mô hình trông có vẻ đúng nhưng lại tích lũy bộ nhớ: tạo THotPDF bên trong vòng lặp, gọi LoadFromFile, thực hiện công việc, gọi Free. Về mặt cấu trúc, điều đó là ổn định. Vấn đề nảy sinh khi công việc bên trong phân bổ các đối tượng tạm (scratch objects), bắt các ngoại lệ (exceptions), và để những đối tượng tạm đó tiếp tục sống trên các đường dẫn lỗi. Trình quản lý bộ nhớ của Delphi không thực hiện nén dồn (compact), vì vậy một trăm vụ rò rỉ đường dẫn lỗi dọc theo một lần chạy hàng loạt có thể đẩy bộ nhớ lên đủ cao để làm chậm quá trình phân bổ cho mọi thứ khác
Bản sửa lỗi này không có gì kỳ lạ. Mỗi THotPDF và mọi TStream hoặc TBitmap trung gian tham gia vào công việc PDF đều thuộc về một khối try/finally nơi Free là câu lệnh cuối cùng. Đặt các con trỏ cục bộ thành nil trước khi try để nhánh finally có thể sử dụng if Assigned(x) then x.Free một cách an toàn khi quá trình khởi tạo thất bại giữa chừng. Đây là kỷ luật quyền sở hữu chuẩn mực của Delphi và đó là toàn bộ câu chuyện đối với lớp vấn đề này
Còn một điều nữa cần kiểm tra trong các ngữ cảnh hàng loạt: AddImage đăng ký các hình ảnh trong một danh sách nội bộ tồn tại suốt vòng đời của đối tượng THotPDF. Nếu bạn tái sử dụng một đối tượng duy nhất trên nhiều tài liệu bằng cách gọi LoadFromFile liên tục, các đăng ký hình ảnh từ những tài liệu trước đó sẽ vẫn nằm trong danh sách. Bạn có thể tạo một đối tượng mới hoàn toàn cho mỗi tài liệu hoặc gọi đường dẫn xóa danh sách hình ảnh ở giữa các tài liệu
Đo lường trước khi thay đổi bất cứ điều gì
Trước khi tìm đến bất kỳ mô hình nào trong số này, hãy đo lường. TStopwatch của Delphi từ System.Diagnostics bao bọc QueryPerformanceCounter và nó đủ chính xác cho việc đo lường (profiling) I/O tệp theo thời gian thực (wall-clock). Hãy bọc riêng LoadFromFile lại và xem nó chiếm bao lâu. Nếu nó chiếm 90% tổng thời gian, thì bản sửa lỗi là Direct File API hoặc giảm số lần bạn phân tích cú pháp cùng một tệp. Nếu dưới 20%, nút thắt cổ chai nằm ở một nơi khác và bạn đang theo đuổi sai thứ
Sự việc trích xuất mất hai phút bắt đầu cho bài đăng này hóa ra hoàn toàn là do mô hình tải lặp lại. Cấu trúc tài liệu không góp phần vào lỗi này; một cây trang phẳng lẽ ra cũng sẽ chạy theo cùng một cách. Việc chuyển sang một lệnh LoadFromFile duy nhất theo sau là một lệnh gọi InsertPagesFromDocument đã đưa nó xuống mức 1,3 giây trên cùng một phần cứng mà không cần đụng tới bất cứ thứ gì khác
API thao tác trang được hiển thị ở đây là một phần của HotPDF Component cho Delphi và C++Builder