Ένα σαρωμένο αρχείο 2 GB μένει σε bucket S3 και ο χρήστης θέλει τη σελίδα 900. Το PDFlibPas μπορεί να σερβίρει εκείνη τη σελίδα χωρίς να κατεβάσει το αρχείο: το LoadFromRangeSource χτίζει read-only seekable stream πάνω από δικό σας callback byte-range και το δίνει στο TPDFDocument, ώστε ο parser να τραβήξει τους cross-reference πίνακες, έναν κλάδο page tree, και ένα content stream
Η πλευρά μεταφοράς αυτού είναι παλιά και βαρετή. Οι διακομιστές HTTP διαφημίζουν byte ranges εδώ και δεκαετίες, πλέον καθορισμένες στο RFC 9110 §14, και κάθε object store μιλά την ίδια διάλεκτο. Η πλευρά PDF είναι εξίσου τακτοποιημένη: το ISO 32000-1 §7.5.8 ορίζει τη linearization ακριβώς ώστε ένας reader να μπορεί να αποδώσει την πρώτη σελίδα από το μπροστινό μέρος του αρχείου. Αυτό που έλειπε στη Delphi είναι το κομμάτι στη μέση, το μέρος που αποφασίζει ποια ranges να ζητήσετε, πόσα να κρατήσετε, και πώς να αποφύγετε να ρωτήσετε δύο φορές
Τι θέλει το LoadFromRangeSource από τη μεταφορά σας;
Δύο πράγματα, και κανένα δεν είναι stream. Το PDFlibPas ζητά ένα authoritative SourceSize και σύγχρονο read callback τύπου TPDFlibRangeReadEvent, δηλωμένο ως function(Sender: TObject; Offset: Int64; Buffer: Pointer; Count: LongInt): LongInt of object. Εσωτερικά το ζευγάρι γίνεται TCallbackByteRangeSource που εκθέτει SourceSize και ReadRange, τυλιγμένο σε stream της οποίας η ιδιοκτησία περνά στο έγγραφο. Ο στόχος callback σας και το backend του μένουν δικά σας: το έγγραφο ελευθερώνει το wrapper σε close, clear, ή reload, αλλά δεν αγγίζει ποτέ το αντικείμενο μεταφοράς πίσω από τον method pointer
Η σύμβαση είναι σκόπιμα επιεικής προς μία κατεύθυνση και αυστηρή προς την άλλη. Ένα short read είναι νόμιμο και απλώς σημαίνει ότι ο parser ξαναρωτά. Ένα callback που πετάει exception μετατρέπεται σε short read και συγκλίνει μέσα από τη συνήθη διαδρομή αποτυχίας φόρτωσης. Ένα callback που ισχυρίζεται ότι έγραψε περισσότερα από Count bytes σφίγγεται, γιατί ένα ελαττωματικό provider δεν πρέπει να μπορεί να ξεπεράσει το cache buffer. Οι επαναλήψεις κωδικού ξαναχτίζουν φρέσκο range stream και φρέσκια parse κατάσταση πάνω στην ίδια πηγή callback, οπότε μια αποτυχημένη προσπάθεια δεν μπορεί να αφήσει πίσω παλιά θέση, παράθυρο, ή κατάσταση αποκρυπτογράφησης
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
{ ένα blocking 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; { ελευθερώνει το wrapper stream }
Src.Free; { η μεταφορά σου, ο κύκλος ζωής σου }
end;
Πόσο κρατά πραγματικά το range cache;
Εξ ορισμού 4 MiB, απλωμένα σε windows ευθυγραμμισμένα σε chunk και εκδιωγμένα LRU. Ο προγενέστερος σχεδιασμός ενός window μεγάλωνε όσο ζητούσε ο καλών, οπότε ένα μεγάλο διαδοχικό read μπορούσε να ξεπεράσει το ονομαστικό μέγεθος chunk ενώ ένα τυχαίο άλμα πετούσε αμέσως το προηγούμενο window. Το τρέχον cache ευθυγραμμίζει κάθε source offset σε ChunkSize, τραβάει ακριβώς ένα chunk ανά miss, και επιβάλλει σκληρό προϋπολογισμό bytes πάνω από πολλά windows. Οποιοσδήποτε ρητός προϋπολογισμός περνάτε ανεβάζεται τουλάχιστον σε ένα πλήρες chunk, οπότε ένα απλό read προχωρά πάντα chunk το chunk και η κορύφωση φόρτωσης cache μένει προβλέψιμη. Ένα ChunkSize κάτω από 4096 γυρνά στο default των 64 KiB
Η λογιστική των repeat reads είναι το κομμάτι που αξίζει να δέσετε στην τηλεμετρία σας. Το PDFlibPas αναγνωρίζει repeat από ευθυγραμμισμένη αρχή chunk και κρατά διατεταγμένα συνεχόμενα διαστήματα, που χωρίζει ένα γνήσιο πρώτο τράβηγμα από refetch μετά από εκδίωξη ενώ κρατά τη λογιστική από το να μεγαλώνει γραμμικά με το μέγεθος αρχείου. Η GetRangeSourceCacheInfo επιστρέφει όλη την εικόνα ως JSON, η SetRangeSourceCacheLimit αλλάζει μέγεθος προϋπολογισμού σε runtime, και η ClearRangeSourceCache ρίχνει τα windows και μηδενίζει τα στατιστικά μαζί. Η συρρίκνωση του προϋπολογισμού σε runtime κρατά το ιστορικό και μετρά τις αποδεσμεύσεις από προϋπολογισμό ως εκδιώξεις, οπότε ανεβαίνοντας repeatedReads απέναντι σε επίπεδο hits είναι το σήμα σας ότι το working set δεν χωράει πια
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;
Τι γίνεται όταν πολλά threads θέλουν το ίδιο chunk;
Περιμένουν σε ένα αίτημα, όχι σε πολλά. Ένα κλασικό TStream έχει έναν δείκτη θέσης, και δύο threads που κλειδώνουν σωστά το καθένα μπορούν ακόμα να έχουν εκείνη τη θέση ξαναγραμμένη ανάμεσα σε Seek και Read, οπότε τα lazy objects και τα segmented reads στο PDFlibPas χρησιμοποιούν απόλυτο ReadAt που δεν μετακινεί ποτέ τον δείκτη. Κάθε ευθυγραμμισμένο chunk παίρνει ένα μόνο in-flight αίτημα που μοιράζεται κάθε καλών για εκείνο το chunk, γειτονικά chunks σε ουρά συγχωνεύονται πριν ξεκινήσει το read πηγής, και ένα φυσικό read περιορίζεται στα 16 MiB, οπότε μια έξαρση παράλληλης δουλειάς σελίδων δεν μεγεθύνεται ούτε σε διπλά μικρά αιτήματα ούτε σε ένα παράλογα μεγάλο. Το merge window είναι εξ ορισμού 2 ms και εφαρμόζεται μόνο στο πρώτο ελλιπές chunk κάθε ReadAt· το positional Read δεν περιμένει ποτέ γι αυτό, και περνώντας μηδέν αφαιρεί εντελώς την αρχική καθυστέρηση συλλογής, που μετράει για μακριές διαδοχικές σαρώσεις που διαφορετικά θα συσσώρευαν την αναμονή chunk το chunk. Θέση, metadata cache, και reads πηγής κάθονται πίσω από τρία ξεχωριστά lock, και το ίδιο το callback πηγής είναι serialized, που είναι αυτό που αφήνει έναν adapter βάσης δεδομένων ή object store χωρίς εσωτερική προστασία threads να χρησιμοποιείται αμετάβλητος. Οι περιμένοντες λαμβάνουν το δικό τους αντίγραφο των δεδομένων, οπότε μια μεταγενέστερη εκδίωξη LRU δεν μπορεί να ακυρώσει buffer που έχει ήδη παραδοθεί
Μπορείτε να ρωτήσετε αν η σελίδα 900 είναι έτοιμη χωρίς να την τραβήξετε;
Ναι, και ακριβώς γι αυτό υπάρχει το προαιρετικό callback διαθεσιμότητας. Ένα απλό read callback δεν ξεχωρίζει bytes που έχουν ήδη προσγειωθεί από bytes που θέλουν blocking κλήση στον διακομιστή, και μια δοκιμή με δοκιμαστικό read θα πυροδοτούσε ακριβώς το κατέβασμα που προσπαθείτε να αποφύγετε. Το TPDFlibRangeAvailabilityEvent απαντά ένα μόνο ερώτημα, αν ένα πλήρες range μπορεί να διαβαστεί αμέσως, και απαγορεύεται να τραβήξει οτιδήποτε· bytes που το καλύπτει ήδη το cache μετράνε πάντα ως διαθέσιμα. Η GetRangeSourceDataAvailability χαρτογραφεί indirect objects στις φυσικές σειρές αποθήκευσης που καταγράφονται στις εγγραφές cross-reference, επιλύει compressed objects στον container object stream τους, διορθώνει για μετατοπισμένη επικεφαλίδα PDF, και αναλύει ένα αντικείμενο μόνο αφού το πλήρες range περάσει τη δοκιμή χωρίς τράβηγμα, οπότε η διαδρομή του ελλιπούς δεν καλεί ποτέ το read callback σας
Η διαδρομή είναι οριοθετημένη και όχι εξαντλητική. Ένα ερώτημα σελίδας περπατά μόνο τον κλάδο του page tree που περιέχει τη σελίδα στόχο και μετά προσθέτει page content, resources, annotations, και κληρονομημένα attributes σελίδας, παραλείποντας τα back-edges Parent και P ώστε μία σελίδα ή widget να μην μπορεί να απλωθεί προς τα πίσω σε όλο το έγγραφο. Το γράφημα αντικειμένων οριοθετείται στα 100000 ζητούμενα αντικείμενα και σε βάθος 256, τα stream objects αναλύονται dictionary πρώτα, και fallback πλήρους ανάλυσης επιτρέπεται μόνο για stored objects έως 4 MiB. Η αναφορά JSON συγχωνεύει αλληλεπικαλυπτόμενα και γειτονικά διαστήματα πριν τη μέτρηση, οπότε requiredBytes και missingBytes υπολογίζονται από τους συγχωνευμένους πίνακες requiredRanges και missingRanges, των οποίων το end είναι συμπεριληπτικό άκρο. Ερώτημα για αντικείμενο ήδη διαθέσιμο μπορεί να γεμίσει το range cache· ερώτημα για ελλιπές αφήνει τα στατιστικά reads ανέγγιχτα
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
{ Η αναφορά κουβαλά "missingBytes" συν τα συγχωνευμένα "missingRanges" }
ShowProgress(Report)
else if Status = PDF_RANGE_DATA_NOT_PRESENT then
ShowMissingFeature; { π.χ. το αρχείο δεν έχει καθόλου AcroForm }
end;
Γιατί το prefetch πρέπει να επαναλαμβάνεται
Επειδή το διάβασμα των τρεχόντων missingRanges μία φορά δεν κάνει τη σελίδα διαθέσιμη. Ένας ελλιπής κόμβος page tree ή object stream αποκαλύπτει το επόμενο στρώμα εξαρτήσεων μόνο αφού φτάσει, οπότε μια δουλειά prefetch του PDFlibPas τρέχει βρόχο ερώτημα, τράβηγμα, ξανά ερώτημα μέχρι η σελίδα, η φόρμα, ή το γράφημα αντικειμένων να είναι πλήρως διαθέσιμα ή ένα όριο bytes ή περασμάτων να τη σταματήσει. Η δουλειά χρησιμοποιεί δικό της reader και μικρό δευτερεύον cache του οποίου η πηγή δεδομένων προωθεί απόλυτα reads στο αρχικό range stream, που κρατά την parse κατάσταση απομονωμένη από τον foreground TSmartPDFReader ενώ τα bytes που κατεβάζει πραγματικά προσγειώνονται ακόμα στο κοινό κύριο cache. Ένα worker thread υπάρχει ανά range stream, ταιριάζοντας τη serialization που απαιτεί ήδη το callback πηγής, και η ουρά διαλέγει με τέσσερα επίπεδα προτεραιότητας και μετά με σειρά υποβολής μέσα σε επίπεδο. Το MaxBytes χρεώνεται σε φυσικά bytes chunk, οπότε parser που ζητά ένα μόνο byte μέσα σε chunk που δεν είναι στο cache πληρώνει ακόμα ολόκληρο το chunk, ενώ chunks ήδη στο κοινό cache δεν κοστίζουν στη δουλειά τίποτα. Η ακύρωση δουλειάς σε ουρά φτάνει σε τελική κατάσταση με μηδενικά reads πηγής· μια τρέχουσα δουλειά ελέγχεται πριν από κάθε πέρασμα εξαρτήσεων και κάθε chunk πηγής, και η απελευθέρωση του range stream περιμένει in-flight callback να επιστρέψει αντί να προσπαθήσει να το διακόψει
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;
Πού αυτό υποβαθμίζεται σε κατέβασμα ολόκληρου αρχείου
Η φόρτωση ranges είναι στοίχημα στη διάταξη αρχείου, και κάποια αρχεία δεν την τιμούν. Ένα linearized αρχείο κατά ISO 32000-1 §7.5.8 είναι η καλή περίπτωση: η ενότητα πρώτης σελίδας ζεσταίνεται στο άνοιγμα, οριοθετημένη τόσο από το υπάρχον κατώφλι ασφαλείας 4 MiB όσο και από τον τρέχοντα προϋπολογισμό cache ώστε το ζέσταμα δεν μπορεί αμέσως να εκδιώξει το μεγαλύτερο μέρος του εαυτού του. Ένα μη linearized αρχείο εξακολουθεί να επιλύεται μέσω trailer και αλυσίδας cross-reference κοντά στο τέλος, που κοστίζει μερικά επιπλέον ταξίδια μετ' επιστροφής και όχι καταστροφή. Ο πραγματικός γκρεμός είναι ένα κατεστραμμένο αρχείο που επιβάλλει τη διαδρομή επισκευής, γιατί η ανακατασκευή cross-reference πίνακα σημαίνει σάρωση για επικεφαλίδες αντικειμένων σε όλο το έγγραφο, και αυτό είναι πλήρες κατέβασμα που φτάνει ένα chunk τη φορά. Η καθυστέρηση είναι το άλλο τίμιο όριο: στα 60 ms ανά αίτημα, ένα parse τυχαίας πρόσβασης που θέλει σαράντα chunks εκτός cache ξοδεύει πάνω από δύο δευτερόλεπτα σε μεταφορά ανεξαρτήτως πόσο καλό είναι το cache, που είναι ακριβώς αυτό που υπάρχει να κρύψει το επιχείρημα read-ahead και η ουρά προτεραιότητας. Η ίδια πειθαρχία εμφανίζεται στην προσέγγιση direct access για συγχώνευση και τεμαχισμό μεγάλων PDF, και αυτό το cache κάθεται από κάτω στην παράλληλη απόδοση σελίδων και στο disk page cache του viewer εξίσου
Το API range source, το ερώτημα διαθεσιμότητας, και ο προγραμματιστής prefetch είναι μέρος του standard PDFlibPas Delphi PDF Library για Delphi, C++Builder, και Free Pascal· η σελίδα προϊόντος κουβαλά την πλήρη αναφορά παραμέτρων για το LoadFromRangeSource μαζί με τις σταθερές προτεραιότητας και κατάστασης prefetch