HotPDF חושף את פרופיל הזיכרון של PDF טעון כגרף שאפשר לשאול: BuildLoadedObjectDependencyGraph מחזיר צומת אחד לכל אובייקט עקיף עם גודל רדוד משוער, גודל מוחזק מבוסס-דומיננטור, ודגל האם האובייקט עדיין נגיש מ-Catalog של המסמך. זה הופך את "הקובץ הזה משתמש ב-800 MB" ל"אובייקט 4173, XObject של תמונה, מחזיק באופן בלעדי 612 MB", וזו עובדה שאפשר לפעול לפיה
ההבחנה בין שני המשפטים האלה היא כל העניין. גודל רדוד אומר לך כמה גדול אובייקט אחד. גודל מוחזק אומר לך כמה זיכרון היה משתחרר באמת אם האובייקט הזה היה נעלם, וזה המספר שקובע אם תיקון עוזר
מדוע שימוש זיכרון כולל אינה עובדה שאפשר לפעול לפיה?
מפני שב-PDF כמעט שום דבר לא בבעלות של דבר אחד בלבד. גופן CID מוטמע יחיד מוזכר ממילון המשאבים של כל עמוד שמשתמש בו. פרופיל ICC אחד תומך במרחב צבע ששהם זרמי תוכן שונים חולקים. XObject טופס המשמש כחותמת מופיע בכל 400 העמודים. אם מחברים בנאיביות גדלי אובייקטים לפי עמוד, סופרים את הגופן הזה 400 פעמים ומסיקים שכל עמוד עצום; אם מחלקים ב-400, מסיקים ששום דבר לא יקר והזיכרון הגיע ממקום אחר
ניתוח דומיננטור פותר את העמימות בדרך היחידה ששורדת מגע עם מסמכים אמיתיים. אובייקט X מיוחס לאובייקט הקרוב ביותר שמחזיק בו באופן בלעדי, כלומר כל נתיב הפניה מה-Catalog אל X עובר דרך אותו דומיננטור. גופן שמשותף לכל העמודים לא מיוחס לאף עמוד; הוא מיוחס לצומת הקרוב ביותר שכל אותם נתיבים עוברים דרכו, שהוא בדרך כלל ה-Catalog עצמו. גופן שמשמש עמוד אחד בלבד מיוחס לאותו עמוד. התוצאה היא ש-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 הוא זיכרון שהמסמך נושא אבל שהצופה לעולם לא יציג. בפועל הוא מגיע משלושה מקומות: עדכונים מצטברים שהחליפו גרסה קודמת של אובייקט והשאירו את המקורי מאחור, מחולל שכתב אובייקטים ואז נכשל לקשר אותם, או קובץ פגום שטבלת ההפניות הצולבות שלו שוחזרה ואספה הגדרות שדבר לא מפנה אליהן
המקרה הראשון נורמלי וצפוי, וזה בדיוק מה שעדכונים מצטברים וזרמי אובייקטים נועדו לעשות. השני והשלישי כדאי לחקור. כשחלק גדול מהבייטים המוחזקים יושב בצמתים לא נגישים, מצאת טיעון קונקרטי לכתיבה מחדש של הקובץ במקום להוסיף אליו, ויש לך את ספירת הבייטים כדי להצדיק את זמן העיבוד הנוסף בפני מי ששואל
שני מוני מקצה זיכרון שכדאי לקרוא קודם
לפני שמסיקים שמסמך גדול מטבעו, בדוק אם המנתח עצמו הוא העלות. HotPDF חושף שני מונים שמתארים איך הטעינה התנהגה ולא מה המסמך מכיל
GetLastParserArenaStatistics מדווח את ערמת הבלוקים המוחזקת המשמשת לטוקנים ולמאגרים קצרי-חיים של המנתח: RetainedBytes, PeakUsedBytes, AllocationCount, ReusedAllocationCount, TokenCount ו-TemporaryObjectElisionCount. השדה האחרון סופר מפתחות מילון שנותחו ישירות לתוך הערמה במקום דרך אובייקט שם זמני, וזו ההקצאה שנהגה לשלוט בניתוח קבצים עתירי-מילונים
GetDocumentStringInternStatistics מדווח את בריכת האחסון בהיקף המסמך שמבטלת כפילויות של שמות PDF חוזרים, אופרטורי זרם תוכן ומחרוזות בלתי משתנות עד 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 ממידע הגרף. אם המספר הזה קרוב לגידול התהליך שלך, הזיכרון נמצא במסמך והגרף יראה לך איפה. אם הוא נמוך בהרבה, הזיכרון נמצא במטמונים שלך, בביטמאפים מרונדרים, או במנתח, ומוני הערמה יגידו איזה מהם
אחר כך קח את עשרת הצמתים המובילים לפי EstimatedRetainedBytes והסתכל על ObjectType שלהם. XObjects של תמונה בראש אומרים שהקובץ עתיר סריקות ושדגימה למטה היא התיקון. תיאורי גופן בראש אומרים שגופנים מלאים הוטמעו במקום שתת-קבוצות היו מספיקות. זרמי תוכן בראש בדרך כלל אומרים גרפיקה וקטורית מיוצרת, לרוב מפות או ייצואי CAD. רק אחרי זה משתלם להסתכל על אובייקטים לא נגישים והתנהגות מקצה. בעבודה בסדר הזה, לרוב תמצא את התשובה בשני הצעדים הראשונים, ולמסמכים גדולים מאוד הגישה הזורמת המתוארת בה-Direct File API היא לעיתים קרובות התיקון המבני ולא כל אופטימיזציה לפי אובייקט
כל האבחונים האלה הם קריאות Pascal רגילות שמחזירות רשומות רגילות, כך שהן נכנסות ישירות לנתיב יומן או טלמטריה קיים. HotPDF הוא רכיב VCL מקורי ל-PDF ל-Delphi ול-C++Builder עם קוד מקור מלא; מדריך ה-API וגרסת ניסיון נמצאים בדף רכיב HotPDF ל-Delphi