Misalkan sebuah layanan Delphi yang berjalan tiap malam menghasilkan satu XLSX per pelanggan, beberapa ratus file, sebagian di antaranya selebar 400.000 baris. Lakukan profiling dan yang mengejutkan jarang berupa loop pengisian sel. Yang mengejutkan adalah panggilan SaveAs. Dengan writer default, setiap worksheet diserialisasi menjadi satu string XML tunggal di memori sebelum string itu dikompresi ke dalam zip OOXML, dan untuk sheet yang lebar, string sementara ini bisa jauh lebih besar daripada model sel tempatnya dibangun. Jadi sebuah job yang dengan nyaman membangun datanya dan bertahan di 800 MB akan melonjak melewati batas kontainer 2 GB selama proses penyimpanan, dan OOM killer yang mengajukan laporan bug pada pukul 03:00 saat tidak ada yang mengawasi. HotXLS, pustaka spreadsheet native losLab untuk Delphi dan C++Builder, memiliki sebuah properti yang menyasar langsung lonjakan itu: StreamingWrite. Di sekelilingnya ada dua tuas lagi yang menentukan apakah sebuah worker batch tetap berada dalam anggaran memori dan waktunya, yaitu callback penulisan tingkat-baris dan cara pool style berperilaku di dalam loop yang ketat
Apa yang Dibuffer oleh Jalur Penyimpanan Default, dan Apa yang Diubah oleh StreamingWrite
Writer XLSX default mengutamakan kesederhanaan. Ia merender XML worksheet secara lengkap, lalu menyerahkan string yang sudah jadi itu ke kompresor zip. Itu adalah pertukaran yang tepat untuk sebagian besar workbook, di mana seluruh XML sheet muat dalam beberapa megabyte saja. Pendekatan itu berhenti tepat ketika bentuk serialisasi satu sheet mencapai ratusan megabyte. XML spreadsheet itu verbose: setiap sel numerik menghabiskan puluhan karakter markup, dan string yang menampung semuanya harus contiguous. Pada grafik memori, tandanya sulit dilewatkan. Sebuah plateau datar yang panjang selagi baris-baris terisi, lalu lonjakan segitiga yang tajam selama SaveAs, lalu keruntuhan begitu zip-nya di-flush
Menyetel Book.StreamingWrite := True mengalihkan SaveAs ke sebuah worksheet writer yang memancarkan XML sheet langsung ke dalam stream zip saat XML itu dihasilkan. String perantara itu tidak pernah dialokasikan, dan lonjakan segitiga itu meleleh menjadi noise
Bersikaplah presisi soal apa yang sebenarnya didapat dari ini, karena melebih-lebihkannya akan mengarah pada rencana kapasitas yang keliru. Flag ini hanya mengubah jalur penyimpanan. Membangun workbook tetap mengalokasikan model sel penuh di memori, sehingga plateau selama fase pengisian tingginya persis sama seperti sebelumnya. Yang hilang adalah lonjakan serialisasi yang dulu menumpuk di atas plateau itu saat penyimpanan, dan untuk job yang mengisi 400 ribu baris, lonjakan itu biasanya adalah keseluruhan perbedaan antara muat dalam anggaran memori dan melampauinya. Properti ini defaultnya False untuk menjaga perilaku historisnya, sehingga mengaktifkannya adalah satu baris eksplisit yang Anda tulis dengan sengaja
Ekspor Massal dengan Flag Diaktifkan
Book := TXLSXWorkbook.Create;
try
BoldIdx := Book.Fonts.Add('Calibri', 11, True, False); // index pool, berbasis 0
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; // berbasis 1 pada sel
end;
Book.StreamingWrite := True; // stream XML sheet langsung ke zip
Book.SaveAs('bulk.xlsx');
finally
Book.Free;
end;
Cells[R, C] membuat sel sesuai kebutuhan, yang menjaga isi loop tetap bersih. Dua batas grid layak dihafal: 1.048.576 baris dan 16.384 kolom, diekspos sebagai XlsxMaxRow dan XlsxMaxCol. Sebuah data feed yang melampaui batas baris harus dipecah ke beberapa sheet dalam kode Anda sendiri. Tidak ada apa pun di hilir yang menyadari pelampauan itu atau memperbaikinya untuk Anda, dan file itu akan berakhir terpotong begitu saja pada batasnya
Mengisi Baris Tanpa Overhead Variant per Sel
Setiap penetapan Cells[R, C].Value membayar biaya pencarian sel dan konversi Variant. Pada sepuluh ribu baris tidak ada yang menyadarinya. Pada satu juta baris dengan masing-masing dua puluh kolom, overhead per-panggilan itu menjadi biaya dominan dari fase pengisian, dan profiler akan menunjuk langsung ke sana. Interface batch justru membiarkan Anda menyerahkan seluruh baris sekaligus kepada writer. WriteRows menjalankan sebuah callback yang menyediakan satu baris per pemanggilan:
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; // sumber data habis: berhenti dengan bersih
Exit;
end;
Values := VarArrayCreate([FirstCol, LastCol], varVariant);
Values[FirstCol] := FReader.RecordId;
Values[FirstCol + 1] := FReader.CustomerName;
Values[FirstCol + 2] := FReader.Amount;
end;
// isi baris 2..100001, kolom A..C, menarik data dari reader
Sheet.WriteRows(2, 1, 100001, 3, FillRow);
Flag Cancel itulah yang mengubah rentang baris tetap menjadi "hingga N baris," yang merupakan bentuk alami ketika jumlah baris berasal dari sebuah query yang belum selesai dieksekusi. Skip adalah sentuhan yang lebih ringan: ia membiarkan satu baris tertentu kosong tanpa menghentikan proses. Selain mengisi sel, callback ini ternyata juga menjadi tempat yang baik untuk urusan operasional yang jika tidak akan tertempel secara canggung pada loop pengisian. Sebuah counter progres yang berdetak setiap seribu baris, sebuah token pembatalan yang dipoll dari job scheduler, sebuah rate limiter pada pembacaan dari database sumber: semuanya tinggal di satu tempat, alih-alih dijalin ke seluruh kode penulisan sel. Di sisi baca, ForEachRow dan ForEachCell mencerminkan pola yang sama, yang penting ketika sebuah job batch sekaligus mengonsumsi dan menghasilkan file besar
Pool Style Memberi Imbalan pada Hoisting
Model styling XLSX adalah sekumpulan pool bersama. Fonts.Add, Fills.AddSolid, dan Borders.Add semuanya mengembalikan index pool berbasis 0, dan sebuah sel mereferensikan font dengan menyimpan index itu ditambah satu di FontIndex, di mana nol dicadangkan untuk default workbook. Tambahan +1 itu persis terlihat pada contoh massal di atas. Lupakan itu dan sel akan diam-diam mengambil style yang salah, karena kesalahan selisih satu dalam index pool style tetap merupakan index yang valid dan tidak ada apa pun yang memunculkan error
Disiplin yang mengikutinya adalah membuat setiap objek style sebelum loop baris dan mereferensikan index-nya di dalam loop. Fonts.Add melakukan deduplikasi definisi yang identik, sehingga memanggilnya sekali per baris hanya membuang-buang CPU. Alignments.Add adalah jebakannya, karena ia mengembalikan entri baru pada setiap panggilan. Di dalam loop 100 ribu baris, itu mengubur styles.xml di bawah seratus ribu record alignment duplikat, yang menggembungkan ukuran file di disk dan memperlambat setiap open berikutnya di Excel karena duplikat-duplikat itu harus di-parse ulang. Bangun setiap style sekali saja di luar loop, lalu referensikan index-nya sesering yang Anda butuhkan
Stream, Direktori Temp, dan Loop Batch di Sekelilingnya
Semua ini tidak membutuhkan file system sama sekali. Kedua facade membawa overload TStream di seluruh permukaan IO-nya, termasuk Open, SaveAs, SaveAsCSV, SaveAsHTML, dan SaveAsODS, sehingga sebuah worker batch bisa merender langsung ke sebuah TMemoryStream yang ditujukan ke blob storage atau respons HTTP tanpa pernah menyentuh disk. Ada satu sisi tajam yang perlu diingat. SaveAs(Stream) menulis dari posisi stream saat ini dan tidak melakukan rewind setelahnya, jadi setel sendiri Position := 0 sebelum menyerahkan stream itu ke apa pun yang mengirimkannya, atau konsumennya akan membaca nol byte. Facade XLS menambahkan dua tuas miliknya sendiri. SetTempDir mengarahkan file temporer milik writer BIFF ke sebuah volume yang punya ruang dan headroom IO untuk menampungnya, yang penting pada server di mana jalur temp default berada di disk sistem yang sempit. UseSharedFormulas melipat badan formula yang berulang ke dalam grup bersama, sebuah pengurangan ukuran nyata untuk bentuk laporan klasik di mana satu formula disalin ke bawah sepanjang satu kolom penuh
Loop batch itu sendiri sengaja dibuat membosankan:
for FileName in SourceFiles do
begin
Book := TXLSXWorkbook.Create; // instance baru: tidak ada state yang bocor
try
Book.StreamingWrite := True;
if Book.Open(FileName) <> 1 then
Continue; // satu input buruk tidak boleh mematikan seluruh batch
Book.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
finally
Book.Free;
end;
end;
Instance workbook baru per file menghabiskan biaya mikrodetik dan menghilangkan seluruh kategori bug kontaminasi lintas-file: style, defined name, dan properti dokumen dari file 17 tidak punya jalan untuk bocor ke file 18. Skip-and-continue pada Open yang gagal juga sama sepadannya, karena satu upload yang terpotong dalam batch 600 file seharusnya hanya menghabiskan biaya satu baris log, bukan sisa keseluruhan run. Layak disoroti juga apa yang sengaja tidak dilakukan oleh jalur CSV. SaveAsCSV menuliskan formula sebagai teks literal dan tidak pernah mengevaluasinya, sehingga batch konversi yang konsumennya mengharapkan angka hasil perhitungan harus menjalankan Calculate pada sel-sel yang relevan lebih dulu, atau dimulai dari workbook yang sudah membawa hasil cache dari perhitungan sebelumnya
Model Konkurensi: Satu Workbook per Thread
Objek dari kedua facade sama-sama tidak thread-safe, dan desainnya tidak pernah berpura-pura sebaliknya. Karena tidak ada state global bersama antar instance, aturan penskalaannya sederhana saja: satu workbook per worker thread, tanpa berbagi satu workbook lintas thread. Sebuah pool berisi N worker, masing-masing memiliki TXLSXWorkbook-nya sendiri, berskala mendekati linear hingga memori menjadi batas atasnya, dan batas atas itu adalah sesuatu yang bisa Anda hitung angkanya: model sel konkuren terbesar dikalikan jumlah worker, ditambah overhead saat penyimpanan mana pun yang sudah diratakan oleh StreamingWrite. Ketika antrean semakin dalam, terapkan back-pressure pada job queue, bukan di dalam writer. Sebuah thread yang kelaparan dan baru menulis separuh workbook tidak menghasilkan apa pun yang berguna, sedangkan job yang menunggu beberapa detik untuk mendapatkan worker bebas akan selesai dengan utuh
Untuk gambaran tuning yang lebih luas, termasuk shared formula, pelewatan grafik di sisi baca, dan tuas-tuas khusus XLS, lihat panduan performa workbook besar. Job batch yang barisnya berasal langsung dari sebuah query dibahas terpisah dalam pola ekspor database untuk laporan Delphi
HotXLS terkompilasi ke dalam layanan Delphi atau C++Builder Anda sebagai Object Pascal native tanpa dependensi eksternal; edisi dan lisensi tersedia di halaman produk HotXLS Delphi Component