2 GB skenuotas archyvas gyvena S3 kibire, o vartotojas nori 900 puslapio. PDFlibPas gali patiekti tą puslapį nesisiunčiant failo: LoadFromRangeSource sukuria tik skaitymui skirtą ieškomą srautą ant jūsų pačių baitų intervalo atgalinio kvietimo ir perduoda jį TPDFDocument, todėl analizatorius atsisiunčia kryžminių nuorodų lenteles, vieną puslapių medžio šaką ir vieną turinio srautą
Transporto pusė čia sena ir nuobodi. HTTP serveriai reklamuoja baitų intervalus jau dešimtmečius, dabar apibrėžtus RFC 9110 §14, ir kiekviena objektų saugykla kalba tą patį dialektą. PDF pusė taip pat išsdirbusi: ISO 32000-1 §7.5.8 apibrėžia lineariavimą būtent tam, kad skaitytuvas galėtų atvaizduoti pirmą puslapį iš failo priekio. Delphi aplinkoje buvo trūkęs vidurinis gabalas — dalis, nusprendžianti, kurių intervalų prašyti, kiek laikyti, ir kaip išvengti prašymo du kartus
Ką LoadFromRangeSource reikalauja iš jūsų transporto?
Du dalykai, ir nė vienas iš jų nėra srautas. PDFlibPas prašo autoritetingo SourceSize ir sinchroninio skaitymo atgalinio kvietimo tipo TPDFlibRangeReadEvent, deklaruoto kaip function(Sender: TObject; Offset: Int64; Buffer: Pointer; Count: LongInt): LongInt of object. Viduje pora tampa TCallbackByteRangeSource, atveriančiu SourceSize ir ReadRange, įvyniotu į srautą, kurio nuosavybė perduodama dokumentui. Jūsų atgalinio kvietimo tikslas ir jo vidinė dalis lieka jūsų: dokumentas atlaisvina įvyniojimą uždarant, išvalant arba persikraunant, bet niekada neliečia transporto objekto už metodo rodyklės
Sutartis sąmoningai atlaisi viena kryptimi ir griežta kita. Trumpas skaitymas yra teisėtas ir tiesiog reiškia, kad analizatorius prašo vėl. Išimtį keliantis atgalinis kvietimas paverčiamas trumpu skaitymu ir sueina per įprastą įkėlimo nesėkmės kelią. Atgalinis kvietimas, teigiantis parašęs daugiau nei Count baitų, užspaudžiamas, nes sugedęs tiekėjas negali perplūsti talpyklos buferio. Slaptažodžio pakartojimai per tą patį atgalinio kvietimo šaltinį atkuria šviežią intervalų srautą ir šviežią analizės būseną, todėl nesėkmingas bandymas negali palikti pasenusios pozicijos, lango arba iššifravimo būsenos
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
{ vienas blokuojantis GET su 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; { atlaisvina įvyniojimo srautą }
Src.Free; { jūsų transportas, jūsų gyvavimo laikas }
end;
Kiek iš tikrųjų laiko intervalų talpykla?
Pagal numatytuosius 4 MiB, paskirstyti per gabalų išlygintus langus ir išmetamus LRU. Ankstesnis vieno lango dizainas augo iki kokio ilgio prašė kvietėjas, todėl vienas didelis nuoseklus skaitymas galėjo peršokti nominalų gabalo dydį, kol atsitiktinis šuolis iškart išmesdavo ankstesnį langą. Dabartinė talpykla išlygina kiekvieną šaltinio poslinkį pagal ChunkSize, paima lygiai vieną gabalą per praleidimą, ir taiko standų baitų biudžetą keliuose languose. Bet kokia jūsų perduota atviroji kvota pakeliama bent iki vieno pilno gabalo, todėl vienas skaitymas visada žengia gabalas po gabalo, o pikinė talpyklos apkrova lieka nuspėjama. ChunkSize žemiau 4096 grįžta į 64 KiB numatytąjį
Pakartotinio skaitymo apskaita yra dalis, verta prijungti prie jūsų telemetrijos. PDFlibPas atpažįsta pakartojimą pagal išlygintą gabalo pradžią ir laiko tvarkingus ištisinius intervalus, kas atskiria tikrą pirmąjį paėmimą nuo pakartotinio po išmetimo, neleisdama apskaitai augti tiesiai su failo dydžiu. GetRangeSourceCacheInfo grąžina visą vaizdą kaip JSON, SetRangeSourceCacheLimit keičia kvotą vykdymo metu, o ClearRangeSourceCache numeta langus ir kartu atnulina statistiką. Kvotos mažinimas vykdymo metu išlaiko istoriją ir kvotomis varomus atlaisvinimus skaičiuoja kaip išmetimus, todėl kylantis repeatedReads prieš plokščią hits yra jūsų signalas, kad darbinis rinkinys daugiau netelpa
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;
Kas nutinka, kai kelios gijos nori to paties gabalo?
Jie laukia vienos užklausos, o ne kelių. Klasikinis TStream turi vieną pozicijos žymeklį, ir dvi gijos, kiekviena teisingai užrakinusi, vis tiek gali turėti tą poziciją perrašytą tarp Seek ir Read, todėl tingūs objektai ir segmentiniai skaitymai PDFlibPas naudoja absoliutų ReadAt, niekada nejudingą žymeklio. Kiekvienas išlygintas gabalas gauna vieną vykstantį užklausą, kurią dalijasi visi to gabalo kvietėjai, gretimi eilės gabalai sujungiami prieš prasidedant šaltinio skaitymui, ir vienas fizinis skaitymas ribojamas 16 MiB, todėl lygiagretaus puslapių darbo sprogimas neįamplifikuojasi nei į dubliuotas mažas užklausas, nei į vieną absurdiškai didelę. Sujungimo langas numatytas 2 ms ir taikomas tik pirmajam kiekvieno ReadAt dingusiam gabalui; pozicinis Read niekada nelaukia jo, o nulio perdavimas visai pašalina pradinį rinkimo uždelsimą, kas svarbu ilgiems nuosekliems sklavimams, kitu atveju kaupiantiems laukimą gabalas po gabalo. Pozicija, talpyklos metaduomenys ir šaltinio skaitymai sėdi už trijų atskirų užraktų, o pats šaltinio atgalinis kvietimas serializuojamas — dėl to duomenų bazės arba objektų saugyklos adapteris be vidinės gijų apsaugos gali būti naudojamas nepakeistas. Laukiantieji gauna savo pačių duomenų kopiją, todėl vėlesnis LRU išmetimas negali padaryti negaliojančiu jau išduoto buferio
Ar galima klausti, ar 900 puslapis paruoštas, jo neparsiunčiant?
Taip, ir būtent tam skirtas atviro pasirinkimo prieinamumo atgalinis kvietimas. Paprastas skaitymo atgalinis kvietimas negali atskirti jau atkeliavusių baitų nuo baitų, reikalaujančių blokuojančios kelionės pirmyn ir atgal, o zondavimas bandomuoju skaitymu sukeltų tą patį parsiuntimą, kurio stengiatės išvengti. TPDFlibRangeAvailabilityEvent atsako tik į vieną klausimą — ar pilnas intervalas gali būti perskaitytas nedelsiant — ir jam draudžiama nieko parsisiųsti; baitai, kuriuos talpykla jau dengia, visada skaitomi prieinamais. GetRangeSourceDataAvailability susieja netiesioginius objektus su fiziniais saugojimo intervalais, užrašytais kryžminių nuorodų įrašuose, išsprendžia suspaustus objektus į jų objektų srauto talpyklę, pataiso paslinktą PDF antraštę, ir analizuoja objektą tik po to, kai pilnas intervalas praeina neisiuntusį zondą, todėl dingimo kelias niekada nekvečia jūsų skaitymo atgalinio kvietimo
Apėjimas yra apibrėžtos apimties, o ne išsamus. Puslapio užklausa eina tik per puslapių medžio šaką, laikančią tikslinį puslapį, o tada prideda puslapio turinį, išteklius, anotacijas ir paveldėtus puslapio atributus, praleisdama Parent ir P atgalines briaunas, kad vienas puslapis arba valdiklis negalėtų išsiplėsti atgal į visą dokumentą. Objektų grafą riboja 100000 prašomų objektų ir 256 gylis, srautų objektai analizuojami pirmiausia pagal žodyną, o pilnos analizės atsarginis kelias leidžiamas tik saugomiems objektams iki 4 MiB. JSON ataskaita prieš skaičiuodama sujungia persidengiančius ir gretimus intervalus, todėl requiredBytes ir missingBytes skaičiuojami iš sujungtų requiredRanges ir missingRanges masyvų, kurių end yra įtraukiantis galinis taškas. Jau prieinamo objekto užklausimas gali užpildyti intervalų talpyklą; dingusio objekto užklausimas palieka skaitymo statistiką nepaliestą
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
{ Reporte — "missingBytes" ir sujungti "missingRanges" }
ShowProgress(Report)
else if Status = PDF_RANGE_DATA_NOT_PRESENT then
ShowMissingFeature; { pvz. failas visai neturi AcroForm }
end;
Kodėl išankstinis parsiuntimas turi kartotis
Nes dabartinių missingRanges perskaitymas kartą nepaverčia puslapio prieinamu. Dingęs puslapių medžio mazgas arba objektų srautas atskleidžia kitą priklausomybių sluoksnį tik atkeliavęs, todėl PDFlibPas išankstinio parsiuntimo darbas vykdo užklausos, paėmimo, pakartotinės užklausos ciklą, kol puslapis, forma arba objektų grafai visiškai prieinami arba baitų arba praėjimų riba sustabdo. Darbas naudoja savo skaitytuvą ir mažą antrinę talpyklą, kurios duomenų šaltinis persiunčia absoliučius skaitymus į pirminį intervalų srautą, kas laiko analizės būseną atskirtą nuo pirmojo plano TSmartPDFReader, kol jo tikrai parsisiunčiami baitai vis tiek nukrenta į bendrą pagrindinę talpyklą. Viena darbinė gija egzistuoja intervalų srautui, atitinkantį serializaciją, kurios jau reikalauja šaltinio atgalinis kvietimas, o eilė renka pagal keturis prioritetų lygius ir tada pagal pateikimo tvarką lygio viduje. MaxBytes skaičiuojamas fiziniais gabalų baitais, todėl analizatorius, prašantis vieno baito neištalpytame gabale, vis tiek moka už visą gabalą, o gabalai, jau esantys bendroje talpykloje, darbui nieko nekainuoja. Eilėje esančio darbo atšaukimas pasiekia galutinę būseną su nuliu šaltinio skaitymų; vykstantis darbas tikrinamas prieš kiekvieną priklausomybių praėjimą ir kiekvieną šaltinio gabalą, o intervalų srauto atlaisvinimas laukia, kol vykstantis atgalinis kvietimas grįš, vietoj to, kad bandytų jį nutraukti
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" ir paskutinė
pilna prieinamumo ataskaita, kad LIMIT_REACHED liktų atskiriamas
nuo FAILED }
Lib.GetRangeSourcePrefetchInfo(Job, Info);
end;
Kur tai degraduoja į viso failo parsiuntimą
Intervalų įkėlimas yra statymas ant failo išdėstymo, ir kai kurie failai jo nesilaiko. Lineariuotas failas pagal ISO 32000-1 §7.5.8 yra geras atvejis: pirmojo puslapio sekcija pašildoma atidarant, ribojama ir esamos 4 MiB saugos ribos, ir dabartinės talpyklos kvotos, todėl pašildymas negali iškart išmesti daugumos savęs paties. Nelineariuotas failas vis tiek išsprendžiamas per priekabą ir kryžminių nuorodų grandinę prie galo, kas kainuoja porą papildomų kelionių pirmyn atgal, o ne katastrofą. Tikrasis skardis yra pažeistas failas, privertęs remonto kelią, nes kryžminių nuorodų lentelės atstatymas reiškia objektų antraščių paiešką per visą dokumentą, o tai yra pilnas parsiuntimas, atkeliaujantis po vieną gabalą. Vėlavimas yra kita sąžininga riba: ties 60 ms užklausai, atsitiktinės prieigos analizė, reikalaujanti keturiasdešimties neištalpytų gabalų, praleidžia virš dviejų sekundžių tranzite, nepaisant kaip gera talpykla — būtent tam, kad tai paslėptų, egzistuoja išankstinio skaitymo argumentas ir prioritetų eilė. Ta pati drausmė matosi tiesioginės prieigos prieipyje, jungiant ir skaidant didelius PDF, ir ši talpykla sėdi po lygiagretiu puslapių atvaizdavimu ir žiūryklės disko puslapių talpykla vienodai
Intervalų šaltinio API, prieinamumo užklausa ir išankstinio parsiuntimo planuotojas yra standartinės PDFlibPas Delphi PDF Library dalis Delphi, C++Builder ir Free Pascal aplinkoms; produkto puslapis neša visą LoadFromRangeSource parametrų nuorodą kartu su išankstinio parsiuntimo prioriteto ir būsenų konstantomis