Bài viết kỹ thuật

Sáu lệnh gọi PDFium quên khóa render trong Delphi

Khóa render của PDFiumPas là một critical section theo từng tài liệu — EnterRenderLockLeaveRenderLock, được hậu thuẫn bởi một trường TRTLCriticalSection trên TPdf — nhằm mục đích bọc mọi lệnh gọi vào bộ raster hóa của PDFium để một trang không thể bị unload hay reload ngay dưới một lần render đang diễn ra. Sáu phương thức chia đều giữa TPdfTPdfView gọi trực tiếp các API bitmap và trích xuất thumbnail của PDFium và hoàn toàn bỏ qua khóa đó, một khoảng trống mà PDFiumPas v2.26.0 đã đóng lại bằng cách bọc cả sáu trong cùng cặp khóa mà mọi điểm vào render khác đã dùng sẵn

Khoảng trống được nói đến ở đây không phải lượt gia cố ABI được nói đến ở nơi khác trên blog này, thứ đã đi qua một sự không khớp calling-convention cdecl và một lỗi cắt cụt độ rộng con trỏ FPC Win64 trong cùng binding PDFium. Những gì theo sau đây hẹp hơn và máy móc hơn: một checklist bao phủ khóa cho sáu điểm gọi đều vươn vào đường render của PDFium, vì sao mỗi điểm đó dễ bị bỏ sót, và vì sao cuộc chạy đua theo sau việc thiếu khóa là một trong những lỗi khó tái tạo theo yêu cầu nhất trong codebase này

Khóa render thực sự bảo vệ điều gì

PDFiumPas serialize việc render vì trang đã tải của PDFium không an toàn để đọc từ một luồng trong khi một luồng khác được tự do giải phóng nó. TPdf sở hữu một TRTLCriticalSection trong FRenderLock, được khởi tạo trong constructor và được bảo vệ bởi một cờ FRenderLockReady để một lệnh gọi đến sau khi dọn dẹp trở thành một no-op âm thầm thay vì đi vào một critical section đã bị xóa. EnterRenderLockLeaveRenderLock là cách duy nhất được chấp thuận để vào và ra khỏi section đó

procedure TPdf.EnterRenderLock;
begin
  if FRenderLockReady then
    EnterCriticalSection(FRenderLock);
end;

procedure TPdf.LeaveRenderLock;
begin
  if FRenderLockReady then
    LeaveCriticalSection(FRenderLock);
end;

TPdf.RenderPage, RenderTile, và RenderPageProgressive đã tuân theo kỷ luật đó từ trước khi cuộc kiểm toán cụ thể này bắt đầu, mỗi phương thức lấy khóa trước khi gọi vào PDFium và giải phóng nó trong một khối finally để một lần pre-render nền và một lần UnloadPage tiền cảnh trên cùng một instance TPdf không thể chồng lấn nhau. Khoảng trống mà PDFiumPas v2.26.0 tìm thấy không nằm ở những điểm vào rõ ràng đó — nó lộ ra trong sáu phương thức đọc như các accessor thay vì các render, dù mỗi phương thức trong số đó đều yêu cầu PDFium raster hóa pixel trước khi có thể trả về bất cứ thứ gì

Sáu lệnh gọi nào đã bỏ qua khóa render?

TPdf.GetObjectBitmap, TPdf.GetBitmap, và TPdf.GetThumbnail chiếm một nửa danh sách, và TPdfView.GetObjectBitmap, TPdfView.GetBitmap, và TPdfView.GetThumbnail chiếm nửa còn lại — cùng ba thao tác, được nhân đôi trên hai lớp component phơi bày cùng trang bên dưới. Cả sáu cuối cùng đều gọi hoặc FPDFImageObj_GetBitmap hoặc FPDFPage_GetThumbnailAsBitmap, và cả hai điểm vào PDFium đó đều raster hóa tại chỗ thay vì trả về một tham chiếu đến thứ gì đó đã render sẵn. Không tên phương thức nào trong sáu tên đó nói render, đó là một giải thích hợp lý cho việc vì sao chúng không được viết theo cùng checklist với RenderPageRenderTile ngay từ đầu

function TPdf.GetObjectBitmap(Index: Integer): TBitmap;
var
  Bitmap: FPDF_BITMAP;
begin
  Result:= nil;
  EnterRenderLock;
  try
    Bitmap:= FPDFImageObj_GetBitmap(GetObjectHandle(Index));
  finally
    LeaveRenderLock;
  end;
  if Bitmap<> nil then
    try
      Result:= ToBitmap(Bitmap);
    finally
      FPDFBitmap_Destroy(Bitmap);
    end;
end;

Vì sao TPdfView bảo vệ lệnh gọi khóa của nó bằng một kiểm tra nil

TPdfView không sở hữu một critical section riêng — mỗi lệnh gọi khóa trong sáu lệnh gọi của nó chuyển tiếp đến FPdf.EnterRenderLockFPdf.LeaveRenderLock, được bọc trong một kiểm tra rằng tham chiếu TPdf liên kết không phải nil trước. Sự bảo vệ đó tồn tại vì một TPdfView có thể nằm trên một form tại thời điểm thiết kế, hoặc trong khoảnh khắc ngắn giữa lúc một tài liệu đóng và tài liệu tiếp theo mở, không có TPdf nào được gán cho FPdf chưa. Bỏ qua sự bảo vệ đó sẽ đổi một crash lấy một crash khác, vì một lệnh gọi khóa trên một tham chiếu nil thất bại không nhẹ nhàng hơn cuộc chạy đua mà khóa tồn tại để ngăn chặn

function TPdfView.GetThumbnail: TBitmap;
var
  PdfBitmap: FPDF_BITMAP;
begin
  CheckActive;
  Result:= nil;
  if FPdf<> nil then
    FPdf.EnterRenderLock;
  try
    PdfBitmap:= FPDFPage_GetThumbnailAsBitmap(Page);
  finally
    if FPdf<> nil then
      FPdf.LeaveRenderLock;
  end;
  if PdfBitmap<> nil then
    try
      Result:= ToBitmap(PdfBitmap);
    finally
      FPDFBitmap_Destroy(PdfBitmap);
    end;
end;

Vì sao RenderPage(HDC) thuộc về cùng cuộc kiểm toán này?

TPdfView.RenderPage đối với một device context không phải một trong sáu lệnh gọi trên — nó lộ ra một bản phát hành trước đó, trong PDFiumPas v2.25.0, và nó xứng đáng có một vị trí trong checklist này vì nó là cùng lỗi khoác một chữ ký khác. Overload đó gọi FPDF_RenderPage thẳng qua mà không có cả EnterRenderLock lẫn lệnh gọi SetArithmeticMask bảo vệ khỏi ngoại lệ FPU trên các trình biên dịch Delphi cũ hơn, trong khi overload TBitmap nằm vài dòng bên dưới nó trong cùng lớp đã mang cả hai. Hai lượt kiểm toán bắt được cùng chế độ lỗi cách nhau một bản phát hành nói lên ít điều về bất kỳ phương thức đơn lẻ nào và nói nhiều hơn về hình dạng của lỗi: nó ẩn mình trong bất cứ overload nào không ai đọc lại một khi anh em của nó trông đúng đắn

procedure TPdfView.RenderPage(DeviceContext: HDC; Left, Top, Width,
  Height: Integer; Rotation: TRotation; Options: TRenderOptions);
var
  ArithmeticMask: TArithmeticMask;
begin
  CheckActive;
  if FPdf<> nil then
    FPdf.EnterRenderLock;
  ArithmeticMask:= SetArithmeticMask;
  try
    FPDF_RenderPage(DeviceContext, FPage, Left, Top, Width, Height,
      Ord(Rotation), EncodeRenderOptions(Options));
  finally
    RestoreArithmeticMask(ArithmeticMask);
    if FPdf<> nil then
      FPdf.LeaveRenderLock;
  end;
end;

Vì sao cuộc chạy đua này gần như không thể tái tạo?

Khoảng trống khóa render của PDFiumPas không thất bại ở mọi lần chạy, hay thậm chí ở hầu hết các lần chạy, vì nó cần hai điều cụ thể đáp xuống cùng một instance TPdf cùng lúc: một lệnh gọi raster hóa đã đang diễn ra, và một lệnh UnloadPage hay ReloadPage đồng thời đến trong đúng khung cửa sổ đó. Việc test đơn luồng hoàn toàn không bao giờ chạy qua đường đó, và ngay cả các khối lượng công việc thực sự đa luồng cũng chỉ kích hoạt nó khi một lần render nền và một sự kiện vòng đời tài liệu tình cờ chồng lấn trong vòng đời một trang. Điều kiện kích hoạt thực tế nhất là pre-render PDF nền được xây dựng trên các future có thể hủy, nơi một luồng worker raster hóa trang tiếp theo trong khi luồng UI reload hay unload trang hiện tại theo đầu vào người dùng

FPDFImageObj_GetBitmapFPDFPage_GetThumbnailAsBitmap duyệt các cấu trúc page-object mà UnloadPage tự do giải phóng giữa lượt duyệt, nên một cuộc chạy đua thực sự kích hoạt cũng không phải lúc nào cũng tạo ra một access violation tức thời. Một cấu trúc được đọc muộn một khoảnh khắc có thể dễ dàng trả về pixel rác, hoặc làm hỏng metadata heap chỉ làm crash một vài lần cấp phát không liên quan sau đó, trong một hàm chưa bao giờ đụng vào một trang PDF. Đó là lý do trung thực vì sao loại lỗi này có thể sống sót trong một codebase qua nhiều chu kỳ phát hành: stack trace tại thời điểm thất bại hiếm khi trỏ đến gần sáu dòng thực sự đang thiếu một khóa

Điều gì thay đổi với các caller

GetBitmap, GetObjectBitmap, GetThumbnail, và overload HDC của RenderPage giữ nguyên chữ ký công khai đúng như trước, vì bản sửa là khóa nội bộ được thêm quanh các lệnh gọi hiện có thay vì một cuộc di trú. Đáng nhớ rằng khóa render có phạm vi theo từng instance TPdf, không toàn cục cho tiến trình, nên hai luồng render hai tài liệu được nạp riêng biệt vẫn chạy hoàn toàn song song — khóa chỉ serialize các thao tác trên một tài liệu mà cả hai luồng tình cờ chia sẻ. Nếu việc khóa của bạn đã đúng đắn và các lần render vẫn cảm thấy chậm khi zoom hay cuộn, đó là một câu hỏi khác, được trả lời trong bài viết về cache render PDFium và các chiến thuật hiệu năng zoom — tính đúng đắn và tốc độ là hai trục riêng biệt ở đây, và bản sửa này chỉ đụng đến trục đầu tiên

Sáu phương thức và một overload anh em là một phần nhỏ trong bề mặt PDFium mà PDFiumPas phơi bày, nhưng chúng là phần duy nhất chỉ hoạt động sai dưới tải mà không ai tình cờ chạy trong một debugger. Bản thân khóa render, và toàn bộ tập hợp điểm vào render mà nó giờ bao phủ, đi kèm sẵn trong PDFium Component dành cho Delphi, C++Builder, và Lazarus/FPC