مقاله فنی

یافتن حافظه‌خورهای PDF با گراف وابستگی اشیاء HotPDF

HotPDF نمایه حافظه یک PDF بارگذاری‌شده را به‌صورت یک گراف قابل پرس‌وجو در اختیار می‌گذارد: BuildLoadedObjectDependencyGraph یک گره به ازای هر شیء غیرمستقیم بازمی‌گرداند، همراه با یک اندازه سطحی تخمینی، یک اندازه نگه‌داشته‌شده مبتنی بر گره غالب، و یک پرچم برای اینکه آیا شیء هنوز از Catalog سند قابل‌دسترس است یا نه. این موضوع «این فایل 800 مگابایت مصرف می‌کند» را به «شیء 4173، یک XObject تصویری، به‌طور انحصاری 612 مگابایت را نگه داشته» تبدیل می‌کند، حقیقتی که می‌توانید بر اساس آن اقدام کنید

تفاوت بین این دو جمله کل موضوع است. اندازه سطحی می‌گوید یک شیء چقدر بزرگ است. اندازه نگه‌داشته‌شده می‌گوید اگر آن شیء از بین برود واقعاً چقدر حافظه آزاد می‌شود، همان عددی که تعیین می‌کند یک راه‌حل کمک می‌کند یا نه

چرا مصرف کل حافظه یک حقیقت قابل‌اقدام نیست؟

چون در یک PDF تقریباً هیچ‌چیز دقیقاً متعلق به یک چیز نیست. یک فونت CID جاسازی‌شده واحد از دیکشنری منابع هر صفحه‌ای که از آن استفاده می‌کند ارجاع داده می‌شود. یک جریان پروفایل ICC یک فضای رنگ را پشتیبانی می‌کند که ده جریان محتوای مختلف آن را به اشتراک می‌گذارند. یک Form 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;

هر دو سقف پارامترهای صریحی با پیش‌فرض 250000 شیء و 2000000 یال هستند. وقتی یک سند از هرکدام فراتر رود، LimitExceeded تنظیم می‌شود و گراف بازگردانده‌شده یک پیشوند کوتاه‌شده است نه یک دروغ: نتایج جزئی همراه با یک پرچم، نه مجموع‌های نادرست بی‌صدا. برای یک اجرای پزشکی‌قانونی سقف‌ها را آگاهانه بالا ببرید، و به یاد داشته باشید که این تحلیل کل گراف اشیاء را می‌پیماید، پس جایگاهش در یک مسیر تشخیصی است، نه در حلقه رندر شما

یک شیء غیرقابل‌دسترس چه چیزی به شما می‌گوید؟

شیئی که ReachableFromCatalog آن روی False تنظیم شده، حافظه‌ای است که سند حمل می‌کند اما نمایشگر هرگز آن را نشان نمی‌دهد. در عمل از سه جا می‌آید: به‌روزرسانی‌های افزایشی که یک نسخه قدیمی‌تر از یک شیء را جایگزین کرده‌اند و نسخه اصلی را پشت سر گذاشته‌اند، یک تولیدکننده که اشیائی نوشته که بعد نتوانسته آن‌ها را پیوند دهد، یا یک فایل آسیب‌دیده که جدول ارجاع متقابل آن بازسازی شده و تعاریفی را برداشته که هیچ‌چیز به آن‌ها ارجاع نمی‌دهد

مورد اول عادی و مورد انتظار است، و دقیقاً همان چیزی است که به‌روزرسانی‌های افزایشی و جریان‌های شیء برای انجامش طراحی شده‌اند. مورد دوم و سوم ارزش بررسی دارند. وقتی بخش بزرگی از بایت‌های نگه‌داشته‌شده درون گره‌های غیرقابل‌دسترس نشسته باشد، شما یک استدلال عینی برای بازنویسی فایل به‌جای ضمیمه‌کردن به آن پیدا کرده‌اید، و شمار بایت را دارید تا زمان پردازش اضافی را برای هر کسی که بپرسد توجیه کنید

دو شمارنده تخصیص‌دهنده که ارزش خواندن اول را دارند

پیش از اینکه نتیجه بگیرید یک سند ذاتاً بزرگ است، بررسی کنید که آیا خود تجزیه‌گر هزینه‌زا است. HotPDF دو شمارنده در اختیار می‌گذارد که توصیف می‌کنند بارگذاری چطور رفتار کرده، نه اینکه سند چه چیزی دارد

GetLastParserArenaStatistics در مورد arena بلوک نگه‌داشته‌شده که برای توکن‌ها و بافرهای کوتاه‌عمر تجزیه‌گر استفاده می‌شود گزارش می‌دهد: RetainedBytes، PeakUsedBytes، AllocationCount، ReusedAllocationCount، TokenCount و TemporaryObjectElisionCount. آن فیلد آخر کلیدهای دیکشنری‌ای را می‌شمارد که مستقیماً درون arena تجزیه شده‌اند به‌جای عبور از یک شیء نام موقت، همان تخصیصی که زمانی بر تجزیه فایل‌های پردیکشنری غالب بود

GetDocumentStringInternStatistics در مورد استخر intern محدوده-سند که نام‌های تکراری PDF، عملگرهای جریان محتوا و رشته‌های تغییرناپذیر تا 64 بایت را حذف تکرار می‌کند گزارش می‌دهد. آن به شما RequestCount، HitCount، MissCount، BypassCount، RetainedBytes و ReusedBytes را می‌دهد، به‌همراه سقف‌های در حال اجرا. استخر عمداً در 65536 مدخل و 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 از اطلاعات گراف شروع کنید. اگر آن عدد به رشد فرآیند شما نزدیک باشد، حافظه در سند است و گراف به شما نشان می‌دهد کجا. اگر بسیار پایین‌تر باشد، حافظه در کش‌های خودتان، در بیت‌مپ‌های رندرشده، یا در تجزیه‌گر است، و شمارنده‌های arena می‌گویند کدام‌یک

سپس ده گره برتر را بر اساس EstimatedRetainedBytes بردارید و به ObjectType آن‌ها نگاه کنید. XObjectهای تصویری در بالا یعنی فایل اسکن‌محور است و کاهش نمونه‌برداری راه‌حل است. توصیفگرهای فونت در بالا یعنی فونت‌های کامل جاسازی شده‌اند در جایی که زیرمجموعه‌ها کافی بودند. جریان‌های محتوا در بالا معمولاً یعنی گرافیک برداری تولیدشده، اغلب نقشه‌ها یا خروجی‌های CAD. فقط پس از آن است که نگاه‌کردن به اشیاء غیرقابل‌دسترس و رفتار تخصیص‌دهنده ارزش دارد. با کار در این ترتیب، معمولاً پاسخ را در دو گام اول پیدا می‌کنید، و برای اسناد بسیار بزرگ، رویکرد جریانی توضیح داده‌شده در جریان کاری Direct File API اغلب راه‌حل ساختاری است نه یک بهینه‌سازی به‌ازای هر شیء

همه این تشخیص‌ها فراخوانی‌های Pascal ساده‌ای هستند که رکوردهای ساده بازمی‌گردانند، بنابراین مستقیماً درون یک مسیر لاگ‌گیری یا تله‌متری موجود جای می‌گیرند. HotPDF یک کامپوننت بومی VCL برای PDF در Delphi و C++Builder با کد منبع کامل است؛ مرجع API و یک نسخه آزمایشی در صفحه کامپوننت PDF Delphi HotPDF موجود است