Bài viết kỹ thuật

HotPDF: Tìm điểm ngốn bộ nhớ PDF bằng đồ thị phụ thuộc

HotPDF phơi bày hồ sơ bộ nhớ của một PDF đã tải dưới dạng một đồ thị bạn có thể truy vấn: BuildLoadedObjectDependencyGraph trả về một node cho mỗi đối tượng gián tiếp, kèm kích thước nông ước tính, kích thước retained dựa trên dominator, và một cờ cho biết đối tượng đó có còn truy cập được từ Catalog của tài liệu hay không. Điều đó biến "tệp này dùng 800 MB" thành "đối tượng 4173, một XObject ảnh, độc quyền giữ 612 MB", một sự thật mà bạn có thể hành động dựa trên đó

Sự khác biệt giữa hai câu đó chính là toàn bộ vấn đề. Kích thước nông cho bạn biết một đối tượng lớn bao nhiêu. Kích thước retained cho bạn biết bao nhiêu bộ nhớ sẽ thực sự được giải phóng nếu đối tượng đó biến mất, và đó chính là con số quyết định liệu một bản sửa có mang lại hiệu quả hay không

Vì sao tổng dung lượng bộ nhớ không phải một sự thật có thể hành động?

Vì trong một PDF, gần như không có gì thuộc sở hữu của đúng một thứ duy nhất. Một font CID nhúng duy nhất được tham chiếu từ từ điển tài nguyên của mọi trang dùng nó. Một hồ sơ ICC hậu thuẫn cho một không gian màu mà mười luồng nội dung khác nhau cùng chia sẻ. Một Form XObject dùng làm hình đóng dấu xuất hiện trên cả 400 trang. Nếu bạn cộng dồn kích thước đối tượng theo từng trang một cách ngây thơ, bạn sẽ đếm font đó 400 lần và kết luận rằng mọi trang đều khổng lồ; nếu bạn chia nó cho 400, bạn sẽ kết luận rằng không có gì đắt đỏ cả và bộ nhớ đến từ một nơi khác

Phân tích dominator giải quyết sự mơ hồ đó theo cách duy nhất còn đứng vững khi va chạm với các tài liệu thực tế. Một đối tượng X được quy về cho đối tượng gần nhất độc quyền giữ nó, nghĩa là mọi đường tham chiếu từ Catalog đến X đều đi qua dominator đó. Một font được mọi trang chia sẻ không được quy về cho bất kỳ trang nào; nó được quy về cho node gần nhất mà mọi đường đi đó cùng đi qua, thường chính là Catalog. Một font chỉ được đúng một trang dùng thì được quy về cho trang đó. Kết quả là EstimatedRetainedBytes cộng dồn đúng thay vì đếm trùng, và các đối tượng đứng đầu danh sách là những đối tượng mà việc loại bỏ chúng thực sự giải phóng được bộ nhớ

Đồ thị thực sự chứa những gì

Mỗi THPDFObjectDependencyNode mang theo danh tính đối tượng dưới dạng ObjectNumberGenerationNumber, cùng ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, danh tính của dominator trực tiếp, và ReachableFromCatalog. Mỗi THPDFObjectDependencyEdge ghi lại nguồn, đích, Path từ điển nơi tham chiếu được tìm thấy, và liệu nó có Resolved hay không

Trường Path đó là thứ mà mọi người ít dùng nhất. Đó là khác biệt giữa việc biết đối tượng 91 trỏ tới đối tượng 4173 và việc biết nó làm vậy thông qua /Resources/XObject/Im3, điều này cho bạn biết ngay lập tức liệu bạn đang nhìn vào nội dung trang, một luồng hiển thị chú thích, hay một nhóm nội dung tùy chọn mà không ai từng hiển thị

var
  Pdf: THotPDF;
  Nodes: THPDFObjectDependencyNodeArray;
  Edges: THPDFObjectDependencyEdgeArray;
  Info: THPDFObjectDependencyGraphInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('report-800mb.pdf') <> 1 then Exit;

    if not Pdf.BuildLoadedObjectDependencyGraph(Nodes, Edges, Info) then Exit;

    if Info.LimitExceeded then
      Log('Graph truncated: raise MaxObjects / MaxEdges');

    Log(Format('%d objects, %d edges, %d reachable, catalog retains %d bytes',
      [Info.ObjectCount, Info.EdgeCount, Info.ReachableObjectCount,
       Info.CatalogRetainedBytes]));

    SortByRetainedDescending(Nodes);
    for I := 0 to Min(9, High(Nodes)) do
      Log(Format('%d %d obj: shallow %d, retained %d, dominator %d',
        [Nodes[I].ObjectNumber, Nodes[I].GenerationNumber,
         Nodes[I].EstimatedShallowBytes, Nodes[I].EstimatedRetainedBytes,
         Nodes[I].ImmediateDominatorObjectNumber]));
  finally
    Pdf.Free;
  end;
end;

Cả hai giới hạn đều là tham số tường minh với giá trị mặc định là 250.000 đối tượng và 2.000.000 cạnh. Khi một tài liệu vượt quá bất kỳ giới hạn nào, LimitExceeded được đặt và đồ thị trả về là một tiền tố bị cắt bớt chứ không phải một lời nói dối: kết quả một phần kèm cờ báo, không phải tổng số sai lệch âm thầm. Hãy nâng giới hạn một cách có chủ đích cho một lượt phân tích pháp y, và nhớ rằng việc phân tích này duyệt toàn bộ đồ thị đối tượng, nên nó thuộc về một đường chẩn đoán, không phải vòng lặp hiển thị của bạn

Một đối tượng không thể truy cập nói lên điều gì?

Một đối tượng có ReachableFromCatalog bằng False là bộ nhớ mà tài liệu đang mang theo nhưng trình xem sẽ không bao giờ hiển thị. Trong thực tế nó đến từ ba nguồn: các bản cập nhật tăng dần đã thay thế một phiên bản trước đó của một đối tượng và để lại bản gốc, một trình tạo đã ghi ra các đối tượng rồi lại không liên kết chúng, hoặc một tệp bị hỏng mà bảng tham chiếu chéo đã được dựng lại và vô tình nhặt phải các định nghĩa mà không gì tham chiếu tới

Trường hợp đầu tiên là bình thường và như dự kiến, và đó chính xác là điều mà các bản cập nhật tăng dần và luồng đối tượng được thiết kế để làm. Trường hợp thứ hai và thứ ba đáng để điều tra. Khi một phần lớn số byte retained nằm trong các node không thể truy cập, bạn đã tìm ra một lý lẽ cụ thể để viết lại tệp thay vì tiếp tục nối thêm vào nó, và bạn đã có số byte để thuyết phục bất kỳ ai hỏi về thời gian xử lý bổ sung

Hai bộ đếm cấp phát đáng đọc trước tiên

Trước khi kết luận rằng một tài liệu vốn dĩ lớn, hãy kiểm tra xem liệu bản thân trình phân tích cú pháp có phải là nguyên nhân hay không. HotPDF cung cấp hai bộ đếm mô tả quá trình tải diễn ra như thế nào thay vì tài liệu chứa gì

GetLastParserArenaStatistics báo cáo arena khối lưu giữ dùng cho các token và bộ đệm tạm thời của trình phân tích cú pháp: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCountTemporaryObjectElisionCount. Trường cuối cùng đó đếm số khóa từ điển được phân tích trực tiếp vào arena thay vì thông qua một đối tượng tên tạm thời, đây chính là kiểu cấp phát từng chi phối việc phân tích các tệp giàu từ điển

GetDocumentStringInternStatistics báo cáo pool intern theo phạm vi tài liệu, dùng để khử trùng lặp các tên PDF lặp lại, toán tử luồng nội dung và chuỗi bất biến dài tới 64 byte. Nó cho bạn RequestCount, HitCount, MissCount, BypassCount, RetainedBytesReusedBytes, cùng các giới hạn đang áp dụng. Pool này bị giới hạn có chủ đích ở 65.536 mục và 4 MiB, nên một tài liệu độc hại tạo ra hàng triệu tên duy nhất không thể biến một tối ưu hóa bộ nhớ thành một bộ khuếch đại bộ nhớ; một khi chạm giới hạn, các chuỗi tiếp theo sẽ bỏ qua pool và BypassCount tăng lên

var
  Arena: THPDFParserArenaStatistics;
  Intern: THPDFDocumentStringInternStatistics;
begin
  if Pdf.GetLastParserArenaStatistics(Arena) then
    Log(Format('arena: peak %d, reused %d of %d allocations, %d tokens',
      [Arena.PeakUsedBytes, Arena.ReusedAllocationCount,
       Arena.AllocationCount, Arena.TokenCount]));

  if Pdf.GetDocumentStringInternStatistics(Intern) then
    Log(Format('intern: %d/%d hits, %d bypassed, %d bytes reused',
      [Intern.HitCount, Intern.RequestCount, Intern.BypassCount,
       Intern.ReusedBytes]));
end;

Một giá trị ReusedBytes cao đi kèm BypassCount thấp nghĩa là tài liệu có vốn từ vựng lặp lại như hầu hết các PDF thực tế, và pool đang phát huy tác dụng. Một BypassCount gần bằng RequestCount nghĩa là có điều gì đó bất thường: hoặc một tài liệu thực sự khổng lồ, hoặc một tài liệu cố tình tạo ra các tên duy nhất, đây là một tín hiệu nhẹ đáng ghi log trên một đường tiếp nhận không đáng tin

Một thứ tự phân loại hiệu quả

Hãy bắt đầu với CatalogRetainedBytes từ thông tin đồ thị. Nếu con số đó gần với mức tăng bộ nhớ của tiến trình bạn, bộ nhớ nằm trong tài liệu và đồ thị sẽ cho bạn biết ở đâu. Nếu nó thấp hơn nhiều, bộ nhớ nằm trong cache của riêng bạn, trong các bitmap đã render, hoặc trong trình phân tích cú pháp, và các bộ đếm arena sẽ cho biết là cái nào

Sau đó hãy lấy mười node đứng đầu theo EstimatedRetainedBytes và xem ObjectType của chúng. Các XObject ảnh đứng đầu nghĩa là tệp giàu ảnh quét và giảm mẫu là bản sửa phù hợp. Các mô tả font đứng đầu nghĩa là các mặt chữ đầy đủ đã được nhúng ở nơi lẽ ra chỉ cần tập con. Các luồng nội dung đứng đầu thường nghĩa là đồ họa vector được tạo tự động, thường là bản đồ hoặc xuất từ CAD. Chỉ sau đó mới đáng để xem xét các đối tượng không thể truy cập và hành vi bộ cấp phát. Làm theo thứ tự đó, bạn thường sẽ tìm ra câu trả lời trong hai bước đầu, và với các tài liệu rất lớn, cách tiếp cận streaming được mô tả trong quy trình API tệp trực tiếp thường là bản sửa mang tính cấu trúc hơn là bất kỳ tối ưu hóa nào theo từng đối tượng

Tất cả các chẩn đoán này đều là các lệnh gọi Pascal thuần túy trả về các bản ghi thuần túy, nên chúng gắn thẳng vào một đường ghi log hoặc telemetry hiện có. HotPDF là một thành phần VCL PDF gốc cho Delphi và C++Builder có đầy đủ mã nguồn; tài liệu tham chiếu API và bản dùng thử có tại trang thành phần PDF HotPDF cho Delphi