2 GBの走査アーカイブがS3バケットにあり、ユーザーは900ページを求めています。PDFlibPasはファイルをダウンロードせずにそのページを提供できます。LoadFromRangeSourceがあなた自身のバイトレンジコールバックの上に読み取り専用のシーク可能ストリームを組み立て、TPDFDocumentへ渡すため、パーサーはクロスリファレンステーブルと、ページツリーの一本の枝と、一つのコンテンツストリームだけを引っ張ります
トランスポート側は古くて退屈な話です。HTTPサーバーはバイトレンジを何十年も告知してきており、今やRFC 9110 §14で規定され、オブジェクトストアはどこも同じ方言を話します。PDF側も同様に決着しています。ISO 32000-1 §7.5.8がリニアライズを定義したのはまさに、リーダーがファイルの先頭から最初のページを描けるようにするためです。Delphiで欠けていたのは中間の部品、どのレンジを尋ね、いくつ保持し、二度尋ねないためにはどうするかを決める部分です
LoadFromRangeSourceはトランスポートに何を求めるのか
二つであり、どちらもストリームではありません。PDFlibPasが求めるのは権威あるSourceSizeと、function(Sender: TObject; Offset: Int64; Buffer: Pointer; Count: LongInt): LongInt of objectと宣言されるTPDFlibRangeReadEvent型の同期読み取りコールバックです。内部的にこの組はSourceSizeとReadRangeを公開するTCallbackByteRangeSourceになり、所有権が文書へ渡るストリームに包まれます。コールバックのターゲットとそのバックエンドはあなたのままです。文書はクローズ、クリア、リロードのときにラッパーを解放しますが、メソッドポインターの背後にあるトランスポートオブジェクトには決して触れません
契約は一方の方向に意図的に寛容で、もう一方では厳密です。読み取りの短さは合法であり、単にパーサーが再び尋ねるという意味です。例外を投げるコールバックは短い読み取りに変換され、通常のロード失敗経路を通って収束します。書き込んだのが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
{ Range: bytes=Offset-(Offset+Count-1) の一回のブロッキングGET }
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に整合させ、ミスごとにちょうど一チャンクを取得し、複数ウィンドウにわたって厳密なバイト予算を強制します。渡された明示的予算は少なくとも一チャンク分まで引き上げられるため、一度の読み取りは常にチャンク単位で進み、キャッシュ負荷のピークは予測可能なままです。4096未満のChunkSizeは64 KiBの既定値へフォールバックします
再読み取りの計上は、テレメトリーに配線する価値のある部分です。PDFlibPasは再読み取りを整合されたチャンク先頭で識別し、順序付きの連続区間を保持します。これにより、退去後の再取得を真の初回取得から分けつつ、帳簿がファイルサイズに比例して育つのを防ぎます。GetRangeSourceCacheInfoは全体像をJSONで返し、SetRangeSourceCacheLimitは実行時に予算を再設定し、ClearRangeSourceCacheはウィンドウを落とし統計を一緒にリセットします。実行時の予算縮小は履歴を保持し、予算駆動の解放を退去として数えるため、hitsが横ばいのままrepeatedReadsが上昇するのは、ワーキングセットがもう収まらないという信号です
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ヘッダーを補正し、全レンジが取得なしの探りを通った後でのみオブジェクトを解析します。欠落経路があなたの読み取りコールバックを呼ぶことは決してありません
走査は全数ではなく範囲を絞って行われます。ページ照会はターゲットページを含むページツリーの枝だけを歩き、そこからページコンテンツ、リソース、注釈、継承されたページ属性を加えます。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、可用性照会、先行取得スケジューラーは、Delphi、C++Builder、Free Pascal向けの標準PDFlibPas Delphi PDF Libraryの一部です。製品ページは先行取得の優先度と状態定数とともに、LoadFromRangeSourceのパラメータリファレンス全文を載せています