Pekerjaan pelaporan berjalan lancar selama setahun. Pekerjaan itu membangun sebuah workbook, mengisi sebuah sheet dengan apa pun yang dikembalikan oleh kueri, lalu menyimpannya. Kemudian seorang pelanggan dengan data historis lima tahun meminta ekspor penuh, jumlah baris melampaui angka satu juta, dan proses mati dengan error kehabisan memori (out-of-memory) jauh sebelum file mencapai disk. Tidak ada yang salah dengan kodenya. Kode itu menahan seluruh workbook di dalam RAM agar dapat menyerialkannya pada bagian akhir, dan memori yang dibutuhkannya membesar seiring dengan jumlah baris yang diminta untuk ditulisnya
Perbaikannya bukan dengan menggunakan mesin yang lebih besar, melainkan model penulisan yang berbeda. Penulis langsung streaming di HotXLS memancarkan (emits) paket OOXML secara inkremental seiring dengan kedatangan baris, sehingga memori yang digunakannya tidak bergantung pada berapa banyak baris yang Anda tulis. Ini adalah pasangan sisi-tulis (write-side) dari pembaca streaming: di mana pembaca menyusuri sheet besar tanpa membangun pohon sel, penulis pun memproduksi sheet tanpa membangun pohon sel juga
Mengapa jalur simpan normal membesar seiring data
Jalur TXLSXWorkbook reguler membangun sebuah model objek penuh terlebih dahulu. Setiap sel, dengan nilai, jenis, dan referensi gayanya, hidup sebagai objek di dalam memori hingga Anda memanggil proses simpan, di mana seluruh pohon tersebut kemudian diserialkan ke dalam paket. Model tersebut adalah yang paling tepat ketika Anda ingin membaca sheet, mengeditnya, menghitung ulang, dan menulisnya kembali, karena akses acak ke sel mana pun adalah persis seperti yang dibutuhkan oleh pengeditan. Namun, ini adalah model yang salah ketika Anda menuangkan baris-baris dalam satu arah dan tidak pernah menoleh ke belakang, karena Anda membayar untuk mempertahankan setiap baris di memori tanpa memperoleh manfaat apa pun. Sejuta baris objek tetaplah sejuta baris objek, terlepas dari apakah Anda akan mengunjunginya kembali atau tidak
Penulis streaming menyingkirkan pohon tersebut. Segera setelah sel ditulis, sel itu menjadi barisan byte di dalam bagian worksheet, dan byte-byte itu diserahkan ke output zip. Stream worksheet adalah satu-satunya buffer yang tumbuh, dan ia tumbuh di sisi output, bukan sebagai objek-objek Delphi yang aktif di heap. Yang tetap berada di memori (residen) hanyalah sejumlah kecil pencatatan (bookkeeping) tetap: nama-nama sheet, beberapa penanda (flags), nomor baris saat ini, pencacah sel. Kumpulan tersebut tidak berubah dari baris pertama hingga baris kesepuluh juta
Tabel shared-string adalah jebakan, dan string inline adalah jalan keluarnya
Sebagian besar penulis XLSX streaming bekerja dengan baik hingga mereka bertemu dengan teks. Format OOXML biasanya menyimpan string di dalam tabel shared-string (string bersama): setiap string yang berbeda ditulis sekali ke dalam bagian terpisah, dan setiap sel yang menampung string tersebut membawa indeks ke tabel alih-alih teks itu sendiri. Ini adalah pengoptimalan ruang yang bagus untuk file-file yang dipenuhi label berulang, dan ini merupakan standar yang digunakan oleh jalur simpan standar. Masalahnya bagi penulis streaming sangat brutal. Untuk melakukan deduplikasi, tabel harus tetap berada di memori selama seluruh pekerjaan berlangsung, karena baris mana pun yang belum datang bisa saja mengulangi string dari baris yang sudah ditulis, dan hanya peta dalam-memori lengkap berisi string-string yang pernah dilihat yang mampu memberikan indeks yang tepat. Jadi, satu struktur yang tidak dapat di-streaming oleh penulis streaming justru adalah struktur yang seharusnya membuat file menjadi lebih kecil. Data yang sarat teks menggagalkan streaming yang Anda inginkan
Penulis langsung mengesampingkan tabel itu seluruhnya. String ditulis secara sebaris (inline), sebagai sel t="inlineStr" di mana teksnya berada langsung di dalam sel dengan elemen <is><t>. Tidak ada tabel yang harus diakumulasikan dan tidak ada peta string yang harus ditahan, sehingga kolom teks tidak memakan memori lebih banyak ketimbang kolom numerik. Komprominya sangat jelas dan layak dinyatakan secara terus terang. String inline mengulangi teks yang sama di mana pun ia muncul, sehingga file dengan banyak label identik berukuran lebih besar pada disk ketimbang yang menggunakan shared-string. Anda mengorbankan ukuran file untuk membeli memori konstan. Untuk ekspor satu-jalan (one-pass), itu adalah kompromi yang tepat, dan pada dasarnya kompresi zip pun akan menyerap sebagian besar pengulangan tersebut saat pembuatan file akhirnya
Tabel gaya tiba di bagian akhir, dengan satu format tanggal
Gaya menghadirkan ketegangan yang sama dengan string. Workbook merujuk pemformatannya melalui sebuah bagian gaya, dan penulis streaming tidak dapat mempertahankan palet gaya yang membesar selaras dengan sel-sel yang telah ia siram (flushed). Penulis langsung menjawab ini dengan mempertahankan tabel gaya tetap kecil dan terfiksasi, lalu memancarkannya saat penutupan, bukan di awal. Satu format sel standar menutupi sel-sel biasa. Satu format angka tanggal menutupi tanggal, yang diregistrasikan dengan kode format yyyy-mm-dd pada posisi yang diketahui di dalam daftar format sel
Format tanggal tersebut adalah alasan mengapa WriteDateTime eksis sebagai panggilannya sendiri. Excel tidak memiliki tipe tanggal bawaan (native date type); tanggal adalah angka yang memakai format tanggal. WriteDateTime menulis nilai sebagai nomor serial polos dan menandai sel itu dengan satu-satunya gaya tanggal, sehingga spreadsheet merendernya sebagai sebuah tanggal ketimbang sebagai angka bulat lima-digit. Nomor serial yang ditulisnya penting untuk pertukaran (round-tripping). Metode ini menyimpan nilai TDateTime secara langsung menggunakan sistem tanggal 1900, yang merupakan konvensi serupa dengan yang digunakan jalur simpan reguler TXLSXWorkbook. Karena kedua jalur sepakat soal nomor serial, file yang diproduksi penulis streaming akan terbaca kembali dengan baik melalui pembaca HotXLS dan terbuka di Excel dengan tanggal yang cocok dengan apa yang Anda maksudkan, tanpa ada kejutan meleset-satu (off-by-one) atau kesalahan epoch antara penulis dan pembaca
Urutan itu wajib, karena byte-byte sudah berlalu
Streaming menukar profil memorinya dengan satu aturan yang harus Anda patuhi. Output dipancarkan selagi Anda berjalan dan tidak dapat dikunjungi ulang, jadi segalanya harus ditulis dalam urutan sebagaimana mereka muncul di dalam file. Dalam suatu baris, sel-sel berurutan menaik (ascending) berdasarkan kolom. Dalam suatu sheet, baris-baris berurutan menaik. Tidak ada buffer yang membiarkan penulis mengurutkan sel-sel Anda belakangan, karena baris yang baru saja Anda tutup sekarang sudah menjadi byte-byte di dalam stream zip dan tidak bisa lagi dijangkau. Berikan kolom 5 kemudian kolom 2 pada baris yang sama maka output akan menjadi cacat, karena penulis semata-mata memancarkan apa yang Anda berikan dalam urutan yang Anda berikan kepadanya
API baris memiliki sedikit kenyamanan untuk kasus umum. AddRow menerima indeks baris berbasis 1, tetapi melempar 0 berarti mengambil baris berikutnya setelah baris sebelumnya, sehingga pengisian sekuensial tidak perlu melacak dan melempar pencacah yang terus meningkat. Setiap AddRow menutup baris sebelumnya, dan setiap AddSheet menutup sheet sebelumnya, sehingga Anda tidak pernah secara eksplisit mengakhiri sebuah baris atau sheet. Anda memulai yang berikutnya dan penulis memfinalkan struktur terbuka tersebut untuk Anda
Penglepasan (escaping) ditangani di mana teks memasuki XML
Teks apa pun yang Anda tulis menjadi bagian dari dokumen XML, sehingga kelima entitas XML pradefinisi harus dilepaskan (escaped) atau paket itu seketika tidak valid saat suatu nilai mengandung ampersand atau kurung sudut. Penulis melepaskan &, <, >, ", dan ' untuk Anda, baik pada teks string inline maupun pada teks formula, dua tempat di mana karakter yang dipasok oleh pemanggil akan mendarat di dalam markup. Anda melempar WideString mentah dan penulis menjadikannya aman. Nama produk seperti Smith & Co <Ltd> atau sebuah formula yang merujuk pada nama sheet dalam tanda kutip akan keluar sebagai XML yang terbentuk dengan baik tanpa perlu Anda lakukan penglepasan sama sekali
Siklus hidup, dan mengapa Destroy masih melakukan penutupan
Menyelesaikan paket adalah langkah yang menulis bagian workbook, bagian gaya, bagian tipe-konten (content-types) dan relasi, serta direktori sentral zip di bagian akhir. Pekerjaan itu terjadi di dalam Close. Paket yang tidak pernah ditutup adalah zip tidak lengkap yang tidak akan dibuka oleh program spreadsheet mana pun, jadi penutupan (closing) bukanlah pembersihan operasional opsional, melainkan langkah yang membuat file menjadi valid. Sebagai penjagaan dari kelupaan pemanggilan Close pada jalur error, Destroy melakukan upaya penutupan sebisanya jika paket masih terbuka, sehingga membebaskan (freeing) penulis tidak menyebabkan kebocoran objek zip di bawahnya bahkan ketika sebuah exception membuat pemanggilan eksplisit tersebut terlewatkan. Pola yang andal tetaplah pola Delphi pada umumnya: lakukan penulisan di dalam balok try, panggil Close, lalu panggil pembebasan di balok finally
Men-streaming sheet besar dari ujung ke ujung
Bentuk pekerjaan ini adalah: mulai, tambahkan sheet, tuangkan baris, tutup. Contoh di bawah ini menulis baris header lalu disusul urutan panjang baris data bertipe, yang mencampurkan string, angka, formula tanpa hasil ter-cache, serta tanggal. Memori yang digunakannya untuk sepuluh baris dan untuk sepuluh juta baris adalah sama saja, karena setiap sel berangkat menuju stream zip segera setelah ia ditulis
uses
lxDirectWrite;
procedure StreamReport(const Path: string; RowCount: Integer);
var
W: TXLSDirectWriter;
I: Integer;
begin
W := TXLSDirectWriter.Create;
try
W.BeginFile(Path);
W.AddSheet('Sales');
// Header row, written in ascending column order
W.AddRow(1);
W.WriteString(1, 'Item');
W.WriteString(2, 'Qty');
W.WriteString(3, 'Price');
W.WriteString(4, 'Total');
W.WriteString(5, 'Date');
// Data rows; pass 0 to AddRow to take the next row automatically
for I := 1 to RowCount do
begin
W.AddRow(0);
W.WriteString(1, 'Item ' + IntToStr(I));
W.WriteNumber(2, I);
W.WriteNumber(3, 1.5 + (I mod 10));
W.WriteFormula(4, Format('B%d*C%d', [I + 1, I + 1]));
W.WriteDateTime(5, EncodeDate(2026, 1, 1) + I);
end;
W.Close; // finalises the package
finally
W.Free;
end;
end;
Sheet kedua cukup dipanggil dengan AddSheet lain sebelum Anda melanjutkannya, dan sang penulis menutup sheet pertama saat ia membuka sheet yang kedua. Tanda (flags) Boolean menggunakan WriteBoolean, yang akan menulis sebuah sel boolean bertipe ketimbang sekadar teks "True". Jika Anda ingin memastikan file tersebut utuh dan dapat digunakan kembali (round-trips), properti CellCount melaporkan berapa banyak sel yang ditulis, dan dengan membaca kembali hasil tersebut dengan pembaca streaming harusnya melaporkan total yang sama pula
// A second sheet of typed flags after the data sheet above
W.AddSheet('Flags');
W.AddRow(1);
W.WriteString(1, 'Name');
W.WriteString(2, 'Active');
W.AddRow(0);
W.WriteString(1, 'alpha');
W.WriteBoolean(2, True);
WriteLn(Format('wrote %d cells', [W.CellCount]));
Menulis ke stream alih-alih file, dapat digunakan kode yang sama tetapi dengan BeginStream sebagai ganti BeginFile, yang memungkinkan server untuk mengirim workbook tersebut ke respons HTTP atau memory stream tanpa menggunakan file sementara pada disk. Penulis tidak memiliki (own) stream yang Anda lempar, sehingga Anda memegang kontrol atas siklus hidupnya
Apabila tugas tersebut adalah titik henti server yang membangun workbook berdasarkan permintaan, ragam pola pada penulisan streaming untuk pekerjaan batch dan server menunjukkan cara merangkainya ke dalam penangan permintaan dan ekspor terjadwal. Ketika pertanyaannya adalah besaran muatan (cost) yang lebih luas dari workbook sangat besar, baik itu dalam membaca maupun menulis, bahasan terkait hal ini tertuang di performa workbook besar di Delphi mencakup ke mana waktu dan memori tersebut sebenarnya dihabiskan. Penulis langsung streaming dikirim sebagai bagian dari Komponen HotXLS untuk Delphi dan C++Builder, bersisian dengan API baca, edit, serta simpan secara penuh yang dicakup di tempat lain di blog ini