Implementasi Lazy Loading Pada Pembaca Teks

Mengapa Notepad terdiam ketika membaca berkas besar? Membuat pembaca teks untuk memahami proses pemuatan data yang efisien pada sistem operasi

Mufid
Pujangga Teknologi
7 min readMay 21, 2019

--

Mari kita membuka berkas log berukuran kecil dengan Notepad pada komputer berbasis Windows:

Membuka berkas kecil berukuran 7,52 KB

Sebagaimana Anda tahu, Notepad adalah penyunting teks yang cukup sederhana untuk digunakan. Anda dapat membuka berkas teks apapun dengan Notepad. Akan tetapi, Notepad menjadi tidak sederhana apabila Anda ingin membuka berkas berukuran besar:

Membuka berkas berukuran 23,2 MB membuat Notepad terdiam! Tidak merespon sama sekali

Terdiamnya Notepad dalam membuka berkas besar menjadi latar belakang pembahasan tulisan ini. Bahkan ini menjadi sangat menarik. Dengan memahami bagaimana membaca berkas di editor teks, kita jadi mempelajari bagaimana sistem operasi bekerja! Kita akan mencoba menjawab pertanyaan berikut:

  • Mari tebak-tebakan: Mengapa Notepad terdiam ketika membuka berkas besar?
  • Apa yang rumit dari membaca berkas? Bukankah semudah File.read saja? Bukankah antarmuka pemrogramannya sudah ada?

Agar artikel ini lebih menarik, saya akan sertakan simulasi membuat pembaca teks. Sehingga kita akan lebih memiliki bayangan dari masalah-masalah yang kita hadapi. Kita tidak akan benar-benar membahas bagaimana Notepad bisa terdiam — karena kita memang tidak memiliki kode sumbernya. Akan tetapi, kita akan coba memahami bagaimana pemuatan data hingga ke layar komputer — dan secara hipotetik mencoba menganalisa mengapa Notepad lama sekali ketika membuka berkas berukuran besar.

Memuat Berkas ke Memori

Secara naif, untuk dapat menampilkan informasi ke layar komputer, kita butuh meletakannya dalam memori. Di bahasa pemrograman, ini dilakukan dengan operator penunjukan ke suatu variabel tertentu. Isi dari variabel inilah yang akan ditampilkan ke layar. Sebagai contoh, pada C# kita dapat melakukan ini:

Mengatur teks dengan cara seperti ini tampak tidak akan menjadi masalah kalau kita menampilkan teks berukuran kecil. Akan tetapi, jika kita membuka berkas berukuran besar dan memasukan untaian string langsung ke properti Text, untaian string tersebut langsung dimasukan ke dalam memori. Jika teks yang dimasukan sangat besar, memori akan digunakan untuk menaruh informasi yang bahkan tidak masuk di layar. Ini pemborosan.

Saya menduga itulah yang dilakukan oleh Notepad. Notepad mencoba memindahkan semua isi berkas ke dalam memori. Ini menyebabkan program lama sekali membuka berkas berukuran besar. Pada akhirnya, Notepad akan membukanya. Akan tetapi, saya tidak menunjukan di tangkapan layar di atas karena memang sangat lama.

Membuka Berkas secara Naif

Banyak bahasa pemrograman telah menyediakan antarmuka pemrograman untuk membaca keseluruhan isi berkas hanya dengan satu baris. Misalnya di Java, kita dapat membaca berkas dengan cara berikut:

Akan tetapi, sekali lagi, meski cara ini mudah, cara ini tidak bisa kita gunakan untuk menampilkan membaca berkas besar. Program akan stuck — seperti yang terjadi pada Notepad di tangkapan layar sebelumnya. Padahal, tidak semua isi berkas ingin ditampilkan. Hanya sebagian saja yang ingin ditampilkan. Oleh karenanya, kita bisa menggunakan konsep lazy loading — hanya muat data ketika dibutuhkan.

Ragam Cara Membuka Berkas

Pada dasarnya, ada tiga cara membuka berkas:

  • Sekuensial penuh
  • Sekuensial bertahap
  • Acak

Akses sekuensial penuh berarti mengakses semua berkas, dari awal sampai akhir, kemudian dimasukkan semuanya ke dalam memori. Ini lumrah dilakukan untuk ukuran berkas yang kecil. Semisal, ketika membuka berkas konfigurasi. Atau, membuka berkas penanda. Akan tetapi, jika Anda membuka berkas besar dan memuatnya langsung ke memori, ini akan sangat lambat dan hanya membuang-buang sumber daya saja.

Akses sekuensial bertahap berarti mengakses berkas secara sekuensial, dari awal hingga akhir. Jika ditulis dalam kode, kira-kira bentuknya akan seperti ini:

Program akan membaca baris berikutnya, jika memang pengguna memintanya. Karena pembacaannya bertahap, program tidak menggunakan seluruh sumber daya yang ada. Sesuai kode di atas, implementasi ini hanya memungkinkan kita untuk membaca baris selanjutnya. Tidak dengan baris sebelumnya. Omong-omong, memang ada implementasi seperti ini di dunia nyata? Membaca baris per baris?

Yakni, perintah more di sistem operasi berbasis UNIX. Perintah more mengizinkan Anda membaca baris per baris berkas dengan menekan ENTER. Meski demikian, sebenarnya more dan secara internal mereka mengimplementasi sebuah buffer circular linkedlist sehingga dia juga bisa membaca baris sebelumnya. Saya hanya ingin menunjukkan bahwa membaca baris per baris tetaplah praktikal jika memang itu kebutuhan Anda.

Membaca baris demi baris dengan more

Pada pengaksesan berkas secara acak, kita hanya perlu mengakses bagian yang kita inginkan, mengakhiri pembacaan pada bagian yang kita inginkan juga. Kita bisa mulai di sembarang posisi, berakhir di sembarang posisi. Akan tetapi, ini agak tricky: kita perlu mengimplementasi semacam pubsub event. Kita hanya akan membaca berkas ketika pengguna menggulung area teksnya. Program akan berhenti membaca ketika layar sudah penuh dengan teks.

Membuka Berkas secara Lebih Efisien dengan Lazy Loading

Kita mengetahui posisi gulung dari sebuah jendela. Kita mengetahui besarnya jendela. Kita mengetahui jumlah teks yang bisa ditampilkan dari ukuran jendela tersebut. Semua informasi ini bisa kita gunakan untuk hanya menampilkan teks yang dibutuhkan. Tidak perlu memuat seluruh data di memori. Akan tetapi, kita tidak bisa menggunakan perintah semacam readAllLines pada paragraf sebelumnya. Kita perlu mengakses berkas secara acak.

Mengapa perlu akses acak? Ini karena pengguna bisa dengan leluasa pindah dan gulung ke bagian yang pengguna inginkan. Akan tetapi, bagian yang ingin pengguna tampilkan belum tersedia di memori. Oleh karenanya perlu dimuat terlebih dahulu di memori.

Akses acak paling primitif adalah dengan menggunakan Seek. Misalnya, saat ini kita membaca bytes ke-0. Jika kita mau membaca byte ke-10, kita lakukan perintah Seek(10). Selanjutnya, jika kita mau membaca byte ke-25, lakukan perintah Seek(15). Selanjutnya, jika kita mau membaca byte ke-17, lakukan perintah Seek(-8). Kita perlu mencatat posisi saat ini sebelum berpindah ke posisi yang kita inginkan dengan Seek.

Pada C#, kita bisa menggunakan perintah FileStream.seek. Akan tetapi, secara internal, FileStream juga mencatat posisi saat ini sehingga dengan FileStream.Position. Perintah Position memungkinkan untuk pergi ke suatu posisi tertentu di suatu berkas — bukan posisi relatif lagi. Semisal, tampilkan byte ke-2841 dari panjang berkas yang berukuran 74274343 bytes. Setelah itu, lakukan pembacaan sebanyak ukuran window yang ada.

private void ReadAfterJumpEvent()
{
this.FileStream.Position = targetPosition;
byte[] output = new byte[WindowSize];
for (int i = 0; i < WindowSize; i++)
{
output[i] = (byte)this.FileStream.ReadByte();
}

var EventArgs = new ReceivedDataEventArgs(Encoding.UTF8.GetString(output), InternalPosition);
SeekDoneEvent(this, EventArgs);
}

Mari kita lihat performa dari program ini dalam membuka berkas berukuran besar:

Implementasi menggunakan akses acak seperti ini memungkinkan kita membuat program yang membaca berkas berukuran 34 MB, tetapi penggunaan memori sangat ringan, hanya 3-5 MB. Itupun karena overhead penggunaan .NET Framework.

Pengembangan Implementasi Saat Ini

Tertarik untuk bereksperimen lebih lanjut? Silahkan fork repositori ini di Github! Saya memiliki beberapa ide untuk membuat proof-of-concept ini lebih menarik:

  • Program akan memulai membaca baris pada karakter tertentu, bukan pada awal baris. Ini karena baris dipisahkan oleh CRLF (atau LF). Untuk memisahkan per baris, kita harus berjalan mundur dari offset.
  • Besarnya buffer untuk window sudah ditanam (hard-code) untuk memudahkan proof-of-concept dengan jendela berukuran tetap. Jika jendelanya diubah, kita harus secara dinamis menentukan jumlah buffer yang dibutuhkan setiap kali melakukan seek.

Implementasi pada bahasa lain

Saya menggunakan C# agar lebih mudah membuat proof-of-concept dengan tampilan antarmuka di Windows. Pada dasarnya, kita dapat membuat pembaca teks yang efisien ini pada bahasa manapun.

Di Ruby, kita dapat menggunakan IO#seek untuk melakukan pengaksesan berkas secara acak. Di Java, kita dapat menggunakan RandomAccessFile untuk mengakses sembarang posisi untuk suatu berkas.

Pengaksesan berkas/IO secara acak adalah hal yang lumrah untuk sistem operasi manapun. Anda dapat mengecek dokumentasi bahasa favorit Anda dan mulai mengimplementasi akses acak untuk proyek perangkat lunak yang relevan.

Pada Perangkat Bergerak dan Web

Implementasi lazy loading seperti ini cukup lazim di area selain desktop application. Pada gim, gim tidak akan memproses objek secara detail hingga objeknya cukup dekat. Ada RecyclerView di Android dan UICollectionView di iOS yang secara cerdas menentukan mana yang harus dimuat, tergantung dari posisi gulungan tampilan pengguna.

Kesimpulan

Mari kita mulai kesimpulan dengan menjawab pertanyaan di awal artikel:

Pertanyan 1: Mengapa Notepad terdiam ketika membuka berkas besar?
Dugaan terbesarnya adalah karena Notepad mencoba memasukkan semua isi berkas dari disk ke memori. Selama berkas belum selesai dimasukkan ke memori, program belum akan merespon. Ini ditunjukkan dari peningkatan penggunaan memori selama membuka berkas tersebut.

Pertanyaan 2: Apa yang rumit dari membaca berkas?
Ternyata, membaca berkas erat hubungannya dengan bagaimana berkas itu diproses. Berhubung kita akan membuat editor teks, maka kita perlu membaca bagian berkas yang diperlukan saja. Tidak perlu semuanya diproses dan dimasukan ke memori.

Untuk merangkum, dalam tulisan ini kita telah membahas:

  • bagaimana data dimuat sebelum ditampilkan ke layar
  • bagaimana cara menulis program yang sangat efisien, yang bisa membuka dan memproses input yang besar.
  • setidaknya ada tiga cara mengakses berkas sistem. Yaitu secara sekuensial utuh, secara sekuensial bertahap, dan secara acak.

Sampai jumpa pada kesempatan mendatang!

--

--