HotPDF udostępnia profil pamięci wczytanego pliku PDF jako graf, który możesz odpytać: BuildLoadedObjectDependencyGraph zwraca jeden węzeł na każdy obiekt pośredni z oszacowanym rozmiarem płytkim, rozmiarem zatrzymanym opartym na dominatorach oraz flagą wskazującą, czy obiekt jest wciąż osiągalny z katalogu dokumentu. To zamienia „ten plik zajmuje 800 MB” w „obiekt 4173, XObject obrazu, wyłącznie zatrzymuje 612 MB”, czyli fakt, na którym można działać
Rozróżnienie między tymi dwoma zdaniami to sedno sprawy. Rozmiar płytki mówi, jak duży jest jeden obiekt. Rozmiar zatrzymany mówi, ile pamięci faktycznie zostałoby zwolnione, gdyby ten obiekt zniknął, czyli liczba decydująca o tym, czy poprawka pomaga
Dlaczego całkowite zużycie pamięci nie jest faktem, na którym można działać?
Ponieważ w pliku PDF niemal nic nie jest własnością wyłącznie jednej rzeczy. Pojedyncza osadzona czcionka CID jest przywoływana ze słownika zasobów każdej strony, która jej używa. Profil ICC wspiera przestrzeń kolorów współdzieloną przez dziesięć różnych strumieni treści. Form XObject użyty jako pieczątka pojawia się na wszystkich 400 stronach. Jeśli naiwnie zsumujesz rozmiary obiektów per strona, policzysz tę czcionkę 400 razy i dojdziesz do wniosku, że każda strona jest ogromna; jeśli podzielisz przez 400, dojdziesz do wniosku, że nic nie jest drogie, a pamięć wzięła się skądinąd
Analiza dominatorów rozwiązuje tę niejednoznaczność w jedyny sposób, który przetrwa kontakt z rzeczywistymi dokumentami. Obiekt X jest przypisywany do najbliższego obiektu, który go wyłącznie zatrzymuje, co oznacza, że każda ścieżka odwołań od katalogu do X przechodzi przez ten dominator. Czcionka współdzielona przez wszystkie strony nie jest przypisana do żadnej strony; jest przypisana do najbliższego węzła, przez który przechodzą wszystkie te ścieżki, co zwykle jest samym katalogiem. Czcionka używana przez dokładnie jedną stronę jest przypisana do tej strony. W efekcie EstimatedRetainedBytes sumuje się poprawnie zamiast liczyć podwójnie, a obiekty na szczycie listy to obiekty, których usunięcie rzeczywiście zwolniłoby pamięć
Co faktycznie zawiera graf
Każdy THPDFObjectDependencyNode niesie tożsamość obiektu jako ObjectNumber i GenerationNumber, plus ObjectType, LifecycleState, EstimatedShallowBytes, EstimatedRetainedBytes, IncomingReferenceCount, OutgoingReferenceCount, tożsamość swojego bezpośredniego dominatora oraz ReachableFromCatalog. Każda THPDFObjectDependencyEdge zapisuje źródło, cel, Path w słowniku, pod którym znaleziono odwołanie, oraz to, czy zostało Resolved
To pole Path jest tym, którego ludzie nie doceniają. Stanowi różnicę między wiedzą, że obiekt 91 wskazuje na obiekt 4173, a wiedzą, że robi to przez /Resources/XObject/Im3, co od razu mówi, czy patrzysz na treść strony, strumień wyglądu adnotacji, czy grupę treści opcjonalnej, której nikt nigdy nie renderuje
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;
Oba limity to jawne parametry z wartościami domyślnymi 250 000 obiektów i 2 000 000 krawędzi. Gdy dokument przekracza jeden z nich, LimitExceeded zostaje ustawione, a zwrócony graf jest obciętym prefiksem, a nie kłamstwem: częściowe wyniki z flagą, a nie po cichu błędne sumy. Podnoś limity świadomie na potrzeby analizy śledczej i pamiętaj, że analiza przechodzi przez cały graf obiektów, więc jej miejsce jest w ścieżce diagnostycznej, a nie w pętli renderowania
Co mówi ci obiekt nieosiągalny?
Obiekt z ReachableFromCatalog ustawionym na False to pamięć, którą dokument niesie, ale której przeglądarka nigdy nie pokaże. W praktyce bierze się to z trzech miejsc: aktualizacji przyrostowych, które zastąpiły wcześniejszą wersję obiektu i zostawiły oryginał w tyle, producenta, który zapisał obiekty, a potem nie zdołał ich powiązać, albo uszkodzonego pliku, którego tablica odsyłaczy krzyżowych została odbudowana i przejęła definicje, do których nic się nie odwołuje
Pierwszy przypadek jest normalny i oczekiwany, i dokładnie do tego służą aktualizacje przyrostowe i strumienie obiektów. Drugi i trzeci są warte zbadania. Gdy duża część zatrzymanych bajtów siedzi w węzłach nieosiągalnych, znalazłeś konkretny argument za przepisaniem pliku od nowa zamiast dopisywania do niego, i masz liczbę bajtów, by uzasadnić dodatkowy czas przetwarzania przed każdym, kto zapyta
Dwa liczniki alokatora warte sprawdzenia jako pierwsze
Zanim uznasz, że dokument jest z natury duży, sprawdź, czy kosztem nie jest sam analizator. HotPDF udostępnia dwa liczniki opisujące przebieg wczytywania, a nie zawartość dokumentu
GetLastParserArenaStatistics zgłasza arenę bloków zatrzymanych, używaną dla krótkotrwałych tokenów i buforów analizatora: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount i TemporaryObjectElisionCount. To ostatnie pole liczy klucze słownika sparsowane bezpośrednio do areny zamiast przez tymczasowy obiekt nazwy, czyli alokację, która kiedyś dominowała w parsowaniu plików gęstych w słowniki
GetDocumentStringInternStatistics zgłasza pulę internowania w zakresie dokumentu, deduplikującą powtarzające się nazwy PDF, operatory strumienia treści i niezmienne łańcuchy znaków do 64 bajtów. Daje ci RequestCount, HitCount, MissCount, BypassCount, RetainedBytes i ReusedBytes, wraz z obowiązującymi pułapami. Pula jest celowo ograniczona do 65 536 wpisów i 4 MiB, więc wrogi dokument generujący milion unikalnych nazw nie może zamienić optymalizacji pamięci we wzmacniacz zużycia pamięci; po osiągnięciu pułapu kolejne łańcuchy omijają pulę, a BypassCount rośnie
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;
Wysoka wartość ReusedBytes przy niskim BypassCount oznacza, że dokument ma powtarzalne słownictwo typowe dla większości prawdziwych plików PDF, a pula zarabia na siebie. BypassCount zbliżający się do RequestCount oznacza coś nietypowego: albo naprawdę ogromny dokument, albo taki, który celowo generuje unikalne nazwy, co jest łagodnym sygnałem wartym zalogowania na niezaufanej ścieżce przyjmowania plików
Kolejność triażu, która działa
Zacznij od CatalogRetainedBytes z informacji o grafie. Jeśli ta liczba jest bliska wzrostowi twojego procesu, pamięć jest w dokumencie, a graf pokaże ci, gdzie. Jeśli jest znacznie niższa, pamięć jest w twoich własnych pamięciach podręcznych, w wyrenderowanych bitmapach albo w analizatorze, a liczniki areny powiedzą, w którym z nich
Następnie weź dziesięć najwyższych węzłów według EstimatedRetainedBytes i przyjrzyj się ich ObjectType. XObjecty obrazów na szczycie oznaczają, że plik jest gęsty w skany, a downsampling jest lekarstwem. Deskryptory czcionek na szczycie oznaczają, że osadzono pełne czcionki tam, gdzie wystarczyłyby podzbiory. Strumienie treści na szczycie zwykle oznaczają wygenerowaną grafikę wektorową, często mapy albo eksporty CAD. Dopiero po tym warto spojrzeć na obiekty nieosiągalne i zachowanie alokatora. Pracując w tej kolejności, zwykle znajdziesz odpowiedź w pierwszych dwóch krokach, a dla bardzo dużych dokumentów podejście strumieniowe opisane w przepływie pracy bezpośredniego API plikowego jest częściej rozwiązaniem strukturalnym niż jakakolwiek optymalizacja pojedynczego obiektu
Wszystkie te diagnostyki to zwykłe wywołania Pascala zwracające zwykłe rekordy, więc wpinają się bezpośrednio w istniejącą ścieżkę logowania albo telemetrii. HotPDF to natywny komponent VCL PDF dla Delphi i C++Builder z pełnym źródłem; dokumentacja API i wersja próbna są na stronie komponentu HotPDF dla Delphi