Teknik Makale

Delphi Sunucu Toplu İşleri için HotXLS Akışlı Yazmalar

Her gece çalışan bir Delphi servisinin müşteri başına birer adet XLSX dosyası ürettiğini varsayalım; birkaç yüz dosya ve bazısı 400.000 satır genişliğinde. Bunu analiz ettiğinizde (profile) sürpriz nadiren hücre doldurma döngüsündedir. Genellikle SaveAs çağrısındadır. Varsayılan yazıcı ile, her çalışma sayfası, o dize OOXML zip dosyasına sıkıştırılmadan önce tek bir bellek içi XML dizesine serileştirilir ve geniş bir sayfa için geçici dize, üzerine kurulduğu hücre modelini gölgede bırakabilir. Dolayısıyla verilerini rahatça oluşturan ve 800 MB'ta oturan bir iş, kaydetme sırasında 2 GB'lık kapsayıcı sınırını aşacak ve kimse izlemezken saat 03:00'te OOM (bellek yetersiz) hatasıyla çökebilecektir. losLab'in Delphi ve C++Builder için yerel elektronik tablo kütüphanesi olan HotXLS, doğrudan bu sıçramayı hedefleyen bir özelliğe sahiptir: StreamingWrite. Bunun etrafında, toplu bir işçinin bellek ve zaman bütçesi içinde kalıp kalmayacağını belirleyen iki manivela daha bulunur: satır düzeyinde yazma geri çağırmaları (write callbacks) ve stil havuzunun sıkı bir döngü içindeki davranış biçimi

Varsayılan kaydetme yolunun neleri arabelleğe aldığı ve StreamingWrite'ın neleri değiştirdiği

Varsayılan XLSX yazıcısı basitliği tercih eder. Çalışma sayfası XML'ini tamamen oluşturur, ardından bitmiş dizeyi zip sıkıştırıcısına teslim eder. Bu, sayfanın XML'inin tamamının birkaç megabayta sığdığı çalışma kitaplarının ezici çoğunluğu için doğru ticarettir. Bir sayfanın serileştirilmiş biçimi yüzlerce megabayta ulaştığında ise bu durum doğru olmaktan çıkar. Elektronik tablo XML'i ayrıntılıdır: her sayısal hücre onlarca karakter biçimlendirme işaretine mal olur ve tüm bunları tutan dizenin bitişik (contiguous) olması gerekir. Bir bellek grafiğinde bu imza gözden kaçırılamaz: Satırlar dolarken uzun, düz bir plato, ardından SaveAs sırasında keskin bir üçgen sıçrama ve zip boşaltıldığında çöküş

Book.StreamingWrite := True ayarını yapmak, SaveAs işlevini, oluşturulduğu anda sayfa XML'ini doğrudan zip akışına aktaran bir çalışma sayfası yazıcısına geçirir. Ara dize hiçbir zaman tahsis edilmez (allocate) ve üçgen sıçrama gürültüye dönüşür

Bunun size gerçekte ne kazandırdığı konusunda kesin olun, çünkü bunu abartmak yanlış kapasite planlarına yol açar. Bu bayrak yalnızca kaydetme yolunu değiştirir. Çalışma kitabını oluşturmak hala bellek içi hücre modelinin tamamını tahsis eder, dolayısıyla doldurma aşamasındaki plato öncekiyle tamamen aynı yüksekliktedir. Ortadan kalkan şey, kaydetme sırasında o platonun üzerine binen serileştirme sıçramasıdır ve 400k satır dolduran bir iş için bu sıçrama, genellikle bellek bütçesine sığmak ile onu aşmak arasındaki tüm farkı oluşturur. Özellik, geçmiş davranışı korumak için varsayılan olarak False değerini alır, bu nedenle dahil olmak kasıtlı olarak yazdığınız açık bir satırdır

Bayrak etkinken toplu dışa aktarma

BoldIdx := Book.Fonts.Add('Calibri', 11, True, False); // havuz dizini, 0 tabanlı
Sheet := Book.Sheets.Add('Bulk');
for R := 1 to 100000 do
begin
  Sheet.Cells[R, 1].Value := R;
  Sheet.Cells[R, 2].Value := 'Row ' + IntToStr(R);
  Sheet.Cells[R, 3].Value := R * 1.5;
  if (R mod 1000) = 0 then
    Sheet.Cells[R, 2].FontIndex := BoldIdx + 1; // hücrede 1 tabanlı
end;
Book.StreamingWrite := True; // sayfa XML'ini doğrudan zip içine aktarır
Book.SaveAs('bulk.xlsx');

Cells[R, C] talep üzerine hücreler oluşturur, bu da döngü gövdesini temiz tutar. Akılda tutulması gereken iki tablo sınırı vardır: XlsxMaxRow ve XlsxMaxCol olarak sunulan 1.048.576 satır ve 16.384 sütun. Satır sınırını aşan bir veri akışının kendi kodunuzda sayfalar arasında bölünmesi gerekir. Aşağı akıştaki hiçbir şey aşımı fark etmez veya sizin yerinize düzeltmez ve dosya sınırda kesilmiş olarak kalır

Hücre başına Variant yükü olmadan satırları doldurma

Her hücre değeri ataması, bir hücre araması ve bir Variant dönüşümü maliyeti getirir. On bin satırda bunu kimse fark etmez. Her biri yirmi sütundan oluşan bir milyon satırda, çağrı başına gelen bu yük doldurma aşamasının baskın maliyeti haline gelir ve analizör (profiler) doğrudan bunu gösterecektir. Toplu arayüzler, bunun yerine yazıcıya tek seferde bütün bir satırı vermenize olanak tanır. WriteRows, çağrı başına bir satır sağlayan bir geri çağırmayı (callback) yönlendirir:

procedure TBulkExporter.FillRow(Sender: TObject; SheetIndex, Row, FirstCol,
  LastCol: Integer; var Values: Variant; var Skip: Boolean;
  var Cancel: Boolean);
begin
  if not FReader.Next then
  begin
    Cancel := True;              // veri kaynağı tükendi: temizce dur
    Exit;
  end;
  Values := VarArrayCreate([FirstCol, LastCol], varVariant);
  Values[FirstCol]     := FReader.RecordId;
  Values[FirstCol + 1] := FReader.CustomerName;
  Values[FirstCol + 2] := FReader.Amount;
end;

// 2..100001 satırlarını, A..C sütunlarını doldurur, okuyucudan çeker
Sheet.WriteRows(2, 1, 100001, 3, FillRow);

Cancel bayrağı, sabit bir satır aralığını "N satıra kadar" haline getiren şeydir; bu, satır sayısı henüz yürütülmesini bitirmediğiniz bir sorgudan geldiğinde doğal şekildir. Skip daha hafif bir dokunuştur: çalışmayı durdurmadan tek bir satırı boş bırakır. Hücreleri doldurmanın ötesinde, geri çağırma, aksi takdirde bir doldurma döngüsüne garip şekillerde cıvatalanacak operasyonel endişeler için iyi bir yuva haline gelir. Her bin satırda bir tik atan bir ilerleme sayacı, iş zamanlayıcıdan sorgulanan bir iptal belirteci, kaynak veri tabanından okumalarda bir hız sınırlayıcı: bunların tümü hücre yazma koduna işlenmek yerine tek bir yerde yaşar. Okuma tarafında, ForEachRow ve ForEachCell aynı kalıbı yansıtır, bu da toplu bir işin büyük dosyaları hem tükettiği hem de ürettiği durumlarda önem taşır

Stil havuzları yukarı taşımayı ödüllendirir

XLSX stil modeli paylaşılan havuzlardan oluşur. Fonts.Add, Fills.AddSolid ve Borders.Add yöntemlerinin tümü 0 tabanlı bir havuz dizini döndürür ve bir hücre, sıfırın çalışma kitabı varsayılanı için ayrıldığı FontIndex içinde bu dizin artı bir değerini saklayarak bir yazı tipine başvurur. +1 değeri yukarıdaki toplu örnekte tam oradadır. Bunu unutursanız hücre sessizce yanlış stili alır çünkü stil havuzu dizinindeki bir eksik/fazla hata hala geçerli bir dizindir ve hiçbir şey hata fırlatmaz

Bunu takip eden disiplin, her stil nesnesini satır döngüsünden önce oluşturmak ve döngü içinde dizinine başvurmaktır. Fonts.Add aynı tanımları tekilleştirir, bu nedenle satır başına bir kez çağırmak yalnızca CPU'yu boşa harcar. Alignments.Add ise bir tuzaktır çünkü her çağrıda yeni bir girdi döndürür. 100k satırlık bir döngü içinde bu, styles.xml dosyasını yüz bin yinelenen hizalama kaydı altında gömer, bu da diskteki dosyayı şişirir ve kopyalar yeniden ayrıştırılırken Excel'deki her sonraki açılışı yavaşlatır. Her stili döngü dışında bir kez oluşturun, ardından ihtiyacınız olduğu kadar çok kez dizinine başvurun

Akışlar, geçici dizinler ve tüm bunların etrafındaki toplu döngü

Bunların hiçbiri bir dosya sistemi gerektirmez. Her iki arayüz de IO yüzeylerinde TStream aşırı yüklemelerini taşır; Open, SaveAs, SaveAsCSV, SaveAsHTML ve SaveAsODS bunlardan bazılarıdır, böylece toplu bir işçi diske hiç dokunmadan doğrudan bulut depolama alanı veya bir HTTP yanıtı için tasarlanmış bir TMemoryStream içine yazabilir. Hatırlanmesi gereken keskin bir nokta vardır. SaveAs(Stream) akışın geçerli konumundan yazar ve sonrasında geri sarmaz, bu nedenle akışı teslim eden her neyse ona vermeden önce Position := 0 ayarını kendiniz yapın, aksi takdirde tüketici sıfır bayt okur. XLS arayüzü kendine ait iki düğme ekler. SetTempDir, BIFF yazıcısının geçici dosyalarını bunları alacak alana ve IO genişliğine sahip bir birime yönlendirir; bu, varsayılan geçici yolun sıkışık bir sistem diskinde bulunduğu sunucularda önem taşır. UseSharedFormulas, yinelenen formül gövdelerini paylaşılan gruplara katlar; bu, bir formülün tüm sütun boyunca kopyalandığı klasik rapor şekli için gerçek bir boyut küçültmedir

for FileName in SourceFiles do
begin
  Book := TXLSXWorkbook.Create;        // taze örnek: durum sızıntısı yok
  try
    Book.StreamingWrite := True;
    if Book.Open(FileName) <> 1 then
      Continue;                        // bir kötü girdi tüm grubu öldürmemeli
    Book.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
  finally
    Book.Free;
  end;
end;

Dosya başına yeni bir çalışma kitabı örneği oluşturmak mikrosaniyelere mal olur ve tüm bir dosyalar arası bulaşma hatası kategorisini ortadan kaldırır: dosya 17'deki stillerin, tanımlanmış adların ve belge özelliklerinin dosya 18'e sızacak bir yolu kalmaz. Başarısız bir Open işleminde atla-ve-devam-et yöntemi de aynı derecede değerini kanıtlar çünkü 600 dosyadan oluşan bir gruptaki tek bir kesintili yükleme, çalışmanın geri kalanı yerine size tek bir günlük satırına mal olmalıdır. CSV bacağının kasıtlı olarak neyi yapmadığını da belirtmekte fayda var. SaveAsCSV formülleri düz metin olarak yazar ve onları asla değerlendirmez, bu nedenle tüketicileri hesaplanmış sayılar bekleyen bir dönüştürme grubunun önce ilgili hücrelerde Calculate çalıştırması veya zaten önceki bir hesaplamadan önbelleğe alınmış sonuçları taşıyan çalışma kitaplarından başlaması gerekir

Eşzamanlılık modeli: İş parçacığı (thread) başına bir çalışma kitabı

Hiçbir arayüzün nesneleri thread-safe (iş parçacığı açısından güvenli) değildir ve tasarım hiçbir zaman aksini iddia etmemiştir. Örnekler arasında paylaşılan küresel bir durum olmadığından, ölçeklendirme kuralı iş parçacıkları arasında bir çalışma kitabının paylaşılmaması koşuluyla iş parçacığı başına tek bir çalışma kitabıdır. Her biri kendi TXLSXWorkbook nesnesine sahip N adet işçiden oluşan bir havuz, bellek tavan olana kadar neredeyse doğrusal olarak ölçeklenir ve bu tavan bir sayı ile ifade edilebilir: en büyük eşzamanlı hücre modelinin işçi sayısı ile çarpımı artı StreamingWrite'ın düzleştirdiği kaydetme süresi yükü. Kuyruk derinleştiğinde, yazıcının içi yerine iş kuyruğuna geri basınç (back-pressure) uygulayın. Bir çalışma kitabını yarım yazmış aç bir iş parçacığı yararlı hiçbir şey üretmemişken, boş bir işçi için birkaç saniye bekleyen bir iş sağlam bir şekilde tamamlanır

Paylaşılan formüller, okuma tarafında grafikleri atlama ve XLS'ye özgü manivelalar dahil olmak üzere daha geniş optimizasyon resmi için büyük çalışma kitabı performans kılavuzuna bakın. Satırları doğrudan bir sorgudan gelen toplu işler, Delphi raporları için veri tabanı dışa aktarım kalıplarında ayrı olarak ele alınmıştır

HotXLS, Delphi veya C++Builder servisinize hiçbir harici bağımlılığı olmadan yerel Object Pascal olarak derlenir; sürümler ve lisanslama HotXLS Bileşeni ürün sayfasında yer almaktadır