مقاله فنی

جریان‌سازی PDF از راه دور در Delphi: ادغام بازه HotPDF

HotPDF یک PDF را از هر منبع دسترسی تصادفی که خودتان پیاده‌سازی کنید بارگذاری می‌کند، و THPDFCoalescingRandomAccessSource آن منبع را می‌پیچد تا خواندن‌های کوچک و پراکنده تجزیه‌گر به یک مجموعه محدود از بازه‌های بلوکی کش‌شده با پیش‌واکشی ناهمگام تبدیل شوند. روی سندی که از طریق درخواست‌های بازه HTTP سرو می‌شود، این تفاوت بین چند صد رفت‌وبرگشت و چند دوجین رفت‌وبرگشت است

هیچ‌چیز درباره تجزیه‌گر تغییر نمی‌کند. همچنان LoadFromRandomAccessSource را فراخوانی می‌کنید، همان شیء سند بازمی‌گردد، و همان API صفحه کار می‌کند. آنچه تغییر می‌کند ترافیک زیرین است

چرا همان PDF به‌صورت محلی فوری بارگذاری می‌شود اما روی شبکه به‌کندی پیش می‌رود؟

چون یک تجزیه‌گر PDF یک فایل را نمی‌خواند، آن را می‌پیماید. برای startxref به انتها می‌رود، به جدول ارجاع متقابل بازمی‌گردد، دیکشنری trailer را حل می‌کند، یک ارجاع به Catalog را دنبال می‌کند، سپس به ریشه درخت صفحه، سپس به یک گره صفحه، سپس به دیکشنری منابع آن. هرکدام از این گام‌ها ده‌ها بایت را از یک آفست متفاوت می‌خواند

روی یک فایل محلی، این الگو تقریباً رایگان است: سیستم‌عامل از پیش صفحه 4 کیلوبایتی اطراف را کش کرده، بنابراین خواندن دوم فقط هزینه یک memcpy دارد. روی یک انتقال شبکه‌ای چنین محلی‌بودنی وجود ندارد. هر خواندن یک درخواست با تأخیر خودش است، و 300 درخواست پیاپی هر کدام 40 میلی‌ثانیه، دوازده ثانیه است که تقریباً به‌طور کامل صرف انتظار می‌شود. راه‌حل کمتر خواندن نیست؛ تجزیه‌گر دقیقاً به آنچه می‌خواهد نیاز دارد. راه‌حل این است که هر خواندن فیزیکی بخش بیشتری از آنچه خواندن منطقی بعدی می‌خواهد را پوشش دهد

ادغام چه چیزی را تغییر می‌دهد

منبع ادغام‌کننده هر خواندن را تا یک بلوک گرد می‌کند و آن بلوک را کش می‌کند. BlockSize پیش‌فرض 262144 بایت دارد و MaxCacheBytes پیش‌فرض 2097152 بایت، پس به‌طور پیش‌فرض هشت بلوک مقیم هستند و به ترتیب کمترین‌استفاده‌اخیر در برابر یک بودجه بایتی سخت‌گیرانه بیرون رانده می‌شوند. خواندن 40 بایتی تجزیه‌گر از یک کلید trailer، 256 کیلوبایت اطراف آن را می‌کشد، و دوجین خواندن بعدی در آن همسایگی، همان‌جا که داده جدول ارجاع متقابل و catalog زندگی می‌کند، از حافظه سرو می‌شوند

منبع خودتان ساده باقی می‌ماند. GetSize و ReadAt را پیاده‌سازی کنید، اگر انتقال شما می‌تواند در حین پرواز لغو شود ReadAtCancellable را بازنویسی کنید، و اجازه دهید wrapper کش‌کردن، ادغام و پیش‌واکشی را مدیریت کند

type
  THttpRangeSource = class(THPDFRandomAccessSource)
  private
    FClient: TMyHttpClient;
    FUrl: string;
    FSize: Int64;
  public
    function GetSize: Int64; override;
    function ReadAt(Offset: Int64; var Buffer; Count: Longint): Longint; override;
    function ReadAtCancellable(Offset: Int64; var Buffer; Count: Longint;
      CancellationToken: THPDFCancellationToken): Longint; override;
  end;

var
  Raw: THttpRangeSource;
  Cached: THPDFCoalescingRandomAccessSource;
  Pdf: THotPDF;
begin
  Raw := THttpRangeSource.Create('https://files.example.com/contract.pdf');
  // OwnsSource=True: wrapper خود Raw را همراه خودش آزاد می‌کند
  Cached := THPDFCoalescingRandomAccessSource.Create(Raw, True, 262144, 8388608);
  Pdf := THotPDF.Create(nil);
  try
    Cached.AsyncPrefetchEnabled := True;
    Cached.AdaptiveReadAheadEnabled := True;
    Cached.MaxReadAheadBlocks := 8;

    if Pdf.LoadFromRandomAccessSource(Cached, True) = 1 then
      RenderFirstPage(Pdf);
  finally
    Pdf.Free;
  end;
end;

تا کجا باید پیش‌خوانی کند؟

پیش‌خوانی تطبیقی این سؤال را برای هر سند جداگانه پاسخ می‌دهد به‌جای اینکه شما را مجبور به حدس‌زدن کند. با فعال‌بودن AdaptiveReadAheadEnabled، پنجره از میان 1، 2، 4 و 8 بلوک رشد می‌کند همان‌طور که خواندن‌های رو-به-جلوی پایدار انباشته می‌شوند، و هرگز از MaxReadAheadBlocks یا ظرفیت کش پیکربندی‌شده فراتر نمی‌رود. لحظه‌ای که یک خواندن برسد که تقریباً جایی نیست که خواندن قبلی تمام شده، پنجره فرومی‌ریزد و پیش‌واکشی سرکوب می‌شود

SequentialReadToleranceBytes، با پیش‌فرض 4096، همان «تقریباً» را تعریف می‌کند. خواندن‌هایی که در آن فاصله از پایان خواندن قبلی فرود می‌آیند همچنان پی‌درپی محسوب می‌شوند، که اهمیت دارد چون یک تجزیه‌گر PDF که یک جریان محتوا را می‌پیماید آفست‌های کاملاً پیوسته تولید نمی‌کند؛ آن یک فیلد طول را اینجا و یک دیکشنری درون‌خطی را آنجا رد می‌کند. اگر تحمل را خیلی پایین تنظیم کنید یک اسکن رو-به-جلوی معمولی به‌عنوان تصادفی طبقه‌بندی می‌شود، پس پیش‌خوانی هرگز فعال نمی‌شود. اگر آن را خیلی بالا تنظیم کنید، دسترسی تصادفی واقعی پی‌درپی به نظر می‌رسد، پس مگابایت‌هایی را واکشی می‌کنید که کسی نمی‌خواهد. پیش‌فرض برای پیمایش جریان محتوا کالیبره شده، و آمار به شما می‌گوید اگر انتقال شما مخالف باشد

این عدم‌تقارن عمدی است: رشد تدریجی است، فروپاشی فوری است. واکشی بیش‌ازحد روی یک بار کاری دسترسی تصادفی، پهنای‌باند واقعی و پول واقعی روی انتقال‌های اندازه‌گیری‌شده هزینه می‌کند، پس اشتباه ارزان بر اشتباه گران‌قیمت ترجیح داده می‌شود

لغوی که واقعاً انتقال را متوقف می‌کند

کلاس پایه ReadAtCancellable را اعلام می‌کند، و منبع ادغام‌کننده آن را سراسر رعایت می‌کند. وقتی یک خواندن پیش‌زمینه برای بازه‌ای می‌رسد که یک پیش‌واکشی در حال پرواز آن را سرو نمی‌کند، آن پیش‌واکشی به‌جای رها شدن برای تمام‌شدن، لغو می‌شود، پس درخواست صفحه کاربر پشت ترافیک حدسی صف نمی‌شود. پیاده‌سازی پیش‌فرض روی THPDFRandomAccessSource به یک ReadAt ساده بازمی‌گردد، که یعنی این ویژگی به‌ازای هر انتقال اختیاری است: کلاینت‌های HTTP که لغو درخواست را پشتیبانی می‌کنند لغو واقعی دریافت می‌کنند، و منابع ساده‌تر بدون تغییر کار می‌کنند

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

خواندن آمار کش بازه

GetStatistics یک رکورد THPDFRangeCacheStatistics را پر می‌کند که کاری که انتقال شما انجام داده را از کاری که کش انجام داده جدا می‌کند. SourceReadCount و SourceBytesRead ترافیک فیزیکی هستند. CacheHitCount و CacheMissCount ترافیک منطقی هستند. SequentialReadCount و RandomReadCount نشان می‌دهند الگوی دسترسی چطور طبقه‌بندی شده، CurrentReadAheadBlocks و PeakReadAheadBlocks نشان می‌دهند پنجره چقدر باز شده، و PrefetchRequestCount، PrefetchCompletedCount، PrefetchCancelledCount و SuppressedPrefetchCount نشان می‌دهند آیا حدس‌زدن نتیجه داده یا نه

var
  S: THPDFRangeCacheStatistics;
begin
  Cached.GetStatistics(S);
  Log(Format('physical %d reads / %d bytes, hits %d, misses %d',
    [S.SourceReadCount, S.SourceBytesRead, S.CacheHitCount, S.CacheMissCount]));
  Log(Format('pattern: %d sequential, %d random, peak window %d blocks',
    [S.SequentialReadCount, S.RandomReadCount, S.PeakReadAheadBlocks]));
  Log(Format('prefetch: %d issued, %d completed, %d cancelled, %d suppressed',
    [S.PrefetchRequestCount, S.PrefetchCompletedCount,
     S.PrefetchCancelledCount, S.SuppressedPrefetchCount]));
end;

سه خوانش به شما می‌گویند چه چیزی را تغییر دهید. تعداد زیاد پیش‌واکشی‌های لغوشده همراه با شمار بالای خواندن تصادفی یعنی سند خارج از ترتیب دسترسی می‌شود، پس MaxReadAheadBlocks را پایین بیاورید و از پرداخت هزینه برای پهنای‌باندی که دور می‌ریزید دست بکشید. تعداد زیاد miss با پنجره‌ای که هنوز روی 1 است یعنی تحمل، الگویی را که عملاً پی‌درپی است رد می‌کند، پس SequentialReadToleranceBytes را بالا ببرید. و بایت‌های خوانده‌شده که به‌مراتب از اندازه فایل فراتر می‌روند یعنی کش در حال تلاطم است، پس پیش از دست‌زدن به هرچیز دیگر، MaxCacheBytes را بالا ببرید

فایل‌های خطی‌شده حساب‌وکتاب را تغییر می‌دهند

اگر بر تولیدکننده کنترل دارید، خطی‌سازی سند مشکل را تغییر می‌دهد نه اینکه آن را بهینه کند. یک PDF خطی‌شده اشیاء صفحه اول و یک جدول راهنما را در ابتدای فایل قرار می‌دهد، پس یک نمایشگر می‌تواند صفحه یک را از مگابایت آغازین رندر کند بدون دیدن باقی فایل. HotPDF آن مسیر را مستقیماً از طریق GetProgressiveLinearizedLoadInfo و ReadProgressiveLinearizedFirstPageSection در اختیار می‌گذارد، و سمت نوشتن در تولید PDFهای خطی‌شده با جدول‌های راهنما پوشش داده شده است

هر دو تکنیک با هم ترکیب می‌شوند. ادغام هر سندی را روی یک لینک کند قابل‌تحمل می‌کند؛ خطی‌سازی باعث می‌شود صفحه اول روی اسنادی که خودتان تولید می‌کنید سریع برسد. برای فایل‌هایی که روی یک دیسک محلی زندگی می‌کنند اما برای نگه‌داشتن در حافظه خیلی بزرگ‌اند، مسیرهای فایل‌نگاشته‌شده و جریان تنبل توضیح داده‌شده در جریان کاری Direct File API معمولاً ابزار بهتری هستند، چون از ابتدا هیچ تأخیر رفت‌وبرگشتی برای استهلاک وجود ندارد

HotPDF یک کامپوننت بومی VCL برای PDF در Delphi و C++Builder است، بدون هیچ DLL خارجی برای تجزیه‌گر و با کد منبع کامل در دسترس. API منبع دسترسی تصادفی، wrapper ادغام‌کننده و نقاط ورودی بارگذاری تدریجی در صفحه کامپوننت PDF Delphi HotPDF مستند شده‌اند