Egy 2 GB-os szkennelt archívum egy S3 bucketben él, és a felhasználó a 900. oldalt akarja. A PDFlibPas kiszolgálhatja azt az oldalt a fájl letöltése nélkül: a LoadFromRangeSource csak olvasható, pozicionálható streamet épít a saját bájttartomány-visszahívása felett, és átadja a TPDFDocument-nek, így az elemző a kereszthivatkozási táblákat, egy oldalfaágat és egy tartalomstreamet húz le
Ennek szállítási oldala régi és unalmas. A HTTP kiszolgálók évtizedek óta hirdetik a bájttartományokat, ma az RFC 9110 §14 szabja meg őket, és minden objektumtároló ugyanazt a nyelvjárást beszéli. A PDF oldal ugyanúgy rendezett: az ISO 32000-1 §7.5.8 pontosan definiálja a linearizációt, hogy egy olvasó az első oldalt a fájl elejéről renderelhesse. Ami Delphiben hiányzott, az a középső darab, az a rész, amely eldönti, mely tartományokat kérje, hányat tartson, és hogyan kerülje el a kétszeri kérdezést
Mit kér a LoadFromRangeSource a szállítási rétegtől?
Két dolgot, és egyik sem stream. A PDFlibPas egy tekintélyes SourceSize-t kér és egy szinkron olvasási visszahívást, TPDFlibRangeReadEvent típusút, amelyet így deklarálnak: function(Sender: TObject; Offset: Int64; Buffer: Pointer; Count: LongInt): LongInt of object. Belsőleg a pár TCallbackByteRangeSource-t válik, amely a SourceSize-t és a ReadRange-et tárja fel, egy streambe csomagolva, amelynek tulajdonlása a dokumentumra passzol át. A visszahívási cél és annak háttérrendszere az Önén marad: a dokumentum a wrapperét zárásnál, törlésnél vagy újratöltésnél szabadítja fel, de a metódusmutató mögötti szállítási objektumot sosem érinti
A szerződés szándékosan megbocsátó az egyik irányban és szigorú a másikban. A rövid olvasás legális, és egyszerűen azt jelenti, hogy az elemző újra kér. A kivételt dobó visszahívás rövid olvasássá alakul, és a szokásos betöltési-hiba útvonalon konvergál. Az a visszahívás, amely azt állítja, hogy a Count-nál több bájtot írt, szorításra kerül, mert egy hibás szolgáltató nem tudhatja túl a gyorsítótár-puffert. A jelszó-újrapróbálások friss tartomány-streamet és friss elemzési állapotot építenek ugyanaz felett a visszahívási forrás felett, így egy elbukott kísérlet nem hagyhat maga után elavult pozíciót, ablakot vagy dekódolási állapotot
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
{ egy blokkoló GET, Range: bytes=Offset-(Offset+Count-1) fejléccel }
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; { felszabadítja a wrapper streamet }
Src.Free; { az Ön szállítása, az Ön élettartama }
end;
Mennyit tart valójában a tartomány-gyorsítótár?
Alapból 4 MiB-t, chunk-igazított ablakokra szórva, LRU szerint kisöpörve. A korábbi egyablakos terv arra a hosszra nőtt, amelyet a hívó kért, így egy nagy szekvenciális olvasás átléphette a névleges chunk méretet, miközben egy véletlen ugrás azonnal kidobta az előző ablakot. A mostani gyorsítótár minden forrás-offszetet a ChunkSize-ra igazít, minden hibánál pontosan egy chunkot húz le, és kemény bájtkeretet léptet érvénybe több ablakon át. Bármely explicit keret, amelyet átad, legalább egy teljes chunkra emelkedik, így egyetlen olvasás mindig chunkonként halad, és a csúcs-gyorsítótárterhelés kiszámítható marad. A 4096 alatti ChunkSize a 64 KiB alapértékre esik vissza
Az ismételt-olvasás elszámolás az a rész, amelyet érdemes a telemetriába kötni. A PDFlibPas az ismétlést az igazított chunk-kezdetről ismeri fel, és rendezett, összefüggő intervallumokat tart, ami elválasztja az igazi első letöltést a kisöpörés utáni újraletöltéstől, miközben a könyvelést megakadályozza abban, hogy a fájlmérettel lineárisan nőjön. A GetRangeSourceCacheInfo a teljes képet JSON-ként adja vissza, a SetRangeSourceCacheLimit futásidőben átméretezi a keretet, a ClearRangeSourceCache pedig együtt dobja az ablakokat és visszaállítja a statisztikákat. A keret futásidőbeni zsugorítása megtartja az előzményeket, és a keret-vezérelt elengedéseket kisöpörésként számolja, így a lapos hits mellett emelkedő repeatedReads az Ön jele arra, hogy a munkahalmaz már nem fér el
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;
Mi történik, amikor több szál akarja ugyanazt a chunkot?
Egy kérésre várnak, nem többre. A klasszikus TStream-nek egyetlen pozíciós kurzora van, és két szál, amelyek mindegyike helyesen zárol, még mindig azt tapasztalhatja, hogy a pozíciót átírják egy Seek és egy Read közt, ezért a lusta objektumok és a szegmentált olvasások a PDFlibPas-ban abszolút ReadAt-ot használnak, amely sosem mozdítja a kurzort. Minden igazított chunk egyetlen folyamatban lévő kérést kap, amelyet a chunk minden hívója megoszt, a szomszédos sorba állított chunkok összeolvadnak, mielőtt a forrásolvasás elindul, és egy fizikai olvasás 16 MiB-n van plafonolva, így a párhuzamos oldal-munka rohama sem duplikált kis kérésekké, sem egyetlen abszurd nagy kéréssé nem erősödik fel. Az összevonási ablak alapértéke 2 ms, és csak minden ReadAt első hiányzó chunkjára vonatkozik; a pozicionális Read sosem vár rá, és a nulla átadása teljesen eltávolítja a kezdeti gyűjtési késleltetést, ami a hosszú szekvenciális szkenneléseknél fontos, amelyek egyébként chunkonként halmoznák a várakozást. A pozíció, a gyorsítótár-metaadatok és a forrásolvasások három külön zárolás mögött ülnek, a forrás-visszahívás maga pedig szerializált, ami lehetővé teszi, hogy egy belső szálvédelem nélküli adatbázis- vagy objektumtároló-adapter változatlanul használható legyen. A várakozók megkapják az adat saját mását, így egy későbbi LRU kisöpörés nem érvényteleníthet már kiosztott puffert
Megkérdezheti-e, hogy a 900. oldal készen áll-e, letöltés nélkül?
Igen, és pontosan erre való az opcionális elérhetőségi visszahívás. Egy egyszerű olvasási visszahívás nem tudja megkülönböztetni a már megérkezett bájtokat azoktól, amelyek blokkoló oda-vissza utat igényelnek, és egy próbáló olvasással történő szondázás épp azt a letöltést indítaná el, amelyet el akar kerülni. A TPDFlibRangeAvailabilityEvent egyetlen kérdésre válaszol csak, arra, hogy egy teljes tartomány olvasható-e azonnal, és tilos bármit letöltenie; a gyorsítótár által már lefedett bájtok mindig elérhetőnek számítanak. A GetRangeSourceDataAvailability a közvetett objektumokat a kereszthivatkozási bejegyzésekben rögzített fizikai tárolótartományokra képezi le, a tömörített objektumokat az objektum-stream konténerükre oldja fel, az eltolódott PDF-fejlécet korrigálja, és egy objektumot csak azután elemz, hogy a teljes tartomány átment a letöltést nem végző szondán, így a hiányzó útvonal sosem hívja az Ön olvasási visszahívását
A bejárás hatókörözött, nem kimerítő. Az oldal-lekérdezés csak az oldalfa azt az ágát járja be, amely a céloldalt tartalmazza, majd hozzáadja az oldal tartalmát, erőforrásait, annotációit és örökölt oldalattribútumait, kihagyva a Parent és a P visszafelé irányuló éleit, hogy egyetlen oldal vagy widget ne tudjon visszafelé a teljes dokumentummá terjeszkedni. Az objektumgráf 100000 kért objektumra és 256 mélységre van kötve, a stream objektumok szótár-először alakban elemződnek, és a teljes elemzés tartaléka csak legfeljebb 4 MiB-os tárolt objektumokra engedélyezett. A JSON-jelentés az átfedő és szomszédos intervallumokat összevonja a számolás előtt, így a requiredBytes és a missingBytes az összevont requiredRanges és missingRanges tömbökből számolódik, amelyek end-je befoglaló végpont. Egy már elérhető objektum lekérdezése feltöltheti a tartomány-gyorsítótárat; egy hiányzó lekérdezése az olvasási statisztikákat érintetlenül hagyja
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
{ A Report hordozza a missingBytes-t és az összevont missingRanges-t }
ShowProgress(Report)
else if Status = PDF_RANGE_DATA_NOT_PRESENT then
ShowMissingFeature; { pl. a fájlnak egyáltalán nincs AcroForm-ja }
end;
Miért kell az előtolásnak iterálnia
Mert a mostani missingRanges egyszeri elolvasása nem teszi elérhetővé az oldalt. Egy hiányzó oldalfa-csomópont vagy objektum-stream csak megérkezése után árulja el a függőségek következő rétegét, így a PDFlibPas előtolási feladata lekérdezés, letöltés, újra lekérdezés ciklust fut, amíg az oldal, az űrlap vagy az objektumgráf teljesen elérhetővé nem válik, vagy egy bájt- vagy menetkeret meg nem állítja. A feladat saját olvasót és kis másodlagos gyorsítótárat használ, amelynek adatforrása az abszolút olvasásokat az eredeti tartomány-streamre továbbítja, ami az elemzési állapotot elkülöníti az előtérben futó TSmartPDFReader-től, miközben a valóban letöltött bájtok továbbra is a megosztott főgyorsítótárba érkeznek. Tartomány-streamenként egy munkaszál létezik, egyezve a szerializációval, amelyet a forrás-visszahívás már amúgy is megkövetel, a sor pedig négy prioritási szint szerint választ, majd szinten belül beküldési sorrend szerint. A MaxBytes fizikai chunk bájtokban kerül elszámolásra, így az elemző, amely egyetlen bájtot kér egy gyorsítótáron kívüli chunkon belül, továbbra is a teljes chunkot fizeti, míg a megosztott gyorsítótárban már ülő chunkok a feladatnak semmit sem kerülnek. A sorba állított feladat törlése nulla forrásolvasással jut véges állapotba; a futó feladat minden függőségi menet előtt és minden forráschunk előtt ellenőrzésre kerül, és a tartomány-stream felszabadítása megvárja, míg egy folyamatban lévő visszahívás visszatér, ahelyett hogy megszakítaná
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 és az utolsó teljes
elérhetőségi jelentés, így a LIMIT_REACHED megkülönböztethető
marad a FAILED-tól }
Lib.GetRangeSourcePrefetchInfo(Job, Info);
end;
Hol csúszik ez át teljes fájl letöltésbe
A tartománybetöltés a fájl-elrendezésre tett fogadás, és néhány fájl nem tartja be. Az ISO 32000-1 §7.5.8 szerinti linearizált fájl a jó eset: az elsőoldal-szakasz nyitáskor felmelegszik, a meglévő 4 MiB biztonsági küszöb és a mostani gyorsítótárkeret egyaránt határolja, hogy a felmelegedés ne tudja azonnal kisöpörni önmagának a legtöbbjét. Egy nem linearizált fájl továbbra is a trailer és a végénél lévő kereszthivatkozási lánc útján oldódik fel, ami néhány extra oda-vissza utat kerül, nem katasztrófa. A valódi szikla az a sérült fájl, amely a javítási útvonalat kényszeríti, mert a kereszthivatkozási tábla újjáépítése az objektumfejlécek átkutatását jelenti a teljes dokumentumon, és az egy teljes letöltés, chunkonként érkezve. A késleltetés a másik őszinte határ: kérésenként 60 ms-nál egy negyven gyorsítótáron kívüli chunkot igénylő véletlen-hozzáféréses elemzés két másodpercnél többet tölt úton, bármilyen jó is a gyorsítótár, ami pontosan az, amit az előreolvasási érv és a prioritási sor létezésének el kell tüntetnie. Ugyanez a fegyelem jelenik meg a nagy PDF-ek olvasztásának és darabolásának közvetlen hozzáférésű megközelítésében, és ez a gyorsítótár a párhuzamos oldalrenderelés és a nézegető lemez-oldalgyorsítótára alatt egyaránt ül
A tartományforrás-API, az elérhetőségi lekérdezés és az előtolási ütemező a standard PDFlibPas Delphi PDF Library része Delphi, C++Builder és Free Pascal számára; a termékoldal a LoadFromRangeSource teljes paraméterreferenciáját hordozza az előtolási prioritás- és állapotkonstansokkal együtt