Техническа статия

Открийте PDF паметоядци: графи на HotPDF

HotPDF излага паметовия профил на зареден PDF като граф, който можете да заявявате: BuildLoadedObjectDependencyGraph връща един възел за всеки indirect обект с оценен shallow размер, dominator-базиран retained размер и флаг дали обектът все още е достижим от document Catalog. Това превръща „този файл използва 800 MB“ в „обект 4173, image XObject, изключително задържа 612 MB“, което е факт, върху който можете да действате

Разликата между тези две изречения е целият смисъл. Shallow размерът ви казва колко голям е един обект. Retained размерът ви казва колко памет реално би се освободила, ако този обект изчезне, а точно това е числото, което решава дали дадена поправка помага

Защо общото използване на памет не е приложим факт?

Защото в PDF почти нищо не е притежание на точно едно нещо. Един-единствен вграден CID шрифт е референциран от resource dictionary на всяка страница, която го използва. ICC профил backва цветово пространство, споделяно от десет различни content stream-а. Form XObject, използван като печат, се появява на всичките 400 страници. Ако наивно съберете размерите на обектите по страница, ще преброите този шрифт 400 пъти и ще заключите, че всяка страница е огромна; ако го разделите на 400, ще заключите, че нищо не е скъпо и паметта е дошла отнякъде другаде

Dominator анализът разрешава двусмислието по единствения начин, който оцелява при контакт с реални документи. Обект X се приписва на най-близкия обект, който го задържа изключително, което означава, че всеки референтен път от Catalog до X минава през този dominator. Шрифт, споделен от всички страници, не се приписва на никоя страница; той се приписва на най-близкия възел, през който минават всички тези пътища, което обикновено е самият Catalog. Шрифт, използван от точно една страница, се приписва на тази страница. Резултатът е, че EstimatedRetainedBytes се сумира правилно, вместо да брои двойно, а обектите на върха на списъка са обекти, чието премахване наистина би освободило памет

Какво реално съдържа графът

Всеки THPDFObjectDependencyNode носи идентичността на обекта като ObjectNumber и GenerationNumber, плюс ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, идентичността на непосредствения му dominator и ReachableFromCatalog. Всеки THPDFObjectDependencyEdge записва източник, цел, Path в речника, под който е намерена референцията, и дали е Resolved

Полето Path е онова, което хората подценяват. То е разликата между това да знаете, че обект 91 сочи към обект 4173, и това да знаете, че го прави през /Resources/XObject/Im3, което ви казва веднага дали гледате съдържание на страница, appearance stream на анотация или група за optional content, която никой никога не рендира

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;

И двете граници са изрични параметри с подразбирания от 250 000 обекта и 2 000 000 ребра. Когато документ надхвърли едната или другата, LimitExceeded се задава, а върнатият граф е отрязан префикс, а не лъжа: частичен резултат с флаг, а не тихо погрешни суми. Повишете лимитите съзнателно за криминалистичен анализ и помнете, че анализът обхожда целия граф на обекти, така че мястото му е в диагностичен път, а не във вашия render цикъл

Какво ви казва недостижим обект?

Обект с ReachableFromCatalog, зададено на False, е памет, която документът носи, но зрителят никога няма да покаже. На практика идва от три места: incremental update, който е заменил по-ранна версия на обект и е оставил оригинала след себе си, производител, записал обекти, които после не е свързал, или повреден файл, чиято cross-reference таблица е била възстановена и е поела дефиниции, към които нищо не сочи

Първият случай е нормален и очакван и е точно онова, за което са проектирани incremental update-ите и object stream-ите. Вторият и третият си струва да бъдат разследвани. Когато голяма част от retained байтовете седи в недостижими възли, сте открили конкретен аргумент за пренаписване на файла, вместо добавяне към него, и разполагате с броя байтове, за да оправдаете допълнителното време за обработка пред всеки, който пита

Два брояча на разпределителя, които си струва да прочетете първо

Преди да заключите, че документ е присъщо голям, проверете дали самият parser е разходът. HotPDF излага два брояча, които описват как е протекло зареждането, а не какво съдържа документът

GetLastParserArenaStatistics докладва задържаната блокова arena, използвана за краткотрайни parser токени и буфери: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount и TemporaryObjectElisionCount. Последното поле брои ключове на речници, парснати директно в arena-та, вместо през временен обект-име, което е разпределението, доминирало парсването на файлове, богати на речници

GetDocumentStringInternStatistics докладва document-scoped intern pool-а, който дедуплицира повтарящи се PDF имена, оператори на content stream и immutable низове до 64 байта. Дава ви RequestCount, HitCount, MissCount, BypassCount, RetainedBytes и ReusedBytes, заедно с действащите тавани. Пулът е съзнателно ограничен до 65 536 записа и 4 MiB, така че враждебен документ, генериращ милион уникални имена, не може да превърне оптимизация на паметта в усилвател на памет; щом таванът бъде достигнат, следващите низове заобикалят пула и BypassCount расте

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;

Висока стойност на ReusedBytes с нисък BypassCount означава, че документът има повтарящия се речник, който имат повечето реални PDF файлове, а пулът си заслужава мястото. BypassCount, приближаващ се към RequestCount, означава нещо необичайно: или наистина огромен документ, или такъв, генериращ уникални имена нарочно, което е слаб сигнал, струващ си логване по път за прием на ненадеждни файлове

Ред на триаж, който работи

Започнете с CatalogRetainedBytes от информацията за графа. Ако това число е близо до растежа на процеса ви, паметта е в документа, а графът ще ви покаже къде. Ако е далеч под него, паметта е в собствените ви кешове, в рендирани bitmap-и или в parser-а, а брояците на arena-та ще кажат кое от трите

После вземете първите десет възела по EstimatedRetainedBytes и погледнете техния ObjectType. Image XObject-и на върха означават, че файлът е богат на сканирания, а downsampling-ът е поправката. Font descriptor-и на върха означават, че са вградени цели шрифтове, там където подмножества биха свършили работа. Content stream-и на върха обикновено означават генерирана векторна графика, често карти или CAD износи. Едва след това си заслужава да се погледнат недостижимите обекти и поведението на разпределителя. Работейки в този ред, обикновено ще намерите отговора в първите две стъпки, а за много големи документи поточният подход, описан в работния процес на Direct File API, често е структурната поправка, а не оптимизация по обект

Всички тези диагностики са прости Pascal извиквания, връщащи прости записи, така че се вписват директно в съществуващ път за логване или телеметрия. HotPDF е native VCL PDF компонент за Delphi и C++Builder с пълен изходен код; референцията на API и пробна версия са на страницата на HotPDF PDF компонента за Delphi