Архив от 2 GB сканирани документи живее в S3 кофа и потребителят иска страница 900. PDFlibPas може да подаде тази страница, без да тегли файла: LoadFromRangeSource строи четим само, позиционируем поток върху вашето собствено обратно повикване за байтови диапазони и го подава на TPDFDocument, така че парсерът дърпа таблиците за кръстосана справка, един клон на дървото от страници и един съдържателен поток
Транспортната страна на това е стара и скучна. HTTP сървърите рекламират байтови диапазони от десетилетия, вече специфицирани в RFC 9110 §14, а всяко обектно хранилище говори същия диалект. Страната на 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 MiB, разпределени по подравнени към части прозорци и изхвърляни LRU. По-ранният дизайн с един прозорец растеше до каквато дължина поиска извикващият, така че едно голямо последователно четене можеше да прелее номиналния размер на частта, докато скок на случаен принцип хвърляше предишния прозорец веднага. Текущият кеш подравнява всяко изходно отместване към ChunkSize, взима точно една част на промах и налага твърд байтов бюджет върху няколко прозореца. Всеки изричен бюджет, подаден от вас, се вдига поне на една пълна част, така че едно четене винаги върви част по част и пикът на кеш товар остава предвидим. ChunkSize под 4096 пада обратно на стандартните 64 KiB
Отчитането на повторни четения е частта, която си струва да включите в телеметрията си. 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 MiB, така че взрив от успоредна странична работа не се усилва нито в дублирани дребни заявки, нито в една абсурдно голяма. Прозорецът за сливане по подразбиране е 2 ms и важи само за първата липсваща част на всеки ReadAt; позиционният Read никога не чака него, а подаването на нула премахва съвсем началното забавяне на събирането, което има значение за дълги последователни сканирования, които иначе трупат чакането част по част. Позицията, метаданните на кеша и изходните четения стоят зад три отделни заключвания, а самото обратно повикване към източника е серийно, което позволява адаптер към база данни или обектно хранилище без вътрешна защита на нишки да се използва непроменен. Чакащите получават собствено копие на данните, така че по-късно LRU изхвърляне не може да опорочи буфер, вече връчен
Може ли да попитате дали страница 900 е готова, без да я теглите?
Да, и точно за това е опционалното обратно повикване за наличност. Обикновено четящо обратно повикване не може да разграничи байтове, вече пристигнали, от байтове, изискващи блокираща обиколка, а сондиране с пробно четене би задействало точно тегленето, което се опитвате да избегнете. TPDFlibRangeAvailabilityEvent отговаря на един-единствен въпрос — дали пълен диапазон може да бъде прочетен веднага — и му е забранено да тегли каквото и да е; байтове, които кешът вече покрива, винаги се броят за налични. GetRangeSourceDataAvailability съпоставя непряките обекти с физическите диапазони на хранилището, записани в записите за кръстосана справка, разрешава компресираните обекти към контейнера им обектен поток, коригира за местен PDF header и анализира обект само след пълният диапазон мине несондиращата проба, така че липсващият път никога не вика вашето четящо обратно повикване
Обхождането е обхванато, а не изчерпателно. Станична заявка минава само по клона на дървото от страници, съдържащ целевата страница, и после добавя станично съдържание, ресурси, анотации и наследени станични атрибути, прескачайки обратните ребра Parent и P, така че една страница или уиджет не може да се разшири назад към целия документ. Обектната графика е ограничена на 100000 поискани обекта и дълбочина 256, потоковите обекти се разчитат първо речникът, а резервен вариант с пълен анализ се позволява само за складирани обекти до 4 MiB. 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;
Защо префетчът трябва да итерира
Защото прочитането на текущите missingRanges веднъж не прави страницата налична. Липсващ възел на дървото от страници или обектен поток разкрива следващия слой зависимости едва след като пристигне, така че префетч задача на 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 MiB, така и от текущия кеш бюджет, така че загряването не може веднага да изхвърли голяма част от себе си. Нелинеаризиран файл все пак се разрешава през трейлъра и веригата за кръстосана справка близо до края, което струва няколко допълнителни обиколки, а не бедствие. Истинският откос е повреден файл, който принуждава пътя на поправка, защото възстановяването на таблица за кръстосана справка означава сканиране за обектни заглавки из целия документ, а това е пълно теглене, пристигащо част по част. Закъснението е другият честен лимит: при 60 ms на заявка парс с произволен достъп, нуждаещ се от четиридесет некеширани части, прекарва над две секунди в транзит, колкото и добър да е кешът, което е точно това, за което съществуват аргументът за четене напред и приоритетната опашка. Същата дисциплина личи в подхода с директен достъп за сливане и рязане на големи PDF файлове, а този кеш стои под успоредното изобразяване на страници и под дисковия станичен кеш на четеца еднакво
API на източника от диапазони, запитването за наличност и планировчикът на префетч са част от стандартната PDFlibPas Delphi PDF Library за Delphi, C++Builder и Free Pascal; продуктовата страница носи пълната справка за параметри на LoadFromRangeSource заедно с константите за приоритет и състояние на префетч