40 sayfalık bir PDF'den üç sayfayı kopyalamanın iki dakika sürmesi bir performans ayarlama (performance tuning) sorunu değildir. Bu, yanlış API yolunun kullanıldığının bir işaretidir. Bu zamanlamayı ilk kez bir HotPDF Bileşeni sayfa kopyalama örneğinde (page-copy sample) gördüğümde, içgüdüm önce kod yerine belge yapısına bakmaktı. Bu sıranın önemli olduğu anlaşıldı
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
Bunların hiçbiri bir kütüphane (library) hatası değildir. Her ikisi de bir iş (job) için tasarlanmış API'yi seçen ve onu başka bir iş için kullanan çağıranlardır (callers)
Sayfa çıkarma için InsertPagesFromDocument kullanımı
Belirli bir sayfa aralığını bir HotPDF belgesinden diğerine kopyalamanın doğru yolu (path), kaynakta LoadFromFile işleminden sonra InsertPagesFromDocument çağrısı yapmaktır (called). Kaynağı (source) bir kez yüklersiniz, hedefi (destination) bir kez yükler veya oluşturursunuz, sayfaları taşır ve kaydedersiniz. Kaynak tüm sayfa ekleme işlemleri (page insertions) boyunca bellekte (memory) kalır:
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 parametresi, komut satırı örneğiyle (command-line sample) aynı biçimi kabul eder: '1-3' veya '1,5,7-9' gibi virgülle ayrılmış sayfa numaraları veya aralıkları (ranges) listesi. Sayfalar 1 tabanlıdır (1-based). InsertPagesFromDocument, meta verilerine (metadata), yer imlerine (bookmarks) veya kopyalanan sayfalardan başvurulmadığı (referenced) sürece gömülü (embedded) dosya eklerine dokunmadan içerik akışlarını (content streams), kaynak sözlüklerini (resource dictionaries) ve sayfa geometrisini kopyalar. 40 sayfalık bir belgeden üç sayfalık bir çıkarma (extraction) için bu küçük bir çalışma kümesidir (working set)
Daha önce iki dakika süren aynı 12 MB'lık dosyada zamanlama: bu modelle 1,5 saniyenin altındadır. Bu sürenin çoğu tek (single) LoadFromFile çağrısıdır. Nesne tablosu ilk kez çözümlendiğinde belge yapısı alakasız (irrelevant) hale gelir
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 is a byte-preserving copy, no re-serialization
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 (batch contexts) kontrol edilmesi gereken bir şey daha var: AddImage, görüntüleri THotPDF örneğinin kullanım ömrü (lifetime) boyunca devam eden (persists) dahili bir listeye (internal list) kaydeder. LoadFromFile'ı tekrar tekrar (repeatedly) çağırarak birçok belgede (documents) tek bir örneği yeniden kullanırsanız, önceki belgelerdeki (documents) resim (image) kayıtları (registrations) listede kalır (stay). Ya her belge için yeni (fresh) bir örnek (instance) oluşturun (create) ya da belgeler (documents) arasında resim listesini (image-list) temizleme yolunu (clear path) çağırın (call)
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ı (post) başlatan (started) iki dakikalık çıkarma (extraction) işleminin tamamen tekrarlanan yükleme modelinden (repeated-load pattern) kaynaklandığı (turned out) anlaşıldı (turned out). Belge yapısı hiçbir katkıda bulunmadı; düz (flat) bir sayfa ağacı da (page tree) aynı (same) şekilde çalışırdı. Tek (single) bir LoadFromFile ve ardından bir InsertPagesFromDocument çağrısına (call) geçmek (switching), başka (anything else) hiçbir şeye (anything) dokunmadan aynı (same) donanımda (hardware) 1,3 saniyeye getirdi (brought)
Burada (here) gösterilen sayfa (page) manipülasyon (manipulation) API'si, Delphi ve C++Builder için HotPDF Bileşeninin bir parçasıdır