Δύο λεπτά για την αντιγραφή τριών σελίδων από ένα PDF 40 σελίδων δεν είναι πρόβλημα ρύθμισης της απόδοσης. Είναι ένα σήμα ότι χρησιμοποιείται η λάθος διαδρομή API. Όταν είδα για πρώτη φορά αυτόν τον χρόνο σε ένα δείγμα αντιγραφής σελίδων του HotPDF Component, το ένστικτό μου ήταν να εξετάσω πρώτα τη δομή του εγγράφου και μετά τον κώδικα. Αυτή η σειρά αποδείχθηκε ότι είχε σημασία
Τι ήταν πραγματικά αργό
Το εν λόγω PDF ήταν ένα έγγραφο αναφοράς 40 σελίδων με ένα μη τετριμμένο δέντρο σελίδων: πολλαπλοί ενδιάμεσοι κόμβοι /Pages αντί για έναν ενιαίο επίπεδο πίνακα. Ο αρχικός κώδικας του δείγματος καλούσε την LoadFromFile, έπειτα δημιουργούσε ένα νέο έγγραφο με την BeginDoc, έκανε βρόχο (loop) στους επιλεγμένους αριθμούς σελίδων και σε κάθε επανάληψη φόρτωνε ξανά το αρχείο προέλευσης από τον δίσκο για να τραβήξει μια σελίδα. Αυτό είναι το πλήρες κόστος ανάλυσης (parsing) πολλαπλασιασμένο με το πόσες σελίδες θέλετε. Ένα αρχείο 12 MB χτύπησε τον δίσκο έξι φορές για μια εξαγωγή τριών σελίδων, επειδή κανείς δεν εξέτασε εάν το αρχείο έπρεπε να παραμείνει ανοιχτό σε όλες τις επαναλήψεις
Ο δεύτερος συντελεστής ήταν αόρατος στον κώδικα: Η LoadFromFile του HotPDF επιλύει ολόκληρο τον πίνακα παραπομπών (cross-reference table) και αποσυμπιέζει κάθε ροή αντικειμένων (object stream) κατά τη φόρτωση. Αυτή είναι η σωστή συμπεριφορά για ένα έγγραφο που πρόκειται να τροποποιήσετε, αλλά είναι περισσότερη δουλειά από ό,τι χρειάζεστε εάν θέλετε μόνο το πλήθος σελίδων και ένα υποσύνολο σελίδων. Για πρόσβαση μόνο για ανάγνωση (read-only) στη δομή, η DAOpenFileReadOnly αποφεύγει την αποσειριοποίηση (deserializing) του πλήρους δέντρου αντικειμένων, κάτι που έχει σημασία σε συμπιεσμένα αρχεία με μεγάλους πόρους εικόνων
Κανένα από αυτά δεν είναι σφάλμα της βιβλιοθήκης. Και στις δύο περιπτώσεις, οι καλούντες επιλέγουν το API που έχει σχεδιαστεί για μια δουλειά και το χρησιμοποιούν για μια διαφορετική
Χρήση της InsertPagesFromDocument για εξαγωγή σελίδων
Η σωστή διαδρομή για την αντιγραφή ενός εύρους σελίδων από ένα έγγραφο HotPDF σε ένα άλλο είναι η InsertPagesFromDocument, καλούμενη μετά την LoadFromFile στην προέλευση. Φορτώνετε την προέλευση μία φορά, δημιουργείτε ή φορτώνετε τον προορισμό μία φορά, μετακινείτε τις σελίδες και αποθηκεύετε. Η προέλευση παραμένει στη μνήμη σε όλες τις εισαγωγές σελίδων:
procedure ExtractPages(const SourceFile, DestFile: string;
const PageRange: string);
var
Source, Dest: THotPDF;
begin
Source := THotPDF.Create(nil);
Dest := THotPDF.Create(nil);
try
// Load source once: full parse happens here and only here
Source.LoadFromFile(SourceFile);
// Build a minimal destination document
Dest.FileName := DestFile;
Dest.BeginDoc;
// Copy the requested range; '1-3' inserts pages 1 through 3
// starting at position 1 in the destination
Dest.InsertPagesFromDocument(Source, PageRange, 1);
Dest.EndDoc;
finally
Source.Free;
Dest.Free;
end;
end;
Η παράμετρος PageRange δέχεται την ίδια μορφή με το δείγμα γραμμής εντολών: μια λίστα αριθμών σελίδων ή ευρών διαχωρισμένη με κόμματα, όπως '1-3' ή '1,5,7-9'. Οι σελίδες βασίζονται στο 1 (1-based). Η InsertPagesFromDocument αντιγράφει ροές περιεχομένου (content streams), λεξικά πόρων και γεωμετρία σελίδας χωρίς να αγγίζει μεταδεδομένα, σελιδοδείκτες ή ενσωματωμένα συνημμένα αρχεία, εκτός εάν γίνεται αναφορά σε αυτά από τις σελίδες που αντιγράφηκαν. Για μια εξαγωγή τριών σελίδων από ένα έγγραφο 40 σελίδων, αυτό είναι ένα μικρό σύνολο εργασίας (working set)
Ο χρονισμός στο ίδιο αρχείο 12 MB που προηγουμένως εκτελούνταν για δύο λεπτά: κάτω από 1,5 δευτερόλεπτο με αυτό το μοτίβο. Ο περισσότερος από αυτόν τον χρόνο είναι η μοναδική κλήση της LoadFromFile. Η δομή του εγγράφου είναι άσχετη άπαξ και ο πίνακας αντικειμένων επιλυθεί την πρώτη φορά
Όταν η LoadFromFile είναι υπερβολική: το Direct File API
Εάν χρειάζεται μόνο να μετρήσετε σελίδες, να επιθεωρήσετε πληροφορίες εγγράφου ή να αντιγράψετε ένα αρχείο χωρίς να αγγίξετε το περιεχόμενό του, το Direct File API αποφεύγει εντελώς την πλήρη ανάλυση (full parse). Η DAOpenFileReadOnly χαρτογραφεί τον πίνακα παραπομπών (xref) χωρίς να αποσυμπιέζει τις ροές αντικειμένων, οπότε η καταμέτρηση των σελίδων είναι O(μέγεθος xref) αντί για O(μέγεθος αρχείου):
procedure InspectPDF(const FileName: string);
var
Pdf: THotPDF;
Handle, PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Handle := Pdf.DAOpenFileReadOnly(FileName, '');
if Handle <= 0 then
Exit;
try
PageCount := Pdf.DAGetPageCount(Handle);
Writeln('Pages: ', PageCount);
// DACopyFile is a byte-preserving copy, no re-serialization
Pdf.DACopyFile(FileName, 'archive-copy.pdf');
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
Η προειδοποίηση: Η DAOpenFileReadOnly δέχεται μια παράμετρο κωδικού πρόσβασης αλλά επιστρέφει σε μια πλήρη ανάλυση για κρυπτογραφημένες εισόδους, επειδή η αποκρυπτογράφηση απαιτεί το δέντρο αντικειμένων για την επίλυση του λεξικού κρυπτογράφησης. Εάν τα αρχεία προέλευσής σας είναι κρυπτογραφημένα, αποκρυπτογραφήστε τα πρώτα με την DecryptFile για να λάβετε ένα μη κρυπτογραφημένο αντίγραφο, και στη συνέχεια ανοίξτε αυτό με το Direct File API. Η συνάρτηση επιπέδου αρχείου DecryptFile ακολουθεί μια άμεση διαδρομή επανεγγραφής AES-256 για τυπική κρυπτογράφηση και είναι πιο γρήγορη από την LoadFromFile ακολουθούμενη από την SaveLoadedDocument για μεγάλα αρχεία, επειδή δεν χτίζει το πλήρες μοντέλο αντικειμένων στη μνήμη
Η Μνήμη κατά τη μαζική επεξεργασία μεγάλου όγκου
Οι μαζικές εργασίες (batch jobs) που επεξεργάζονται δεκάδες αρχεία σε έναν βρόχο (loop) έχουν ένα μοτίβο που φαίνεται σωστό αλλά συσσωρεύει μνήμη: δημιουργία του THotPDF μέσα στον βρόχο, κλήση της LoadFromFile, εκτέλεση εργασίας, κλήση της Free. Αυτό είναι δομικά σωστό. Το πρόβλημα εμφανίζεται όταν η εσωτερική εργασία κατανέμει (allocates) προσωρινά αντικείμενα (scratch objects), συλλαμβάνει εξαιρέσεις (catches exceptions) και αφήνει αυτά τα προσωρινά αντικείμενα ζωντανά στις διαδρομές σφαλμάτων. Ο διαχειριστής μνήμης του Delphi δεν κάνει συμπύκνωση (compacting), επομένως εκατό διαρροές από διαδρομές σφαλμάτων σε μια μαζική εκτέλεση μπορούν να ωθήσουν τη μνήμη αρκετά ψηλά ώστε να επιβραδύνουν την κατανομή για οτιδήποτε άλλο
Η διόρθωση δεν είναι εξωτική. Κάθε THotPDF και κάθε ενδιάμεσο TStream ή TBitmap που συμμετέχει στην εργασία PDF ανήκει σε ένα μπλοκ try/finally όπου η Free είναι η τελευταία δήλωση. Ορίστε τους τοπικούς δείκτες (local pointers) σε nil πριν από το try, ώστε ο κλάδος finally να μπορεί να χρησιμοποιήσει την if Assigned(x) then x.Free με ασφάλεια, όταν η αρχικοποίηση αποτυγχάνει στα μισά της διαδρομής. Αυτή είναι η τυπική πειθαρχία ιδιοκτησίας (ownership discipline) του Delphi και αποτελεί ολόκληρη την ιστορία για αυτήν την κατηγορία προβλημάτων
Ένα ακόμη πράγμα που πρέπει να ελέγξετε σε μαζικά πλαίσια: Η AddImage καταχωρεί εικόνες σε μια εσωτερική λίστα που παραμένει για όλη τη διάρκεια ζωής του στιγμιότυπου THotPDF. Εάν επαναχρησιμοποιήσετε ένα μόνο στιγμιότυπο σε πολλά έγγραφα καλώντας επανειλημμένα την LoadFromFile, οι καταχωρίσεις εικόνων από προηγούμενα έγγραφα παραμένουν στη λίστα. Είτε δημιουργήστε ένα νέο (fresh) στιγμιότυπο ανά έγγραφο, είτε καλέστε τη διαδρομή εκκαθάρισης λίστας εικόνων (image-list clear path) μεταξύ των εγγράφων
Μέτρηση πριν από την αλλαγή οτιδήποτε
Πριν προσεγγίσετε οποιοδήποτε από αυτά τα μοτίβα, μετρήστε. Το TStopwatch του Delphi από το System.Diagnostics περιβάλλει την QueryPerformanceCounter και είναι αρκετά ακριβές για τη σκιαγράφηση (profiling) χρόνου τοίχου (wall-clock) της I/O του αρχείου. Τυλίξτε μόνο την LoadFromFile και δείτε πόσο χρόνο αντιπροσωπεύει. Εάν είναι το 90% του συνολικού χρόνου, η λύση είναι το Direct File API ή η μείωση του πόσες φορές αναλύετε (parse) το ίδιο αρχείο. Εάν είναι κάτω από 20%, το σημείο συμφόρησης είναι κάπου αλλού και κυνηγάτε το λάθος πράγμα
Η εξαγωγή των δύο λεπτών που ξεκίνησε αυτήν την ανάρτηση αποδείχθηκε ότι ήταν εξ ολοκλήρου το μοτίβο επαναλαμβανόμενης φόρτωσης. Η δομή του εγγράφου δεν συνέβαλε καθόλου· ένα επίπεδο δέντρο σελίδων θα είχε εκτελεστεί με τον ίδιο τρόπο. Η μετάβαση σε μία μόνο LoadFromFile ακολουθούμενη από μία κλήση InsertPagesFromDocument το μείωσε στο 1,3 δευτερόλεπτο στο ίδιο υλικό (hardware) χωρίς να αγγίξει τίποτα άλλο
Το API χειρισμού σελίδων που παρουσιάζεται εδώ είναι μέρος του HotPDF Component για Delphi και C++Builder