HotPDF надає профіль пам'яті завантаженого PDF у вигляді графа, який можна опитувати: BuildLoadedObjectDependencyGraph повертає один вузол на кожен непрямий об'єкт із приблизним поверхневим розміром, домінаторним retained-розміром і прапорцем того, чи об'єкт досі досяжний з каталогу документа. Це перетворює «цей файл використовує 800 МБ» на «об'єкт 4173, XObject зображення, ексклюзивно утримує 612 МБ» — а це вже факт, на основі якого можна діяти
Різниця між цими двома реченнями — і є вся суть. Поверхневий розмір показує, наскільки великий один об'єкт. Retained-розмір показує, скільки пам'яті насправді звільниться, якщо цей об'єкт зникне, — і саме це число визначає, чи допоможе виправлення
Чому загальне використання пам'яті — не факт, на якому можна діяти?
Тому що в PDF майже ніщо не належить рівно одній речі. Один вбудований CID-шрифт має посилання зі словника ресурсів кожної сторінки, що його використовує. Потік ICC-профілю підтримує колірний простір, який спільно використовують десять різних потоків вмісту. Form XObject, що використовується як штамп, з'являється на всіх 400 сторінках. Якщо наївно підсумувати розміри об'єктів по сторінках, ви порахуєте цей шрифт 400 разів і зробите висновок, що кожна сторінка величезна; якщо поділити на 400, ви зробите висновок, що нічого не коштує дорого, а пам'ять узялася звідкись іще
Аналіз домінаторів усуває цю неоднозначність єдиним способом, який витримує зіткнення з реальними документами. Об'єкт X приписується найближчому об'єкту, який ексклюзивно його утримує, тобто кожен шлях посилань від каталогу до X проходить через цей домінатор. Шрифт, спільний для всіх сторінок, не приписується жодній конкретній сторінці — він приписується найближчому вузлу, через який проходять усі ці шляхи, і зазвичай це сам каталог. Шрифт, що використовується лише однією сторінкою, приписується саме їй. У результаті EstimatedRetainedBytes підсумовується коректно, без подвійного рахунку, а об'єкти на вершині списку — це об'єкти, видалення яких справді звільнить пам'ять
Що насправді містить граф
Кожен THPDFObjectDependencyNode несе ідентичність об'єкта як ObjectNumber і GenerationNumber, а також ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, ідентичність свого безпосереднього домінатора та ReachableFromCatalog. Кожне THPDFObjectDependencyEdge фіксує джерело, ціль, шлях словника Path, під яким було знайдено посилання, та чи воно Resolved
Це поле Path люди найчастіше недооцінюють. Різниця між знанням того, що об'єкт 91 вказує на об'єкт 4173, і знанням того, що це відбувається через /Resources/XObject/Im3, — саме та, що одразу підказує, чи ви дивитеся на вміст сторінки, потік зовнішнього вигляду анотації, чи на групу необов'язкового вмісту, яку ніхто ніколи не рендерить
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, а повернутий граф — це обрізаний префікс, а не брехня: часткові результати з прапорцем, а не тихо неправильні підсумки. Свідомо підвищуйте межі для криміналістичного прогону, і пам'ятайте, що аналіз обходить увесь граф об'єктів, тож йому місце в діагностичному шляху, а не у вашому циклі рендерингу
Про що свідчить недосяжний об'єкт?
Об'єкт зі значенням ReachableFromCatalog, рівним False, — це пам'ять, яку документ несе, але яку переглядач ніколи не покаже. На практиці вона походить із трьох джерел: інкрементні оновлення, що замінили попередню версію об'єкта й залишили оригінал позаду, виробник документа, що записав об'єкти, а потім не зв'язав їх, або пошкоджений файл, чию таблицю перехресних посилань перебудували і вона підхопила визначення, на які ніщо не посилається
Перший випадок нормальний і очікуваний, і це саме те, для чого призначені інкрементні оновлення та потоки об'єктів. Другий і третій варто розслідувати. Коли значна частка retained-байтів сидить у недосяжних вузлах, ви знайшли конкретний аргумент на користь перезапису файлу замість дописування в нього, і у вас є кількість байтів, щоб обґрунтувати додатковий час обробки перед будь-ким, хто запитає
Два лічильники алокатора, які варто перевірити першими
Перш ніж робити висновок, що документ за своєю природою великий, перевірте, чи не є вартість наслідком самого парсера. HotPDF надає два лічильники, які описують те, як поводилося завантаження, а не те, що містить документ
GetLastParserArenaStatistics повідомляє про арену блоків, що утримується для короткоживучих токенів і буферів парсера: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount і TemporaryObjectElisionCount. Останнє поле рахує ключі словника, розібрані прямо в арену замість через тимчасовий об'єкт імені, — саме те виділення пам'яті, яке раніше домінувало під час розбору файлів, насичених словниками
GetDocumentStringInternStatistics повідомляє про пул інтернування в межах документа, який дедуплікує повторювані імена PDF, оператори потоку вмісту та незмінні рядки до 64 байтів. Він дає вам RequestCount, HitCount, MissCount, BypassCount, RetainedBytes і ReusedBytes, разом із чинними межами. Пул навмисно обмежений 65 536 записами та 4 МіБ, тож ворожий документ, що генерує мільйон унікальних імен, не може перетворити оптимізацію пам'яті на її підсилювач; коли межу досягнуто, подальші рядки обходять пул, і 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 з інформації графа. Якщо це число близьке до приросту пам'яті вашого процесу, пам'ять — у документі, і граф покаже де саме. Якщо воно значно менше, пам'ять — у ваших власних кешах, у відрендерених растрах або в парсері, і лічильники арени підкажуть, де саме
Далі візьміть десять вузлів із найбільшим EstimatedRetainedBytes і подивіться на їхній ObjectType. XObjects зображень нагорі означають, що файл насичений сканами, і виправлення — це зменшення розподільної здатності. Дескриптори шрифтів нагорі означають, що вбудовано повні гарнітури там, де вистачило б підмножин. Потоки вмісту нагорі зазвичай означають згенеровану векторну графіку, часто карти або експорти з CAD. Лише після цього варто дивитися на недосяжні об'єкти й поведінку алокатора. Працюючи в такому порядку, ви зазвичай знайдете відповідь уже на перших двох кроках, а для дуже великих документів потоковий підхід, описаний у статті про робочий процес прямого файлового API, часто є структурним виправленням, а не поодинокою оптимізацією окремого об'єкта
Усі ці діагностики — звичайні виклики Pascal, що повертають звичайні записи, тож вони вбудовуються прямо в наявний шлях логування чи телеметрії. HotPDF — це нативний VCL-компонент PDF для Delphi та C++Builder із повним вихідним кодом; довідник API та пробна збірка — на сторінці HotPDF Delphi PDF component