Το HotPDF φορτώνει ένα PDF από οποιαδήποτε πηγή τυχαίας πρόσβασης υλοποιήσετε, και η THPDFCoalescingRandomAccessSource περιτυλίγει αυτή την πηγή ώστε οι διάσπαρτες μικρές αναγνώσεις του parser να γίνονται ένα οριοθετημένο σύνολο cached block ranges με ασύγχρονο prefetch. Σε ένα έγγραφο που εξυπηρετείται μέσω HTTP range requests, αυτή είναι η διαφορά ανάμεσα σε μερικές εκατοντάδες round trips και μερικές δεκάδες
Τίποτα στον parser δεν αλλάζει. Εξακολουθείτε να καλείτε την LoadFromRandomAccessSource, το ίδιο αντικείμενο εγγράφου επιστρέφει, και το ίδιο API σελίδας λειτουργεί. Αυτό που αλλάζει είναι η κίνηση από κάτω
Γιατί το ίδιο PDF φορτώνει ακαριαία τοπικά αλλά σέρνεται στο δίκτυο;
Επειδή ένας parser PDF δεν διαβάζει ένα αρχείο, το πλοηγεί. Πηγαίνει στο τέλος για το startxref, πηδά πίσω στον πίνακα cross-reference, επιλύει το trailer dictionary, ακολουθεί μια αναφορά προς το Catalog, μετά προς τη ρίζα του δέντρου σελίδων, μετά προς έναν κόμβο σελίδας, μετά προς το resource dictionary του. Κάθε ένα από αυτά τα βήματα διαβάζει δεκάδες bytes από διαφορετικό offset
Σε ένα τοπικό αρχείο αυτό το μοτίβο είναι σχεδόν δωρεάν: το λειτουργικό σύστημα έχει ήδη cache την γύρω σελίδα των 4 KiB, οπότε η δεύτερη ανάγνωση κοστίζει ένα memcpy. Πάνω από μια μεταφορά δικτύου δεν υπάρχει τέτοια τοπικότητα. Κάθε ανάγνωση είναι ένα αίτημα με το δικό του latency, και 300 διαδοχικά αιτήματα στα 40 ms το καθένα είναι δώδεκα δευτερόλεπτα που ξοδεύονται σχεδόν εξ ολοκλήρου σε αναμονή. Η λύση δεν είναι να διαβάζετε λιγότερο· ο parser χρειάζεται ακριβώς αυτό που ζητά. Η λύση είναι να κάνετε κάθε φυσική ανάγνωση να καλύπτει περισσότερο από ό,τι θα ζητήσει η επόμενη λογική ανάγνωση
Τι αλλάζει το coalescing
Η πηγή coalescing στρογγυλοποιεί κάθε ανάγνωση προς τα πάνω σε ένα block και κάνει cache το block. Το BlockSize έχει προεπιλογή 262.144 bytes και το MaxCacheBytes 2.097.152, οπότε οκτώ blocks είναι resident από προεπιλογή και εκδιώκονται με σειρά least-recently-used έναντι ενός αυστηρού budget bytes. Η ανάγνωση 40 bytes ενός κλειδιού trailer από τον parser τραβά μαζί τα 256 KiB γύρω της, και η επόμενη δωδεκάδα αναγνώσεων σε αυτή τη γειτονιά, όπου βρίσκονται τα δεδομένα cross-reference και catalog, εξυπηρετούνται από τη μνήμη
Η δική σας πηγή παραμένει απλή. Υλοποιήστε τις GetSize και ReadAt, παρακάμψτε την ReadAtCancellable αν η μεταφορά σας μπορεί να ακυρώσει εν κινήσει, και αφήστε το wrapper να χειριστεί το caching, το coalescing και το prefetch
type
THttpRangeSource = class(THPDFRandomAccessSource)
private
FClient: TMyHttpClient;
FUrl: string;
FSize: Int64;
public
function GetSize: Int64; override;
function ReadAt(Offset: Int64; var Buffer; Count: Longint): Longint; override;
function ReadAtCancellable(Offset: Int64; var Buffer; Count: Longint;
CancellationToken: THPDFCancellationToken): Longint; override;
end;
var
Raw: THttpRangeSource;
Cached: THPDFCoalescingRandomAccessSource;
Pdf: THotPDF;
begin
Raw := THttpRangeSource.Create('https://files.example.com/contract.pdf');
// OwnsSource=True: το wrapper απελευθερώνει το Raw μαζί με τον εαυτό του
Cached := THPDFCoalescingRandomAccessSource.Create(Raw, True, 262144, 8388608);
Pdf := THotPDF.Create(nil);
try
Cached.AsyncPrefetchEnabled := True;
Cached.AdaptiveReadAheadEnabled := True;
Cached.MaxReadAheadBlocks := 8;
if Pdf.LoadFromRandomAccessSource(Cached, True) = 1 then
RenderFirstPage(Pdf);
finally
Pdf.Free;
end;
end;
Πόσο μπροστά πρέπει να διαβάζει;
Το προσαρμοστικό read-ahead απαντά σε αυτό το ερώτημα ανά έγγραφο αντί να σας αναγκάζει να μαντεύετε. Με το AdaptiveReadAheadEnabled ενεργό, το παράθυρο μεγαλώνει μέσα από 1, 2, 4 και 8 blocks καθώς συσσωρεύονται συνεχείς αναγνώσεις προς τα εμπρός, και ποτέ δεν υπερβαίνει το MaxReadAheadBlocks ή τη ρυθμισμένη χωρητικότητα cache. Τη στιγμή που φτάνει μια ανάγνωση που δεν βρίσκεται περίπου εκεί όπου τελείωσε η προηγούμενη, το παράθυρο καταρρέει και το prefetch καταστέλλεται
Το SequentialReadToleranceBytes, με προεπιλογή 4.096, ορίζει το "περίπου". Αναγνώσεις που προσγειώνονται εντός αυτής της απόστασης από το τέλος της προηγούμενης ανάγνωσης εξακολουθούν να μετρούν ως διαδοχικές, κάτι που έχει σημασία επειδή ένας parser PDF που διατρέχει ένα content stream δεν παράγει τέλεια συνεχόμενα offsets· παραλείπει ένα πεδίο μήκους εδώ, ένα ενσωματωμένο dictionary εκεί. Ορίστε την ανοχή πολύ χαμηλά και μια κανονική σάρωση προς τα εμπρός ταξινομείται ως τυχαία, οπότε το read-ahead δεν ενεργοποιείται ποτέ. Ορίστε την πολύ ψηλά και η γνήσια τυχαία πρόσβαση φαίνεται διαδοχική, οπότε φέρνετε megabytes που κανείς δεν θέλει. Η προεπιλογή είναι βαθμονομημένη για διάτρεξη content stream, και τα στατιστικά θα σας πουν αν η μεταφορά σας διαφωνεί
Αυτή η ασυμμετρία είναι σκόπιμη: η αύξηση είναι σταδιακή, η κατάρρευση άμεση. Το over-fetching σε φόρτο εργασίας τυχαίας πρόσβασης κοστίζει πραγματικό εύρος ζώνης και πραγματικά χρήματα σε μετρούμενες μεταφορές, οπότε το φθηνό λάθος προτιμάται από το ακριβό
Ακύρωση που πράγματι σταματά τη μεταφορά
Η βασική κλάση δηλώνει την ReadAtCancellable, και η πηγή coalescing την τιμά πλήρως. Όταν φτάνει μια ανάγνωση προσκηνίου για ένα εύρος που δεν εξυπηρετεί ένα prefetch εν εξελίξει, το prefetch ακυρώνεται αντί να αφεθεί να ολοκληρωθεί, ώστε το αίτημα σελίδας του χρήστη να μην ουρανιάζεται πίσω από κερδοσκοπική κίνηση. Η προεπιλεγμένη υλοποίηση στην THPDFRandomAccessSource επιστρέφει σε απλή ReadAt, που σημαίνει ότι το χαρακτηριστικό είναι opt-in ανά μεταφορά: πελάτες HTTP που υποστηρίζουν ακύρωση αιτήματος αποκτούν γνήσια ακύρωση, και απλούστερες πηγές συνεχίζουν να λειτουργούν αμετάβλητες
Συνδυάστε το με ένα cancellation token περασμένο μέσα από το UI σας και ένας χρήστης που κλείνει ένα έγγραφο πράγματι σταματά την κίνηση δικτύου αντί να περιμένει να αδειάσει. Το ίδιο μοντέλο token υποστηρίζει και την ουρά που περιγράφεται στο background rendering με ουρά αιτημάτων, οπότε ένα token μπορεί να καλύψει ολόκληρη τη διαδρομή από το viewport έως το socket
Ανάγνωση των στατιστικών του range cache
Η GetStatistics γεμίζει μια εγγραφή THPDFRangeCacheStatistics που διαχωρίζει τι έκανε η μεταφορά σας από τι έκανε το cache. Τα SourceReadCount και SourceBytesRead είναι φυσική κίνηση. Τα CacheHitCount και CacheMissCount είναι λογική κίνηση. Τα SequentialReadCount και RandomReadCount δείχνουν πώς ταξινομήθηκε το μοτίβο πρόσβασης, τα CurrentReadAheadBlocks και PeakReadAheadBlocks δείχνουν πόσο άνοιξε το παράθυρο, και τα PrefetchRequestCount, PrefetchCompletedCount, PrefetchCancelledCount και SuppressedPrefetchCount δείχνουν αν η κερδοσκοπία απέδωσε
var
S: THPDFRangeCacheStatistics;
begin
Cached.GetStatistics(S);
Log(Format('physical %d reads / %d bytes, hits %d, misses %d',
[S.SourceReadCount, S.SourceBytesRead, S.CacheHitCount, S.CacheMissCount]));
Log(Format('pattern: %d sequential, %d random, peak window %d blocks',
[S.SequentialReadCount, S.RandomReadCount, S.PeakReadAheadBlocks]));
Log(Format('prefetch: %d issued, %d completed, %d cancelled, %d suppressed',
[S.PrefetchRequestCount, S.PrefetchCompletedCount,
S.PrefetchCancelledCount, S.SuppressedPrefetchCount]));
end;
Τρεις ενδείξεις σας λένε τι να αλλάξετε. Πολλά ακυρωμένα prefetch με υψηλό αριθμό τυχαίων αναγνώσεων σημαίνει ότι το έγγραφο προσπελαύνεται εκτός σειράς, οπότε χαμηλώστε το MaxReadAheadBlocks και σταματήστε να πληρώνετε για εύρος ζώνης που πετάτε. Πολλά misses με peak παράθυρο ακόμη στο 1 σημαίνει ότι η ανοχή απορρίπτει ένα μοτίβο που είναι ουσιαστικά διαδοχικό, οπότε ανεβάστε το SequentialReadToleranceBytes. Και bytes που διαβάστηκαν κατά πολύ πέρα από το μέγεθος του αρχείου σημαίνει ότι το cache παλεύει, οπότε ανεβάστε το MaxCacheBytes πριν αγγίξετε οτιδήποτε άλλο
Τα linearized αρχεία αλλάζουν την αριθμητική
Αν ελέγχετε τον παραγωγό, το linearizing του εγγράφου αλλάζει το πρόβλημα αντί να το βελτιστοποιεί. Ένα linearized PDF τοποθετεί τα αντικείμενα της πρώτης σελίδας και έναν πίνακα υποδείξεων στην αρχή του αρχείου, ώστε ένας viewer να μπορεί να αποδώσει την πρώτη σελίδα από το ανοιχτό megabyte χωρίς να δει τα υπόλοιπα. Το HotPDF εκθέτει αυτή τη διαδρομή απευθείας μέσω των GetProgressiveLinearizedLoadInfo και ReadProgressiveLinearizedFirstPageSection, και η πλευρά εγγραφής καλύπτεται στο δημιουργία linearized PDF με πίνακες υποδείξεων
Και οι δύο τεχνικές συνδυάζονται. Το coalescing κάνει κάθε έγγραφο ανεκτό πάνω από αργή σύνδεση· το linearization κάνει την πρώτη σελίδα να φτάνει γρήγορα σε έγγραφα που παράγετε εσείς οι ίδιοι. Για αρχεία που ζουν σε τοπικό δίσκο αλλά είναι πολύ μεγάλα για να χωρέσουν στη μνήμη, οι διαδρομές mapped-file και lazy-stream που περιγράφονται στο API απευθείας αρχείου είναι συνήθως το καλύτερο εργαλείο, αφού δεν υπάρχει καθόλου round-trip latency προς απόσβεση εξαρχής
Το HotPDF είναι ένα εγγενές VCL PDF component για Delphi και C++Builder, χωρίς εξωτερικό DLL για τον parser και με πλήρη διαθέσιμο πηγαίο κώδικα. Το API πηγής τυχαίας πρόσβασης, το wrapper coalescing και τα σημεία εισόδου προοδευτικής φόρτωσης τεκμηριώνονται στη σελίδα του HotPDF Delphi PDF component