Dua menit untuk menyalin tiga halaman dari PDF 40 halaman bukanlah masalah penyetelan performa. Ini adalah sinyal bahwa jalur API yang salah sedang digunakan. Saat saya pertama kali melihat waktu ini pada sampel salin-halaman HotPDF Component, naluri saya adalah memeriksa struktur dokumen terlebih dahulu, baru kodenya kemudian. Urutan itu ternyata penting
Apa yang sebenarnya lambat
PDF yang bersangkutan adalah dokumen referensi 40 halaman dengan pohon halaman yang tidak trivial: beberapa node /Pages perantara alih-alih satu array datar. Kode sampel awal memanggil LoadFromFile, kemudian membangun dokumen baru dengan BeginDoc, melakukan iterasi pada nomor halaman yang dipilih, dan pada setiap iterasi memuat kembali dokumen sumber dari disk untuk mengambil satu halaman. Itulah biaya parsing penuh dikalikan dengan jumlah halaman yang diinginkan. File 12 MB ditulis ke disk enam kali hanya untuk ekstraksi tiga halaman, karena tidak ada yang memeriksa apakah file tersebut perlu tetap terbuka selama iterasi
Penyebab kedua tidak terlihat dalam kode: LoadFromFile milik HotPDF menyelesaikan seluruh tabel cross-reference dan mendekompresi setiap aliran objek saat pemuatan. Ini adalah perilaku yang tepat untuk dokumen yang akan dimodifikasi, namun itu lebih banyak pekerjaan dari yang diperlukan jika Anda hanya menginginkan jumlah halaman dan sebagian kecil halaman. Untuk akses hanya-baca ke struktur, DAOpenFileReadOnly menghindari deserialisasi pohon objek penuh, yang penting pada file terkompresi dengan sumber daya gambar besar
Keduanya bukan bug library. Keduanya adalah pemanggil yang memilih API yang dirancang untuk satu tugas lalu menggunakannya untuk tugas yang berbeda
Menggunakan InsertPagesFromDocument untuk ekstraksi halaman
Jalur yang tepat untuk menyalin sekumpulan halaman dari satu dokumen HotPDF ke dokumen lain adalah InsertPagesFromDocument, yang dipanggil setelah LoadFromFile pada sumber. Anda memuat sumber sekali, memuat atau membuat tujuan sekali, memindahkan halaman, dan menyimpan. Sumber tetap di memori selama semua penyisipan halaman berlangsung:
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;
Parameter PageRange menerima format yang sama dengan sampel baris perintah: daftar yang dipisahkan koma berupa nomor halaman atau rentang seperti '1-3' atau '1,5,7-9'. Halaman dimulai dari 1. InsertPagesFromDocument menyalin aliran konten, kamus sumber daya, dan geometri halaman tanpa menyentuh metadata, bookmark, atau lampiran file tertanam kecuali jika direferensikan dari halaman yang disalin. Untuk ekstraksi tiga halaman dari dokumen 40 halaman, itu adalah kumpulan kerja yang kecil
Waktu pada file 12 MB yang sama yang sebelumnya berjalan dua menit: di bawah 1,5 detik dengan pola ini. Sebagian besar waktu itu dihabiskan untuk satu panggilan LoadFromFile. Struktur dokumen tidak relevan setelah tabel objek diselesaikan pertama kali
Saat LoadFromFile terlalu berat: Direct File API
Jika Anda hanya perlu menghitung halaman, memeriksa informasi dokumen, atau menyalin file tanpa menyentuh isinya, Direct File API menghindari parsing penuh sepenuhnya. DAOpenFileReadOnly memetakan tabel cross-reference tanpa mendekompresi aliran objek, sehingga jumlah halaman adalah O(ukuran xref) bukan O(ukuran file):
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;
Peringatannya: DAOpenFileReadOnly menerima parameter kata sandi namun kembali ke parsing penuh untuk input terenkripsi, karena dekripsi memerlukan pohon objek untuk menyelesaikan kamus enkripsi. Jika file sumber Anda terenkripsi, dekripsi terlebih dahulu dengan DecryptFile untuk mendapatkan salinan tidak terenkripsi, lalu buka dengan Direct File API. Fungsi DecryptFile tingkat file mengambil jalur penulisan ulang AES-256 langsung untuk enkripsi standar dan lebih cepat daripada LoadFromFile diikuti oleh SaveLoadedDocument untuk file besar, karena tidak membangun model objek dalam memori secara penuh
Memori selama pemrosesan batch besar
Pekerjaan batch yang memproses puluhan file dalam satu loop memiliki pola yang terlihat benar namun mengakumulasi memori: membuat THotPDF di dalam loop, memanggil LoadFromFile, melakukan pekerjaan, memanggil Free. Secara struktural itu benar. Masalahnya adalah ketika pekerjaan dalam mengalokasikan objek sementara, menangkap pengecualian, dan membiarkan objek sementara tersebut tetap hidup pada jalur kesalahan. Manajer memori Delphi tidak melakukan kompaksi, sehingga seratus kebocoran jalur-kesalahan selama satu proses batch bisa mendorong memori cukup tinggi untuk memperlambat alokasi untuk segalanya
Perbaikannya tidak rumit. Setiap THotPDF dan setiap TStream atau TBitmap perantara yang terlibat dalam pekerjaan PDF harus berada dalam blok try/finally di mana Free adalah pernyataan terakhir. Atur pointer lokal ke nil sebelum try sehingga cabang finally dapat menggunakan if Assigned(x) then x.Free dengan aman saat inisialisasi gagal di tengah jalan. Ini adalah disiplin kepemilikan Delphi standar dan itulah keseluruhan cerita untuk kelas masalah ini
Satu hal lagi yang perlu diperiksa dalam konteks batch: AddImage mendaftarkan gambar dalam daftar internal yang bertahan selama masa hidup instance THotPDF. Jika Anda menggunakan kembali satu instance di banyak dokumen dengan memanggil LoadFromFile berulang kali, pendaftaran gambar dari dokumen sebelumnya tetap ada dalam daftar. Baik buat instance baru per dokumen atau panggil jalur penghapusan daftar gambar di antara dokumen
Ukur sebelum mengubah apa pun
Sebelum mencoba salah satu pola ini, ukur terlebih dahulu. TStopwatch milik Delphi dari System.Diagnostics membungkus QueryPerformanceCounter dan cukup akurat untuk pembuatan profil waktu dinding I/O file. Bungkus LoadFromFile saja dan lihat berapa lama waktu yang dihabiskan. Jika 90% dari total waktu, perbaikannya adalah Direct File API atau mengurangi berapa kali Anda mem-parsing file yang sama. Jika di bawah 20%, hambatannya ada di tempat lain dan Anda mengejar hal yang salah
Ekstraksi dua menit yang memulai artikel ini ternyata sepenuhnya disebabkan oleh pola pemuatan berulang. Struktur dokumen tidak berkontribusi apa pun; pohon halaman datar pun akan berjalan dengan cara yang sama. Beralih ke satu LoadFromFile diikuti oleh satu panggilan InsertPagesFromDocument membawanya ke 1,3 detik pada perangkat keras yang sama tanpa menyentuh hal lain
API manipulasi halaman yang ditunjukkan di sini adalah bagian dari HotPDF Component untuk Delphi dan C++Builder