Η απόδοση (rendering) μιας σελίδας στο PDFium είναι σύγχρονη. Καλείτε τη βιβλιοθήκη, ραστεροποιεί (rasterises) σε ένα bitmap που της δώσατε, και ο έλεγχος επιστρέφει όταν γραφτούν τα pixels. Για μια μεμονωμένη σελίδα μεγέθους οθόνης σε ένα επίπεδο ζουμ αυτό διαρκεί λίγα χιλιοστά του δευτερολέπτου και κανείς δεν το παρατηρεί. Για μια εξαγωγή 300 dpi ενός εγγράφου 200 σελίδων, ή για μια λωρίδα μικρογραφιών (thumbnail strip) που πρέπει να ραστεροποιήσει κάθε σελίδα ταυτόχρονα, η ίδια κλήση κοστίζει δευτερόλεπτα. Αν κάνετε αυτήν την κλήση από το κύριο νήμα (main thread), ο βρόχος μηνυμάτων (message loop) σταματά, το παράθυρο σταματά να επανασχεδιάζεται (repainting), και τα Windows ζωγραφίζουν το τρομακτικό "Δεν ανταποκρίνεται" ("Not Responding") πάνω στη γραμμή τίτλου σας. Η εργασία είναι σωστή. Το μέρος που την εκτελέσατε είναι λάθος
Η λύση είναι να μετακινήσετε τη χρονοβόρα απόδοση σε ένα νήμα παρασκηνίου (background thread) και να φέρετε το αποτέλεσμα πίσω στο κύριο νήμα, όπου το bitmap μπορεί να παραδοθεί σε ένα στοιχείο ελέγχου (control). Το ίδιο το PDFium δεν σας εμποδίζει να το κάνετε αυτό, αλλά η σύνδεση (binding) πρέπει να κάνει την παράδοση ασφαλή, επειδή η επιφάνεια σφαλμάτων (bug surface) γύρω από το "εκτέλεση σε έναν εργάτη (worker), απάντηση στο UI" είναι ευρεία και οι αποτυχίες είναι διακοπτόμενες (intermittent). Η μονάδα (unit) FPdfAsync στο PDFiumPas υπάρχει για να δώσει σε αυτό το μοτίβο μία σωστή υλοποίηση, με ένα μοντέλο ακύρωσης (cancellation model) που ταιριάζει στον τρόπο που πραγματικά συμπεριφέρεται μια χρονοβόρα απόδοση
Το σχήμα της εργασίας
Τρεις λειτουργίες κυριαρχούν στις περιπτώσεις όπου μια απόδοση διαρκεί περισσότερο από ένα καρέ (frame). Η μαζική απόδοση (batch rendering) διατρέχει ένα εύρος σελίδων και ραστεροποιεί κάθε σελίδα, συνήθως στο δίσκο. Η εξαγωγή πολλαπλών σελίδων (multi-page export) κάνει το ίδιο αλλά συναρμολογεί (assembles) την έξοδο σε ένα αρχείο. Η απόδοση σελίδας στο παρασκήνιο (background page rendering) είναι αυτό που κάνει ένα πρόγραμμα προβολής (viewer) όταν ο χρήστης μεταβαίνει σε μια σελίδα που δεν βρίσκεται ακόμα στην κρυφή μνήμη (cache), οπότε το bitmap παράγεται εκτός νήματος (off-thread) και εμφανίζεται όταν είναι έτοιμο. Και τα τρία μοιράζονται τους ίδιους περιορισμούς. Διαρκούν αρκετά ώστε το νήμα UI να μην μπορεί να τα φιλοξενήσει, παράγουν ένα αποτέλεσμα που το νήμα UI τελικά χρειάζεται, και ο χρήστης μπορεί να τα εγκαταλείψει. Το κλείσιμο του εγγράφου, η κύλιση (scrolling) πέρα από τη σελίδα ή το πάτημα της Ακύρωσης θα πρέπει να σταματά την εργασία αντί να αναγκάζει τον χρήστη να περιμένει για μια έξοδο που δεν θέλει πια
Αυτός ο τελευταίος περιορισμός είναι αυτός που διαμορφώνει τον σχεδιασμό. Μια απόδοση που δεν μπορεί να ακυρωθεί είναι μια απόδοση που κρατά το έγγραφο ανοιχτό και καίει (burns) CPU αφού η απάντηση έχει πάψει να έχει σημασία. Επομένως, η μονάδα (unit) είναι χτισμένη γύρω από δύο πρωταρχικά στοιχεία (primitives) που συνδυάζονται: ένα future που μεταφέρει το αποτέλεσμα πίσω, και ένα κουπόνι (token) που μεταφέρει το αίτημα ακύρωσης προς τα εμπρός
Ένα future τύπου πυροδότησε-και-ξέχασε (fire-and-forget)
Το TPdfFuture<T>.Run παίρνει έναν εργάτη (worker), μια απάντηση (reply) και ένα προαιρετικό κουπόνι ακύρωσης (cancellation token). Εκκινεί τον εργάτη σε ένα νήμα παρασκηνίου, και όταν ο εργάτης τελειώσει παραδίδει την απάντηση στο κύριο νήμα. Η γενική παράμετρος (generic parameter) T είναι οτιδήποτε παράγει η απόδοση, συχνά μια λαβή (handle) bitmap ή μια εγγραφή (record) κατάστασης. Ο εργάτης εκτελείται εκτός νήματος· η απάντηση εκτελείται εκεί που είναι ασφαλές να αγγίξετε τη VCL
class procedure TPdfFuture<T>.Run(
const AWorker: TPdfFutureWorker<T>;
const AReply: TPdfFutureReply<T>;
const AToken: IPdfCancellationToken = nil); static;
Η σκόπιμη παράλειψη (deliberate omission) είναι οποιοδήποτε είδος Wait. Δεν υπάρχει μέθοδος για τον αποκλεισμό (block) του καλούντος μέχρι να ολοκληρωθεί το future, και αυτό δεν είναι παράβλεψη. Ένα Wait που καλείται από το κύριο νήμα είναι ο κλασικός τρόπος για να προκληθεί αδιέξοδο (deadlock) σε ένα UI: ο εργάτης χρειάζεται το κύριο νήμα για να εκτελέσει την απάντησή του μέσω του Synchronize, το κύριο νήμα είναι σταθμευμένο (parked) μέσα στο Wait, και καμία πλευρά δεν μπορεί να προχωρήσει. Αρνούμενο να προσφέρει το πρωταρχικό στοιχείο (primitive), το future αποκλείει (rules out) το μοτίβο που πιο συχνά νικάει τους ανθρώπους που προσπαθούν να γράψουν αυτό το πράγμα μόνοι τους. Κώδικας που πραγματικά χρειάζεται να αποκλειστεί θα πρέπει να χρησιμοποιήσει ένα απλό TThread και να αναλάβει (own) τις συνέπειες. Το future προορίζεται για την περίπτωση fire-and-forget (πυροδότησε-και-ξέχασε), που είναι και αυτό που πραγματικά αποτελεί η απόδοση στο παρασκήνιο
Το αποτέλεσμα τυλίγεται (wrapped) στο TPdfFutureResult<T>, μια εγγραφή που λέει στην απάντηση ποιο από τα τρία πράγματα συνέβη. Το IsSuccess σημαίνει ότι ο εργάτης επέστρεψε κανονικά και το Value περιέχει την απόδοση. Το IsCancelled σημαίνει ότι το κουπόνι (token) ενεργοποιήθηκε (fired) και ο εργάτης αποχώρησε (bailed out) σε ένα σημείο ακύρωσης. Το IsFailure σημαίνει ότι ο εργάτης προκάλεσε εξαίρεση (raised), και το ErrorMessage φέρει το κείμενο. Η απάντηση επιθεωρεί την κατάσταση μία φορά και διακλαδίζεται (branches), αντί να μαντεύει από μια τιμή-φρουρό (sentinel value) εάν ένα επιστρεφόμενο bitmap είναι πραγματικό
Το race condition της έκδοσης v1.61.0 που άλλαξε την παράδοση της απάντησης
Το πιο διδακτικό μέρος αυτής της μονάδας είναι μια αλλαγή μίας γραμμής που πήρε λίγο χρόνο να γίνει κατανοητή. Στις πρώιμες εκδόσεις το νήμα του εργάτη παρέδιδε την απάντησή του με το TThread.Queue. Η ουρά (Queue) δημοσιεύει (posts) την απάντηση στην ουρά του κύριου νήματος και επιστρέφει αμέσως, το οποίο διαβάζεται ακριβώς ως αυτό που θέλει ένα future τύπου fire-and-forget. Ήταν λάθος, και αξίζει να εξηγήσουμε τον λόγο (spelling out) επειδή είναι το είδος του σφάλματος που περνάει κάθε δοκιμή (test) που μπορείτε να σκεφτείτε να γράψετε
Το νήμα του εργάτη δημιουργείται με FreeOnTerminate := True. Αυτό σημαίνει ότι τη στιγμή που η Execute επιστρέφει, το νήμα αποδομεί (tears down) τον εαυτό του, και το TThread.Destroy καλεί το RemoveQueuedEvents(Self) ως μέρος του καθαρισμού (cleanup). Η μέθοδος RemoveQueuedEvents εκκαθαρίζει (purges) οποιαδήποτε μέθοδο στην ουρά που έχει ως στόχο το νήμα που πεθαίνει. Οπότε η ακολουθία ήταν: ο εργάτης τελειώνει, βάζει στην ουρά (queues) την απάντηση σε σχέση με τον εαυτό του, η Execute επιστρέφει, το νήμα καταστρέφει τον εαυτό του, και το RemoveQueuedEvents διαγράφει την απάντηση που το κύριο νήμα δεν είχε τρέξει ακόμα. Το αποτέλεσμα απλώς εξαφανιζόταν. Χειρότερα, στο στενό παράθυρο όπου το κύριο νήμα τράβηξε την απάντηση στην ουρά και άρχισε να την εκτελεί την ίδια στιγμή που το νήμα απελευθερωνόταν (freed), η απάντηση άγγιξε πεδία ενός μισο-κατεστραμμένου (half-destroyed) αντικειμένου, το οποίο είναι μια περίπτωση χρήσης-μετά-την-απελευθέρωση (use-after-free)
Η διόρθωση στην έκδοση v1.61.0 ήταν να παραδοθεί η απάντηση με το Synchronize αντί για το Queue. Το Synchronize αποκλείει (blocks) το νήμα του εργάτη μέχρι το κύριο νήμα να εκτελέσει την απάντηση μέχρι την ολοκλήρωσή της. Ο εργάτης είναι ακόμα ζωντανός (alive) ενώ η απάντησή του εκτελείται, επομένως δεν υπάρχει τίποτα για να απελευθερωθεί (free) κάτω από αυτόν (out from under it), και το νήμα δεν επιστρέφει από την Execute (και επομένως δεν αρχίζει να καταστρέφει τον εαυτό του) μέχρι να παραδοθεί η απάντηση. Η παράδοση είναι εγγυημένη (guaranteed), και το παράθυρο χρήσης-μετά-την-απελευθέρωση είναι κλειστό
procedure TPdfFutureThread<T>.Execute;
begin
FResult.Status := pfsSuccess;
FResult.ErrorMessage := '';
try
FToken.ThrowIfCancelled; // already cancelled? skip the worker
FResult.Value := FWorker(FToken);
except
on E: EPdfOperationCancelled do
begin
FResult.Status := pfsCancelled;
FResult.ErrorMessage := E.Message;
end;
on E: Exception do
begin
FResult.Status := pfsFailure;
FResult.ErrorMessage := E.Message;
end;
end;
if Assigned(FReply) then
// Synchronize, not Queue: this thread is FreeOnTerminate, so a queued reply
// could be dropped by RemoveQueuedEvents before the main thread ran it.
Synchronize(DispatchReply);
end;
Το γενικό μάθημα διαρκεί περισσότερο από τη συγκεκριμένη διόρθωση. Οι ασύγχρονες ανακλήσεις (callbacks) fire-and-forget είναι το πιο εύκολο μοτίβο ταυτοχρονισμού (concurrency pattern) για να το κάνετε ανεπαίσθητα (subtly) λάθος, επειδή η ευτυχής διαδρομή (happy path) λειτουργεί με την πρώτη προσπάθεια και το σφάλμα (bug) ζει στην αλληλεπίδραση (interaction) μεταξύ της σειράς αποδόμησης (teardown order) του νήματος και της ουράς. Δεν αναπαράγεται κατ' απαίτηση (on demand). Εξαρτάται από το αν το κύριο νήμα έτυχε (happened) να στραγγίξει (drain) την ουρά πριν ο εργάτης τύχει να τελειώσει την καταστροφή του εαυτού του, κάτι που είναι ένας χρονισμός (timing) που ο χρονοπρογραμματιστής (scheduler) αποφασίζει διαφορετικά σε κάθε εκτέλεση. Ένα πρωταρχικό στοιχείο (primitive) που είναι σωστό μία φορά, μέσα στη σύνδεση (binding), αξίζει πολύ περισσότερο από τον ίδιο κώδικα που εξάγεται εκ νέου (re-derived) σε κάθε εφαρμογή που χρειάζεται μια απόδοση στο παρασκήνιο
Γιατί οι ανακλήσεις (callbacks) είναι δείκτες μεθόδων (method pointers)
Ο εργάτης και η απάντηση δεν είναι ανώνυμες μέθοδοι. Είναι τύποι procedure of object, TPdfFutureWorker<T> και TPdfFutureReply<T>, και αυτή η επιλογή επιβάλλεται από τη μήτρα (matrix) μεταγλωττιστών (compiler). Το PDFiumPas μεταγλωττίζεται σε Delphi XE5 και μεταγενέστερες εκδόσεις καθώς και σε Free Pascal 3.2 σε λειτουργία (mode) Delphi, και η FPC 3.2 σε αυτή τη λειτουργία δεν υποστηρίζει ανώνυμες μεθόδους. Μια ανάκληση (callback) αναφοράς-προς-διαδικασία (reference-to-procedure) που συλλαμβάνει (captures) τοπικές μεταβλητές θα μεταγλωττιζόταν στη Delphi και θα αποτύγχανε στην FPC, επομένως η μονάδα (unit) χρησιμοποιεί τον ελάχιστο κοινό παρονομαστή που αποδέχονται και οι δύο μεταγλωττιστές
Η πρακτική συνέπεια (practical consequence) είναι το πού ζει η κατάσταση (state). Μια ανώνυμη μέθοδος κλείνει πάνω από (closes over) τοπικές μεταβλητές· ένας δείκτης μεθόδου όχι. Έτσι, οποιαδήποτε κατάσταση χρειάζεται ο εργάτης, ο δείκτης (index) σελίδας, το ζουμ (zoom), η διαδρομή εξόδου (output path), και οποιαδήποτε κατάσταση χρειάζεται να ενημερώσει (update) η απάντηση, το στοιχείο ελέγχου της εικόνας προορισμού ή η ετικέτα προόδου, πρέπει να κρέμεται (hang off) από το αντικείμενο του οποίου η μέθοδος περνιέται (passed). Σε ένα πρόγραμμα προβολής (viewer) αυτό το αντικείμενο είναι συνήθως η φόρμα ή ένας ελεγκτής απόδοσης (render controller) που της ανήκει (owns). Αυτό δεν είναι μια προσωρινή λύση (workaround) που επιβάλλεται απρόθυμα (grudgingly)· διατηρεί την ιδιοκτησία αυτής της κατάστασης ρητή και ορατή στο αντικείμενο λήψης (receiving object) αντί να είναι κρυμμένη μέσα σε ένα κλείσιμο (closure)
Συνεργατική ακύρωση, όχι σκληρός τερματισμός
Η ακύρωση εδώ είναι συνεργατική (cooperative). Δεν υπάρχει API που να φτάνει μέσα στο νήμα του εργάτη και να το τερματίζει, επειδή ο τερματισμός (terminating) ενός νήματος στα μέσα της απόδοσης (mid-render) αφήνει το PDFium να κρατά κλειδώματα (locks) και μερικώς (partially) γραμμένα bitmaps, και η κατάσταση της διεργασίας μετά από έναν αναγκαστικό (forced) τερματισμό δεν είναι κάτι που μπορείτε να αιτιολογήσετε (reason about). Αντ' αυτού, παραδίδεται στον εργάτη ένα κουπόνι (token) μόνο για ανάγνωση (read-only) και αναμένεται να το ελέγξει, και ο βρόχος απόδοσης (render loop) είναι γραμμένος ώστε να το ελέγχει μεταξύ σελίδων ή μεταξύ πλακιδίων (tiles), όπου η διακοπή (stopping) είναι καθαρή (clean)
Το κουπόνι προσφέρει τρεις τρόπους παρατήρησης της ακύρωσης. Το IsCancelled είναι μια φθηνή ψηφοφορία (poll) boolean για έναν βρόχο που θέλει να δοκιμάσει και να αποφασίσει μόνος του. Το ThrowIfCancelled είναι η κοινή περίπτωση: καλέστε το σε ένα φυσικό σημείο ακύρωσης και, εάν έχει ζητηθεί ακύρωση, προκαλεί (raises) μια εξαίρεση EPdfOperationCancelled, η οποία ξετυλίγει (unwinds) τον εργάτη κατευθείαν πίσω στο future. Το RegisterCallback επισυνάπτει (attaches) μια ειδοποίηση μίας βολής (one-shot notification) που πυροδοτείται (fires) μία φορά όταν η πηγή ακυρώνεται, χρήσιμο όταν ένας εργάτης είναι αποκλεισμένος (blocked) σε κάτι που μπορεί να διακόψει αντί να κάθεται σε έναν σφιχτό (tight) βρόχο
Η εξαίρεση είναι το σημείο όπου έχει σημασία το όριο του νήματος (thread boundary). Όταν ο εργάτης προκαλεί (raises) ένα EPdfOperationCancelled, το future το πιάνει (catches) και το μετατρέπει σε κατάσταση ακύρωσης (cancelled status), οπότε η απάντηση βλέπει IsCancelled και όχι αποτυχία (failure). Το ίδιο το αντικείμενο της εξαίρεσης δεν περνάει (marshaled) ποτέ στο κύριο νήμα. Ζει και πεθαίνει στο νήμα του εργάτη· μόνο η συμβολοσειρά του μηνύματός του αντιγράφεται (copied) στο ErrorMessage. Το πέρασμα (marshaling) ενός ζωντανού (live) αντικειμένου εξαίρεσης σε διαφορετικά νήματα θα σήμαινε πρόσβαση σε μνήμη που ανήκει σε ένα νήμα που τελειώνει, το οποίο είναι η ίδια κατηγορία (class) σφάλματος που η διόρθωση Synchronize υπάρχει για να αποτρέψει. Ένας κωδικός κατάστασης (status code) και μια συμβολοσειρά περνούν το όριο καθαρά· ένα αντικείμενο δεν θα το έκανε
Δύο διεπαφές (interfaces), ώστε ένας εργάτης να μην μπορεί να ακυρώσει τον εαυτό του
Η ακύρωση χωρίζεται (split) σε δύο διεπαφές σκόπιμα. Το IPdfCancellationTokenSource είναι η πλευρά εγγραφής (write side): έχει το Cancel, και ο ιδιοκτήτης που το δημιουργεί, συνήθως η φόρμα, το κρατά και καλεί το Cancel όταν ο χρήστης κάνει κλικ στο κουμπί ή η φόρμα κλείνει. Το IPdfCancellationToken είναι η πλευρά ανάγνωσης (read side): έχει τα IsCancelled, ThrowIfCancelled, και RegisterCallback, και αυτό είναι όλο (all) που λαμβάνει (receives) ποτέ ο εργάτης. Ένα συγκεκριμένο (concrete) αντικείμενο υλοποιεί και τα δύο, αλλά στον εργάτη παραδίδεται πάντα μόνο το κουπόνι (token), επομένως δεν έχει κανέναν τρόπο να ακυρώσει τη λειτουργία που εκτελεί. Ο διαχωρισμός είναι ένα προστατευτικό κιγκλίδωμα σε επίπεδο API (API-level guard rail). Ένας εργάτης που θα μπορούσε να φτάσει το Cancel μέσω του κουπονιού του θα προσκαλούσε ένα μπερδεμένο (confused) κομμάτι κώδικα να ακυρώσει τον εαυτό του, και το σύστημα τύπων (type system) αφαιρεί την πιθανότητα (possibility)
Υπάρχει μια αντίστοιχη (matching) λεπτομέρεια για την περίπτωση όπου ένας καλών θέλει μια απόδοση αλλά δεν σκοπεύει ποτέ να την ακυρώσει. Αντί να επιβάλλει (force) μια φρέσκια πηγή (fresh source) ανά κλήση, η μονάδα (unit) εκθέτει το PdfNoCancellationToken, ένα κουπόνι singleton (μοναδικό στιγμιότυπο) που βρίσκεται μόνιμα στην κατάσταση (state) μη ακυρωμένο (not-cancelled). Το Run το αντικαθιστά (substitutes) όταν το όρισμα του κουπονιού παραμένει μηδενικό (nil). Αυτό το singleton κατασκευάζεται πρόθυμα (eagerly) κατά την αρχικοποίηση (initialization) της μονάδας αντί νωχελικά (lazily) στην πρώτη χρήση, και ο λόγος είναι και πάλι ο ταυτοχρονισμός (concurrency). Εάν πολλές κλήσεις Run σε διαφορετικά νήματα εργατών προσέγγιζαν ταυτόχρονα ένα lazily δημιουργημένο singleton, θα μπορούσαν να προκαλέσουν race (ανταγωνισμό) στην κατασκευή του, να διαρρεύσουν ένα διπλότυπο (duplicate), ή να παρατηρήσουν (observe) για λίγο ένα μισο-αρχικοποιημένο (half-initialised) στιγμιότυπο (instance). Η κατασκευή του πριν μπορέσει να τρέξει οποιοσδήποτε εργάτης αφαιρεί εντελώς το race
Εκτέλεση μιας ακυρώσιμης απόδοσης
Στην πράξη (in practice) δημιουργείτε μια πηγή (source), την κρατάτε στη φόρμα, περνάτε το κουπόνι (Token) της στο Run μαζί με μια μέθοδο εργάτη και μια μέθοδο απάντησης (reply method), και συνδέετε (wire) το κουμπί Ακύρωσης στην πηγή. Ο εργάτης ελέγχει το κουπόνι ενώ αποδίδει· η απάντηση ενημερώνει το UI μόλις επιστρέψει το αποτέλεσμα. Επειδή οι ανακλήσεις (callbacks) είναι δείκτες μεθόδων (method pointers), ο εργάτης και η απάντηση διαβάζουν ό,τι χρειάζονται από τα πεδία της φόρμας
procedure TMainForm.StartRender;
begin
FCancelSource := TPdfCancellationTokenSource.New; // field, lives on the form
TPdfFuture<Boolean>.Run(RenderWorker, RenderReply, FCancelSource.Token);
end;
procedure TMainForm.CancelButtonClick(Sender: TObject);
begin
if Assigned(FCancelSource) then
FCancelSource.Cancel; // worker observes this at its next cancel point
end;
// Runs on a background thread. Reads FPageRange / FOutputDir from the form.
function TMainForm.RenderWorker(const AToken: IPdfCancellationToken): Boolean;
var
PageIndex: Integer;
begin
for PageIndex := FFirstPage to FLastPage do
begin
AToken.ThrowIfCancelled; // clean stop between pages
RenderOnePage(PageIndex); // synchronous PDFium rasterisation
end;
Result := True;
end;
// Runs on the main thread. Safe to touch the VCL here.
procedure TMainForm.RenderReply(const AResult: TPdfFutureResult<Boolean>);
begin
if AResult.IsSuccess then
StatusLabel.Caption := 'Render complete'
else if AResult.IsCancelled then
StatusLabel.Caption := 'Cancelled'
else
StatusLabel.Caption := 'Failed: ' + AResult.ErrorMessage;
end;
Η απάντηση (reply) χειρίζεται και τα τρία αποτελέσματα (outcomes) επειδή και τα τρία είναι προσιτά (reachable). Μια ολοκληρωμένη απόδοση αναφέρει επιτυχία, ένας χρήστης που πάτησε Ακύρωση βλέπει τον ακυρωμένο κλάδο (cancelled branch), και ένα αρχείο που δεν μπόρεσε να γραφτεί ή μια σελίδα που απέτυχε να αναλυθεί (parse) φτάνει ως αποτυχία με ένα μήνυμα. Κανένας από αυτούς τους κλάδους δεν προκαλεί αποκλεισμό (block), κανένας δεν αγγίζει το νήμα του εργάτη, και το bitmap ή η κατάσταση που παρήγαγε ο εργάτης διαβάζεται μόνο αφού το future το έχει παραδώσει στο νήμα στο οποίο ανήκει το UI
Η ίδια πειθαρχία (discipline) νημάτων αποδίδει (pays off) και αλλού σε ένα πρόγραμμα προβολής. Ο τρόπος με τον οποίο τα αποδοσμένα (rendered) bitmaps διατηρούνται και επαναχρησιμοποιούνται (reused) σε αλλαγές ζουμ καλύπτεται στη σημείωσή μας σχετικά με την κρυφή μνήμη απόδοσης (render cache) και την απόδοση του ζουμ, και το ευρύτερο (broader) ζήτημα (question) της διατήρησης του ορίου (boundary) του PDFium ασφαλούς στο περιβάλλον της Delphi βρίσκεται στην ενίσχυση (hardening) του PDFium VCL ABI για ασφάλεια μνήμης. Η ασύγχρονη υποδομή που περιγράφεται εδώ αποστέλλεται (ships) ως μέρος του στοιχείου PDFium για Delphi και C++Builder, μαζί με τα API απόδοσης (rendering), κειμένου και φορμών που καλύπτονται αλλού σε αυτό το ιστολόγιο