40 sayfalık bir PDF'den üç sayfayı kopyalamanın iki dakika sürmesi bir performans ayarlama sorunu değildir; yanlış API yolunun kullanıldığının işaretidir. Bu zamanlamayı ilk kez, HotPDF Delphi Component üzerine kurulu sayfa kopyalama kodunda gördüğümde içgüdüm, önce belge yapısına, kodu ikinci sıraya koymaktı. O sıra, tersi çıktı
Aslında ne yavaştı
Söz konusu PDF, basit (non-trivial) olmayan bir sayfa ağacına sahip 40 sayfalık bir referans belgesiydi: tek bir düz (flat) dizi (array) yerine çok sayıda ara (intermediate) /Pages düğümü (node) vardı. Orijinal örnek kod LoadFromFile'ı çağırıyor (calling), ardından BeginDoc ile yeni bir belge oluşturuyor, seçilen sayfa numaraları üzerinde döngü yapıyor (looping) ve her yinelemede (iteration) bir sayfayı çekmek için kaynak belgeyi diskten tekrar yüklüyordu. Bu tam ayrıştırma (parse) maliyetinin (cost) istediğiniz sayfa sayısıyla çarpılmasıdır. Dosyanın yinelemeler (iterations) boyunca açık kalması gerekip gerekmediğine kimse bakmadığı için, 12 MB'lık bir dosya üç sayfalık bir çıkarma (extraction) işlemi için diske altı kez vuruyordu (hit)
İkinci katkıda bulunan (contributor) kodda görünmüyordu: HotPDF'in LoadFromFile'ı tüm çapraz referans (cross-reference) tablosunu çözer (resolves) ve yükleme (load) sırasında her nesne akışını (object stream) sıkıştırılmış halinden (decompresses) çıkarır. Değiştirmek üzere olduğunuz bir belge için bu doğru davranıştır, ancak yalnızca sayfa sayısına ve sayfaların bir alt kümesine ihtiyacınız varsa ihtiyacınız olandan daha fazla iştir (work). Yapıya (structure) salt okunur erişim (read-only access) için DAOpenFileReadOnly tüm nesne ağacını seri durumdan çıkarmaktan (deserializing) kaçınır; bu da büyük resim (image) kaynaklarına sahip sıkıştırılmış (compressed) dosyalarda önemlidir
İkisi de, bir iş için tasarlanmış API'yi seçip onu başka bir iş için kullanmaktan ibarettir
Sayfa çıkarma için ExtractPagesToFile kullanımı
Açılmış bir HotPDF belgesinden belli bir sayfa aralığını yeni bir dosyaya kopyalamanın doğru yolu, kaynaktaki LoadFromFile'dan sonra çağrılan ExtractPagesToFile'dur. Kaynağı bir kez yüklersiniz; çağrı hedef belgeyi kurar, seçilen sayfaları içine kopyalar ve diske yazar — hepsini, zaten bellekte duran kopyadan:
procedure ExtractPages(const SourceFile, DestFile: string;
const PageRange: string);
var
Source: THotPDF;
begin
Source := THotPDF.Create(nil);
try
// Kaynağı bir kez yükle: tam ayrıştırma burada ve yalnızca burada olur
if Source.LoadFromFile(SourceFile) <= 0 then
raise Exception.Create('Source PDF has no pages');
// İstenen aralığı ('1-3', '1,5,7-9') doğrudan yeni bir dosyaya yaz
Source.ExtractPagesToFile(DestFile, PageRange);
finally
Source.Free;
end;
end;
PageRange parametresi, komut satırı örneğiyle aynı biçimi kabul eder: '1-3' ya da '1,5,7-9' gibi virgülle ayrılmış sayfa numaraları ve aralıkları listesi. Sayfalar 1 tabanlıdır. ExtractPagesToFile seçilen her sayfayı, başvurduğu içerik akışları ve kaynaklarla birlikte kopyalar; dolayısıyla 40 sayfalık bir belgeden üç sayfa çıkarmak için çalışma seti küçük kalır
Daha önce iki dakika süren aynı 12 MB'lık dosyada zamanlama: bu desenle yaklaşık 1,3 saniye. Sürenin çoğu tek LoadFromFile çağrısıdır. Nesne tablosu ilk kez çözüldükten sonra belge yapısı önemsizdir; düz bir sayfa ağacı ile çok katmanlı bir ağaç aynı biçimde çalışır
LoadFromFile çok fazla geldiğinde: Direct File API
Yalnızca sayfaları saymanız, belge bilgilerini incelemeniz veya içeriklerine dokunmadan bir dosyayı kopyalamanız gerekiyorsa, Direct File API tam ayrıştırmadan tamamen kaçınır. DAOpenFileReadOnly çapraz referans (cross-reference) tablosunu, nesne akışlarını (object streams) sıkıştırılmış halinden çıkarmadan (decompressing) haritalar (maps); bu nedenle sayfa sayısı (page count) dosya boyutuna O(file size) değil, xref boyutuna O(xref size) bağlıdır:
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 baytları koruyan bir kopyadır, yeniden serileştirme yoktur
Pdf.DACopyFile(FileName, 'archive-copy.pdf');
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
Uyarı (caveat): DAOpenFileReadOnly bir şifre (password) parametresi kabul eder, ancak şifrelenmiş (encrypted) girdiler (inputs) için tam bir ayrıştırmaya (full parse) geri döner (falls back), çünkü şifre çözme (decryption), şifreleme sözlüğünü çözmek için nesne ağacını (object tree) gerektirir. Kaynak dosyalarınız şifrelenmişse, önce şifrelenmemiş (unencrypted) bir kopya elde etmek için bunları DecryptFile ile çözün, ardından bunu Doğrudan Dosya API'si (Direct File API) ile açın. Dosya düzeyindeki DecryptFile işlevi, standart şifreleme için doğrudan bir AES-256 yeniden yazma yolu (direct AES-256 rewrite path) alır ve büyük dosyalar (large files) için LoadFromFile ve ardından SaveLoadedDocument işlemlerinden daha hızlıdır, çünkü bellekte (in-memory) tam bir nesne modeli (object model) oluşturmaz
Büyük toplu (large-batch) işleme sırasında bellek
Düzinelerce dosyayı bir döngüde (loop) işleyen toplu işler (batch jobs), doğru görünen ancak bellek (memory) biriktiren bir modele sahiptir: döngünün içinde THotPDF oluşturmak, LoadFromFile'ı çağırmak, işi yapmak, Free'yi çağırmak. Bu yapısal (structurally) olarak iyidir. Sorun, iç çalışmanın karalama (scratch) nesneleri ayırması (allocates), istisnaları (exceptions) yakalaması (catches) ve bu karalama nesnelerini hata yollarında (error paths) canlı bırakmasıdır. Delphi'nin bellek yöneticisi sıkıştırma yapmaz (does not compact), bu nedenle toplu çalıştırma boyunca yüz hata yolu sızıntısı (leak), belleği diğer her şey için ayırmayı (allocation) yavaşlatacak kadar yukarı itebilir (push)
Düzeltme o kadar da yabancı (exotic) değil. Her THotPDF ve PDF çalışmasına katılan (participates) her ara (intermediate) TStream veya TBitmap, Free'nin son ifade (last statement) olduğu bir try/finally bloğuna (block) aittir. Başlatma (initialization) işlemi kısmen başarısız olduğunda finally dalının (branch) if Assigned(x) then x.Free öğesini güvenle kullanabilmesi için try ifadesinden önce yerel işaretçileri nil olarak ayarlayın. Bu standart Delphi sahiplik disiplinidir (ownership discipline) ve bu tür sorunların (class of problem) tüm hikayesi (full story) budur
Toplu bağlamlarda bilinmesi gereken bir şey daha var: AddImage, her görüntüyü kurulmakta olan belgeye kaydeder ve HotPDF o kayıtları, EndDoc o dosyayı bitirdiğinde serbest bırakır. Kayıt defteri LoadFromFile'ı değil BeginDoc/EndDoc döngüsünü izler; o yüzden toplu iş için temiz desen, belge başına tek THotPDF örneğidir: kurun, işi yapın, kaydedin, serbest bırakın
Herhangi bir şeyi değiştirmeden önce ölçmek
Bu kalıpların (patterns) hiçbirine ulaşmadan önce ölçün (measure). Delphi'nin System.Diagnostics içindeki TStopwatch özelliği QueryPerformanceCounter'ı sarar (wraps) ve dosya G/Ç'sinin (file I/O) duvar saati profilini (wall-clock profiling) çıkarmak için yeterince doğrudur (accurate). Yalnızca LoadFromFile öğesini sarın (wrap) ve ne kadar süre kapsadığını görün (see). Eğer toplam sürenin %90'ı ise, düzeltme Direct File API'dir veya aynı dosyayı (same file) kaç kez ayrıştırdığınızı (parse) azaltmaktır (reducing). %20'nin (20%) altındaysa, darboğaz (bottleneck) başka (somewhere else) bir yerdedir (somewhere) ve yanlış (wrong) şeyi takip ediyorsunuzdur (chasing)
Bu yazıyı başlatan iki dakikalık çıkarma, tümüyle tekrarlanan yükleme deseninden çıktı. Belge yapısı hiç katkı yapmadı; düz bir sayfa ağacı da aynı biçimde çalışırdı. Tek bir LoadFromFile ve ardından tek bir ExtractPagesToFile çağrısına geçmek, başka hiçbir şeye dokunmadan aynı donanımda süreyi yaklaşık 1,3 saniyeye indirdi
Burada gösterilen sayfa düzenleme API'si, Delphi ve C++Builder için HotPDF Delphi Component parçasıdır