HotPDF завантажує PDF із будь-якого реалізованого вами джерела з довільним доступом, а THPDFCoalescingRandomAccessSource обгортає це джерело так, щоб розсіяні дрібні читання парсера перетворювалися на обмежений набір кешованих блокових діапазонів з асинхронним попереднім вибиранням. Для документа, що подається через HTTP range-запити, це різниця між кількома сотнями циклів запит-відповідь і кількома десятками
У самому парсері нічого не змінюється. Ви так само викликаєте LoadFromRandomAccessSource, повертається той самий об'єкт документа, і працює той самий API сторінок. Змінюється лише трафік під капотом
Чому один і той самий PDF завантажується миттєво локально, але повзе по мережі?
Тому що парсер PDF не читає файл, він навігує ним. Він переміщується в кінець для startxref, стрибає назад до таблиці перехресних посилань, розв'язує словник трейлера, переходить за посиланням до каталогу, потім до кореня дерева сторінок, потім до вузла сторінки, потім до її словника ресурсів. Кожен із цих кроків читає десятки байтів з іншого зміщення
На локальному файлі такий шаблон майже безкоштовний: операційна система вже кешувала навколишню сторінку 4 КіБ, тож друге читання коштує лише memcpy. У мережевому транспорті такої локальності немає. Кожне читання — це запит з власною затримкою, і 300 послідовних запитів по 40 мс кожен — це дванадцять секунд, витрачених майже цілком на очікування. Виправлення не в тому, щоб читати менше — парсеру потрібно рівно те, що він запитує. Виправлення в тому, щоб кожне фізичне читання покривало більше з того, чого захоче наступне логічне читання
Що змінює об'єднання
Джерело з об'єднанням округлює кожне читання до блоку й кешує цей блок. BlockSize за замовчуванням становить 262 144 байти, а MaxCacheBytes — 2 097 152, тож за замовчуванням у пам'яті резидентні вісім блоків, і вони витісняються за порядком «найдавніше використаний» проти жорсткого байтового бюджету. 40-байтове читання ключа трейлера парсером підтягує 256 КіБ навколо нього, і наступний десяток читань у цій околиці, де й живуть дані перехресних посилань та каталогу, обслуговується з пам'яті
Ваше власне джерело залишається простим. Реалізуйте GetSize і ReadAt, перевизначте ReadAtCancellable, якщо ваш транспорт може перервати передачу на льоту, і дозвольте обгортці подбати про кешування, об'єднання та попереднє вибирання
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: обгортка звільняє 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, і закриття документа користувачем справді зупиняє мережевий трафік, а не чекає, поки він виллється сам. Та сама модель токенів лежить в основі черги запитів, описаної в статті про фонове рендерування з чергою запитів, тож один токен може покривати весь шлях від вьюпорту до сокета
Читання статистики кешу діапазонів
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 і перестаньте платити за пропускну здатність, яку ви відкидаєте. Багато промахів при піковому вікні, що досі дорівнює 1, означає, що допуск відхиляє шаблон, який фактично послідовний, тож підвищіть SequentialReadToleranceBytes. А кількість прочитаних байтів, що значно перевищує розмір файлу, означає, що кеш «б'ється», тож підвищіть MaxCacheBytes, перш ніж чіпати щось інше
Лінеаризовані файли змінюють арифметику
Якщо ви керуєте виробником документів, лінеаризація документа не оптимізує проблему, а змінює її. Лінеаризований PDF розміщує об'єкти першої сторінки та таблицю підказок на початку файлу, тож переглядач може відрендерити першу сторінку з початкового мегабайта, не бачачи решти. HotPDF надає цей шлях безпосередньо через GetProgressiveLinearizedLoadInfo і ReadProgressiveLinearizedFirstPageSection, а сторона запису розглянута в статті про генерування лінеаризованих PDF із таблицями підказок
Обидві техніки поєднуються. Об'єднання робить будь-який документ прийнятним по повільному каналу; лінеаризація забезпечує швидке надходження першої сторінки для документів, які ви виробляєте самі. Для файлів, що живуть на локальному диску, але завеликі, щоб вміститися в пам'яті, шляхи через відображений файл і ліниві потоки, описані в статті про робочий процес прямого файлового API, зазвичай є кращим інструментом, адже там немає затримки циклу запит-відповідь, яку взагалі потрібно амортизувати
HotPDF — це нативний VCL-компонент PDF для Delphi та C++Builder, без зовнішньої DLL для парсера й з доступним повним вихідним кодом. API джерела з довільним доступом, обгортка з об'єднанням та точки входу прогресивного завантаження задокументовані на сторінці HotPDF Delphi PDF component