Melihat Kinerja Aplikasi Desktop Berbasis Griffon dan simple-jpa


Sepuluh tahun silam saat bekerja sebagai developer di salah satu software house, seorang technical leader melarang saya untuk memakai Java Reflection dalam menghasilkan data transfer objects (DTO).   Alasannya: reflection lebih ‘berat’ dibanding copy paste!   Karena larangan tersebut, saya dan rekan-rekan menghabiskan waktu hampir seminggu lebih untuk modul tersebut.   Delivery proyek menjadi terlambat.   Iya, pada zaman tersebut, memakai reflection memang lebih ‘berat’ , tapi pertanyaan penting yang sering terlupakan adalah: seberapa ‘berat‘ atau seberapa lambat??   Apakah penalti kinerja ini cukup berharga untuk ditukarkan dengan berkurangnya ketepatan waktu dalam delivery proyek?  Apakah penalti kinerja ini memiliki poin lebih penting sehingga tidak apa-apa bila aplikasi menjadi rumit dan sulit di-maintain? Mempertimbangkan kinerja di atas segala-galanya adalah salah satu kecendurangan negatif dalam dunia software engineering yang disebut sebagai premature optimization.

Griffon adalah sebuah framework untuk aplikasi desktop yang berdasarkan pada bahasa Groovy.   Groovy sendiri adalah sebuah bahasa pemograman dinamis yang lebih lambat dibandingkan bahasa statis seperti Java.   Tapi pertanyaan adalah: seberapa lambat sebuah aplikasi desktop yang dikembangkan dengan Groovy? Apakah masih bisa diterima dalam batas kewajaran?

Untuk menjawab pertanyaan tersebut, saya akan melakukan pengujian dengan menggunakan sebuah aplikasi desktop yang dibuat dengan framework Griffon dan plugin simple-jpa.   Aplikasi ini akan membaca data invoice (faktur) dari sebuah file teks kemudian memproses setiap baris data tersebut: menyimpan invoice, menambah entri pada inventory, serta menambah entri pada pembayaran bila diperlukan.   Data lainnya yang dibutuhkan oleh invoice sudah tersimpan di tabel dengan jumlah yang mendekati pemakaian sehari-hari. Sebagai contoh, di tabel produk terdapat 7.360 record.  Sebuah jumlah yang tidak terlalu kecil dan tidak terlalu besar.

Jumlah baris yang harus diproses oleh aplikasi ini adalah 91.190 baris yang mewakili data invoice dan item-nya.   Aplikasi ini akan memakai Griffon 1.2.0, simple-jpa 0.4.2, Hibernate JPA 4.2.0.Final dan MySQL Server 5.6.11.   Selama pengujian, database hanya akan dipakai oleh program tersebut saja.   Pengujian dipakai untuk mensimulasikan aktifitas single user.  Btw, hasil percobaan ini hanya sebagai gambaran umum yang terbatas pada aplikasi yang saya uji tersebut.   Untuk hasil yang lebih akurat tentunya dibutuhkan aplikasi dan environment yang lebih terkendali dan fair.

Percobaan 1

Saya akan mulai dengan menjalankan aplikasi dalam modus hanya membaca dari database.   Pengguna biasanya akan lebih sering melihat data inventory atau mencari data pembayaran ketimbang membuat data invoice baru.   Hasil percobaan menunjukkan bahwa 91.190 baris data berhasil diproses dalam waktu 49 menit.   Ini sudah meliputi membaca tabel produk, membaca data suplier/konsumen, mengerjakan business logic, tapi hasil prosesnya tidak disimpan (tidak ada query INSERT).

Aplikasi percobaan berbasis Griffon dan simple-jpa ini dapat memproses hingga 31 transaksi dalam waktu 1 detik.   Saya akan menyebutnya sebagai 31 TPS.   Perhitungannya adalah 91.190 baris dibagi dengan 49 x 60 = 2940 detik.   Transaksi disini bukanlah transaksi dalam arti operasi database, melainkan satu kesatuan operasi membuat invoice baru serta implikasinya (pada pembayaran dan inventory).  Termasuk juga didalamnya operasi mencari suplier, konsumen atau produk berdasarkan kode.   Dengan demikian, sebuah transaksi akan terdiri atas lebih dari satu operasi SQL.

Percobaan 2

Walaupun saya yakin pengguna tidak akan men-klik setiap menu sebanyak 31 kali per detik, tapi angka tersebut cukup kecil.   Saya pun menyelidiki kenapa aplikasi Griffon ini begitu lambat.  Pada akhirnya saya menemukan jawabannya:

  1. Kode program memproses setiap baris data dalam looping.  Keseluruh looping ini akan dianggap sebagai satu transaction (disini saya merujuk pada database transaction).
  2. EntityManager selaku first level cache akan menampung data hasil query SELECT.   Beberapa query men-trigger terjadinya flushing.
  3. Semakin banyak data yang ada di first level cache, maka semakin lama proses validasi yang timbul akibat flushing yang di-trigger secara otomatis.

Untuk mengatasinya, saya mengubah struktur program dari:

...
beginTransaction()
baris.each {
   ... // baca data
   Product product = findProductByCode(productCode)
   Invoice invoice = new Invoice()
   invoice.tambahItem(...)
   if (bayar) {
      invoice.bayar(...)
   }
}
commitTransaction()
...

menjadi:

...
def total = 0
beginTransaction()
baris.each {
   ... // baca data
   Product product = findProductByCode(productCode)
   Invoice invoice = new Invoice()
   invoice.tambahItem(...)
   if (bayar) {
      invoice.bayar(...)
   }
   total++
   if (total%20000==0) clear() // memanggil EntityManager.clear()
}
commitTransaction()
...

Setelah perubahan, saya menemukan bahwa aplikasi tersebut dapat menyelesaikan tugasnya dalam waktu 6 menit.   Hal ini berarti meningkat dari 31 TPS menjadi 253 TPS.   Ini adalah peningkatan yang cukup drastis.

Kesimpulannya:  kode program business logic dengan Groovy terlihat memiliki kinerja yang wajar.   Bahasa dinamis memang lebih lambat dari bahasa statis, tapi seberapa besar dampaknya?   Kode program aplikasi ini juga memakai dynamic finders di simple-jpa untuk mencari entity berdasarkan kodenya.   Penggunaan method dinamis sering kali dianggap lebih lambat dibanding memanggil method secara biasa. Tapi seberapa besar selisih lambatnya? Hari ini saya menemukan jawabannya: layak dipakai🙂

Percobaan 3

Perjalanan belum selesai!   Kali ini saya akan mengubah kode program sehingga tidak hanya membaca dan membuat object baru di memori, tapi juga menuliskan hasil proses ke database.   Kali ini, saya perlu men-flush() EntityManager setiap 1000 kali baris diproses, sebelum men-clear() EntityManager tersebut.   Perintah flush() akan memaksa Hibernate untuk menuliskan perubahan tersebut ke database dengan melakukan eksekusi SQL.   Walaupun demikian, perubahan yang permanen tetap akan terjadi hanya setelah perintah commit() diberikan.   Jadi, bila saya membatalkan aplikasi di tengah perjalanan, maka tidak akan ada tabel yang isinya berubah.

Lalu, berapa banyak waktu yang dibutuhkan?  Hasil pengujian menunjukkan bahwa untuk memproses 91.190 baris dibutuhkan waktu selama 38 menit.   Dengan demikian kecepatannya adalah 40 TPS.   Hasilnya adalah 91.190 record di tabel Invoice, 17.484 record di tabel pembayaran, dan 129.049 record di tabel inventory.   Nilai 40 TPS adalah hal wajar dan bisa diterima, tapi masalahnya adalah data yang dihasilkan tidak akurat!!   Banyak baris duplikat ditemukan di tabel inventory.   Mengapa demikian?

Percobaan 4

Hal ini berkaitan dengan penggunaan @Canonical di Groovy.   Keinginan saya untuk mendapatkan ‘kenyamanan’ serba otomatis akhirnya membuat saya memperoleh ‘hukuman’.   simple-jpa memberikan @Canonical pada seluruh domain classes yang ada. Annotation ini akan membuat Groovy menciptakan constructor, equals(), hashCode() dan toString() secara otomatis.   Hibernate (lebih tepatnya Java) akan memakai hashCode() dan equals() untuk membandingkan apakah dua buah entity adalah entity yang sama atau berbeda.   Masalahnya: hashCode() dan equals() yang dihasilkan oleh @Canonical sepertinya tidak memperhatikan atribut pada superclass.   Padahal untuk sebuah entity yang diturunakn dari entity lainnya,  biasanya id atau natural primary key yang bisa dipakai untuk membandingkan kesamaan akan terletak di superclass.   Solusinya, saya perlu membuat isi equals() dan hashCode() secara manual pada domain class yang bermasalah.

Setelah dijalankan ulang, tidak ada lagi record yang duplikat di tabel inventory.   Saya juga sempat memanfaatkan kesempatan untuk meningkatkan efisiensi kode program.   Hasil akhirnya, seluruh baris bisa diproses dalam waktu 32 menit (47 TPS).   Data yang dihasilkan adalah 91.190 record di tabel invoice, 17.485 record di tabel pembayaran, dan 73.413 record di tabel inventory.

Kecepatan ini sudah lebih dari cukup mengingat ini adalah aplikasi desktop yang dipakai oleh pengguna tunggal dan hanya sesekali berinteraksi dengan sistem lainnya.

Percobaan 5

Apakah masih bisa lebih cepat?  simple-jpa 0.4.2 memungkinkan pengaturan flush mode untuk EntityManager secara global.   Nilai yang diperboleh adalah "AUTO" atau "COMMIT".   Secara default, di Hibernate JPA, nilainya adalah "AUTO".   Nilai ini menyebabkan Hibernate terkadang akan men-flush() secara otomatis.   Pada saat memanggil sebuah query,  Hibernate bisa saja akan men-flush() bila dirasa perlu.   Bila nilai flush mode adalah "COMMIT", maka proses flush() hanya akan dikerjakan pada saat transaksi di-commit() atau saat flush() dipanggil secara manual.   Pada dokumentasi JPA, disebutkan bahwa nilai "COMMIT" tidak disarankan karena terdapat kemungkinan menghasilkan data yang tidak konsisten. Tapi tidak ada salahnya saya mencoba.

Untuk itu, saya menambahkan baris berikut ini di Config.groovy:

griffon.simplejpa.entityManager.defaultFlushMode = "COMMIT"

Saya cukup terkejut melihat kecepatannya meningkat drastis.   Bahkan saya bisa dengan aman membuang flush() dan clear() yang tadinya secara periodik membersihkan EntityManager agar tidak lambat.   Seluruh perubahan akan diproses ke dalam memori terlebih dahulu tanpa menyentuh database (butuh memori yang besar?), lalu setelah selesai, mereka baru dimasukkan ke database secara keseluruhan.   Hasilnya? Transaksi dapat diselesaikan dalam waktu 8 menit (190 TPS)!   Atau dengan kata lain, untuk jangka waktu 1 detik, aplikasi yang saya uji ini dapat memproses hingga maksimal 190 penyimpanan data invoice baru termasuk segala business process-nya. Ini adalah nilai yang cukup menggembirakan.

Perihal Solid Snake
I'm nothing...

6 Responses to Melihat Kinerja Aplikasi Desktop Berbasis Griffon dan simple-jpa

  1. Komang Hendra Santosa mengatakan:

    downloadnya dmn yg versi 0.4.2 mas?saya cek di artifact mse blom ada..

  2. Hey there! Another thing you could try is replacing the usage of collection.each{} with forEach loops; the latter are usually faster. You can even make the code go faster if @CompileState is applied to the loop, this will make method calls be really fast as the whole MOP will be disabled. It goes withoutsaying that applying @CompileStatic disables a lof od Groovy’s dynamic features, so use it with care.

    • Solid Snake mengatakan:

      Thank you for the pointers. In my case, I’ve noticed 18% TPS increment after replacing all collection.each{} with Java’s forEach loops. Unfortunately, I can’t use @CompileStatic because I will need to rewrite my code to a new version that don’t depend on Groovy’s dynamic features. Guess I will do that only if it become too slow.

      Btw, thank for creating Griffon. Groovy and Griffon have proven to be useful in saving time and speeding up development.

  3. Tolhah Hamzah mengatakan:

    mas, kalau ingin menerapkan Perobaan 2 pada Griffon 1.5 dan simple-jpa 0.6 gimana ya mas?

    loop di bagian mana yang perliu diatasi? kalau dari yang saya lihat di bagian controller, saya hanya menemkan beberapa finder untuk table dan form..

    sementara ini saya baru melakukan optimasi dengan menonaktifkan finder pada form sebelum form benar2 digunakan..

    terima kasih mas

    • Solid Snake mengatakan:

      Pada percobaan 2, masalah yang saya hadapi adalah cache dari EntityManager menjadi penuh sebelum di-flush. Pada Hibernate, isi dari EntityManager akan seluruh diperiksa untuk di-invalidate setiap kali ada penambahan entity baru. Karena isinya sudah banyak, ada ribuan entity yang belum di-flush, maka proses menjadi lambat. Hal ini hanya terjadi bila memproses sangat banyak entity pada sebuah transaksi yang sama.

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: