Архів сканів на 2 ГБ лежить у S3-бакеті, і користувач хоче сторінку 900. PDFlibPas може віддати ту сторінку, не завантажуючи файл: LoadFromRangeSource будує доступний лише для читання потік із позиціонуванням над вашим власним колбеком байтових діапазонів і передає його TPDFDocument, тож парсер тягне таблиці перехресних посилань, одну гілку дерева сторінок і один потік вмісту
Транспортна сторона цього стара і нудна. HTTP-сервери рекламують байтові діапазони десятиліттями, тепер специфіковані в RFC 9110 §14, і кожен object store говорить тим самим діалектом. PDF-сторона так само усталена: ISO 32000-1 §7.5.8 визначає лінеаризацію саме для того, щоб читач міг відрендерити першу сторінку з початку файлу. Чого бракувало в Delphi, то це шматка посередині, частини, що вирішує, які діапазони просити, скільки їх тримати і як не просити двічі
Що LoadFromRangeSource потребує від вашого транспорту?
Дві речі, і жодна з них не є потоком. PDFlibPas просить авторитетний SourceSize і синхронний колбек читання типу TPDFlibRangeReadEvent, оголошений як function(Sender: TObject; Offset: Int64; Buffer: Pointer; Count: LongInt): LongInt of object. Всередині пара стає TCallbackByteRangeSource, що відкриває SourceSize і ReadRange, загорнутим у потік, чиє володіння переходить до документа. Ваш цільовий обʼєкт колбека і його бекенд лишаються вашими: документ звільняє обгортку при закритті, очищенні або перезавантаженні, але ніколи не торкається транспортного обʼєкта за вказівником методу
Контракт навмисно поблажливий в одному напрямку і строгий в іншому. Коротке читання легальне і просто означає, що парсер спитає ще раз. Колбек, що кидає виключення, конвертується в коротке читання і сходиться через звичайний шлях невдачі завантаження. Колбек, що заявляє про запис більше ніж Count байтів, затискається, бо багуватий провайдер не повинен мати змогу перебігти буфер кешу. Повторні спроби пароля будують свіжий потік діапазонів і свіжий стан розбору над тим самим джерелом-колбеком, тож невдала спроба не може лишити за собою застарілу позицію, вікно чи стан розшифрування
type
TObjectStoreSource = class
private
FClient: TRangeHttpClient;
FSize: Int64;
public
function ReadRange(Sender: TObject; Offset: Int64;
Buffer: Pointer; Count: LongInt): LongInt;
function IsResident(Sender: TObject; Offset: Int64;
Count: LongInt): Integer;
property Size: Int64 read FSize;
end;
function TObjectStoreSource.ReadRange(Sender: TObject; Offset: Int64;
Buffer: Pointer; Count: LongInt): LongInt;
begin
{ один блокуючий GET з Range: bytes=Offset-(Offset+Count-1) }
Result := FClient.FetchInto(Offset, Count, Buffer);
end;
{ ... }
Lib := TPDFlib.Create;
Src := TObjectStoreSource.Create(BucketUrl);
try
if Lib.LoadFromRangeSource(Src.Size, Src.ReadRange, '',
65536, 8 * 1024 * 1024, 2, Src.IsResident) = 1 then
Lib.SelectPage(900);
finally
Lib.Free; { звільняє потік-обгортку }
Src.Free; { ваш транспорт, ваш час життя }
end;
Скільки насправді тримає кеш діапазонів?
За замовчуванням 4 МіБ, розкладені по вирівняних на чанки вікнах і витіснявані за LRU. Попередній дизайн з одним вікном розростався до будь-якої довжини, яку просив викликач, тож одне велике послідовне читання могло перелетіти номінальний розмір чанка, тоді як випадковий стрибок негайно викидав попереднє вікно. Поточний кеш вирівнює кожен вихідний зсув до ChunkSize, тягне рівно один чанк за промах і застосовує жорсткий байтовий бюджет крізь кілька вікон. Будь-який явний бюджет, який ви передаєте, підіймається щонайменше до одного повного чанка, тож одне читання завжди просувається чанк за чанком, а пікове навантаження кешу лишається передбачуваним. ChunkSize нижчий за 4096 падає до типового 64 КіБ
Облік повторних читань — та частина, варта підключення до вашої телеметрії. PDFlibPas розпізнає повтор за вирівняним стартом чанка і тримає упорядковані суміжні інтервали, що відділяє справжнє перше витягування від повторного після витіснення, уникаючи зростання обліку лінійно з розміром файлу. GetRangeSourceCacheInfo повертає всю картину як JSON, SetRangeSourceCacheLimit змінює бюджет у рантаймі, а ClearRangeSourceCache скидає вікна і статистику разом. Звуження бюджету в рантаймі зберігає історію і рахує бюджетно-примушені вивільнення як витіснення, тож зростаючий repeatedReads проти плоского hits — ваш сигнал, що робочий набір більше не вміщується
var
Info: WideString;
begin
Lib.SetRangeSourceCacheLimit(16 * 1024 * 1024);
Lib.SelectPage(900);
if Lib.GetRangeSourceCacheInfo(Info) = 1 then
{ "windowCount", "cacheLimitBytes", "cachedBytes", "hits", "misses",
"evictions", "sourceReads", "sourceBytes", "repeatedReads",
"coalescedRequests", "coalescedSourceReads" }
LogRangeStats(Info);
end;
Що буває, коли кілька ниток хочуть той самий чанк?
Вони чекають на один запит, а не на кілька. Класичний TStream має єдину каретку позиції, і дві нитки, що кожна правильно блокується, все одно можуть мати ту позицію переписаною між Seek і Read, тож ліниві обʼєкти і сегментовані читання в PDFlibPas використовують абсолютне ReadAt, яке ніколи не рухає каретку. Кожен вирівняний чанк отримує один запит у польоті, який поділяють усі викликачі того чанка, суміжні чанки в черзі зливаються до старту читання джерела, а одне фізичне читання обмежене 16 МіБ, тож спалах паралельної сторінкової роботи не посилюється ані в дубльовані дрібні запити, ані в один абсурдно великий. Вікно злиття типове 2 мс і застосовується лише до першого відсутнього чанка кожного ReadAt; позиційне Read ніколи не чекає його, а передача нуля прибирає початкову затримку збору повністю, що важливо для довгих послідовних сканів, які інакше накопичували б очікування чанк за чанком. Позиція, метадані кешу і читання джерела сидять за трьома окремими блокуваннями, а колбек джерела сам серіалізований, і саме це дозволяє адаптер бази даних чи object store без внутрішнього ниткового захисту використовувати без змін. Очікувачі отримують власну копію даних, тож пізніше LRU-витіснення не може інвалідувати буфер, який уже вручено
Чи можна спитати, чи сторінка 900 готова, не витягуючи її?
Так, і саме для того існує опціональний колбек доступності. Звичайний колбек читання не може відрізнити байти, що вже приземлилися, від байтів, що потребують блокуючого круглого виходу, а проба пробним читанням спровокувала б саме те завантаження, якого ви уникаєте. TPDFlibRangeAvailabilityEvent відповідає лише на одне питання, чи можна негайно прочитати повний діапазон, і їй заборонено що-небудь витягувати; байти, які кеш уже покриває, завжди рахуються доступними. GetRangeSourceDataAvailability відображає непрямі обʼєкти на фізичні діапазони сховища, записані в записах перехресних посилань, розвʼязує стиснуті обʼєкти до їх контейнера object stream, коригує на зсунутий PDF-заголовок і розбирає обʼєкт лише після того, як повний діапазон пройде незбираючу пробу, тож відсутній шлях ніколи не викликає ваш колбек читання
Обхід має окреслені межі, а не вичерпний. Запит сторінки проходить лише гілку дерева сторінок, що містить цільову сторінку, і потім додає вміст сторінки, ресурси, анотації та успадковані атрибути сторінки, пропускаючи зворотні ребра Parent і P, тож одна сторінка чи віджет не може розгорнутися назад у весь документ. Граф обʼєктів обмежений 100000 запитуваних обʼєктів і глибиною 256, потокові обʼєкти розбираються словник-першими, а повний резервний розбір дозволений лише для збережених обʼєктів до 4 МіБ. JSON-звіт зливає перекривні та суміжні інтервали перед підрахунком, тож requiredBytes і missingBytes обчислюються зі злитих масивів requiredRanges і missingRanges, чий end — включна кінцева точка. Запит обʼєкта, що вже доступний, може наповнити кеш діапазонів; запит відсутнього лишає статистику читання недоторканою
var
Report: WideString;
Status: Integer;
begin
Status := Lib.GetRangeSourceDataAvailability(PDF_RANGE_DATA_PAGE, 900,
Report);
if Status = PDF_RANGE_DATA_AVAILABLE then
RenderPageNow
else if Status = PDF_RANGE_DATA_NOT_AVAILABLE then
{ Report несе "missingBytes" плюс злиті "missingRanges" }
ShowProgress(Report)
else if Status = PDF_RANGE_DATA_NOT_PRESENT then
ShowMissingFeature; { напр. у файлі взагалі немає AcroForm }
end;
Чому prefetch мусить ітерувати
Бо одноразове читання поточних missingRanges не робить сторінку доступною. Відсутній вузол дерева сторінок чи object stream розкриває лише наступний шар залежностей після свого прибуття, тож prefetch-робота PDFlibPas крутить цикл запит–витягування–перепит до повної доступності сторінки, форми чи графа обʼєктів або до зупинки лімітом байтів чи проходів. Робота використовує власного читача і малий вторинний кеш, чиє джерело даних переадресує абсолютні читання початковому потоку діапазонів, що тримає стан розбору ізольованим від переднього TSmartPDFReader, тоді як байти, які вона справді завантажує, все одно приземляються в спільний головний кеш. На кожен потік діапазонів існує одна робоча нитка, що відповідає серіалізації, якої вже вимагає колбек джерела, а черга обирає за чотирма рівнями пріоритету і потім за порядком подання в межах рівня. MaxBytes стягується у фізичних байтах чанків, тож парсер, що просить один байт усередині некешованого чанка, все одно платить за весь чанк, тоді як чанки, вже в спільному кеші, не коштують роботі нічого. Скасування чергової роботи досягає термінального стану з нульовими читаннями джерела; робоча перевіряється перед кожним проходом залежностей і кожним чанком джерела, а звільнення потоку діапазонів чекає на колбек у польоті, замість намагатися його перервати
var
Job: Integer;
Info: WideString;
begin
Job := Lib.StartRangeSourcePrefetch(PDF_RANGE_DATA_PAGE, 901,
PDF_RANGE_PREFETCH_PRIORITY_HIGH, 8 * 1024 * 1024, 65536);
if Lib.WaitForRangeSourcePrefetch(Job, 5000) =
PDF_RANGE_PREFETCH_STATE_COMPLETED then
PrepareNextPage
else
Lib.CancelRangeSourcePrefetch(Job);
{ "passes", "plannedRanges", "sourceReads", "fetchedBytes" і останній
повний звіт доступності, тож LIMIT_REACHED лишається виразним
від FAILED }
Lib.GetRangeSourcePrefetchInfo(Job, Info);
end;
Де це деградує у завантаження цілого файлу
Завантаження діапазонами — ставка на компонування файлу, і деякі файли її не шанують. Лінеаризований файл за ISO 32000-1 §7.5.8 — добрий випадок: секція першої сторінки прогрівається при відкритті, обмежена і наявним порогом безпеки 4 МіБ, і поточним бюджетом кешу, тож прогрів не може негайно витіснити більшу частину себе. Нелінеаризований файл усе одно розвʼязується крізь трейлер і ланцюг перехресних посилань ближче до кінця, що коштує пари зайвих кругових виходів, а не катастрофи. Справжня скеля — пошкоджений файл, що змушує шлях ремонту, бо реконструкція таблиці перехресних посилань означає сканування заголовків обʼєктів крізь весь документ, а це повне завантаження, що прибуває по одному чанку за раз. Затримка — друга чесна межа: при 60 мс на запит парсер випадкового доступу, якому потрібно сорок некешованих чанків, проводить у дорозі понад дві секунди, наскільки б добрим не був кеш, — і саме те, що приховувати покликані аргумент read-ahead і черга пріоритетів. Та сама дисципліна проявляється в підході прямого доступу до злиття та розбиття великих PDF, і цей кеш лежить під паралельним рендерингом сторінок і дисковим кешем сторінок переглядача однаково
API джерела діапазонів, запит доступності і планувальник prefetch — частина стандартної PDFlibPas Delphi PDF Library для Delphi, C++Builder і Free Pascal; продуктова сторінка несе повний довідник параметрів LoadFromRangeSource разом із константами пріоритету та станів prefetch