Mengganti hot path di JavaScript aplikasi Anda dengan WebAssembly

Produk ini selalu cepat, jadi

Dalam versi sebelumnya artikel yang saya gunakan untuk membahas cara WebAssembly memungkinkan Anda untuk membawa ekosistem {i>library<i} C/C++ ke web. Satu aplikasi yang yang memanfaatkan library C/C++ secara ekstensif adalah squoosh, web yang memungkinkan Anda mengompresi gambar dengan berbagai codec yang dikompilasi dari C++ ke WebAssembly.

WebAssembly adalah mesin virtual tingkat rendah yang menjalankan bytecode yang disimpan dalam .wasm file. Kode byte ini diketik dan disusun sedemikian rupa bahwa hal itu dapat dikompilasi dan dioptimalkan untuk sistem {i>host<i} jauh lebih cepat daripada JavaScript dapat melakukannya. WebAssembly menyediakan lingkungan untuk menjalankan kode yang telah {i>sandbox<i} dan {i>embedding<i} sejak awal.

Berdasarkan pengalaman saya, sebagian besar masalah performa di web disebabkan oleh dan cat yang berlebihan tapi sesekali aplikasi perlu melakukan tugas komputasi yang mahal dan membutuhkan banyak waktu. WebAssembly dapat membantu di sini.

{i>Hot Path<i}

Di squoosh, kita menulis fungsi JavaScript yang memutar {i>buffer <i}gambar sebanyak 90 derajat. Meskipun OffscreenCanvas akan ideal untuk ini, itu tidak didukung di seluruh browser yang kami targetkan, dan sedikit bug di Chrome.

Fungsi ini mengiterasi setiap piksel gambar input dan menyalinnya ke posisi yang berbeda pada gambar output untuk mencapai rotasi. Untuk 4094px x gambar 4096px (16 megapiksel) yang diperlukan lebih dari 16 juta iterasi blok kode bagian dalam, yang kita sebut "jalur panas". Meskipun begitu besar, jumlah iterasi, dua dari tiga {i>browser<i} yang kami uji menyelesaikan tugas dalam 2 detik atau kurang. Durasi yang dapat diterima untuk jenis interaksi ini.

for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

Namun, satu browser memerlukan waktu lebih dari 8 detik. Cara browser mengoptimalkan JavaScript sangat rumit, dan mesin yang berbeda mengoptimalkannya untuk berbagai hal. Sebagian mengoptimalkan eksekusi mentah, sebagian lainnya mengoptimalkan interaksi dengan DOM. Di beberapa kasus ini, kita menemukan jalur yang tidak dioptimalkan di satu browser.

Di sisi lain, WebAssembly dibangun sepenuhnya berdasarkan kecepatan eksekusi mentah. Namun jika ingin performa yang cepat dan dapat diprediksi di seluruh browser untuk kode seperti ini, WebAssembly dapat membantu.

WebAssembly untuk performa yang dapat diprediksi

Secara umum, JavaScript dan WebAssembly dapat mencapai kinerja puncak yang sama. Namun, untuk JavaScript, performa ini hanya dapat dicapai di "jalur cepat", dan sering kali sulit untuk tetap berada di "jalur cepat" itu. Salah satu manfaat utama yang Penawaran WebAssembly adalah kinerja yang dapat diprediksi, bahkan di seluruh browser. Kebijakan dan arsitektur tingkat rendah memungkinkan compiler untuk memperkuat jaminan sehingga kode WebAssembly hanya harus dioptimalkan sekali dan akan selalu menggunakan “jalur cepat”.

Menulis untuk WebAssembly

Sebelumnya kami mengambil library C/C++ dan mengompilasinya ke WebAssembly untuk menggunakan fungsionalitas di web. Kita tidak benar-benar menyentuh kode {i>library<i}, kami baru saja menulis sejumlah kecil kode C/C++ untuk membentuk jembatan antar {i>browser<i} dan perpustakaan. Kali ini motivasi kita berbeda: Kita ingin menulis sesuatu dari awal dengan mempertimbangkan WebAssembly sehingga kita dapat memanfaatkan keunggulan yang dimiliki WebAssembly.

Arsitektur WebAssembly

Saat menulis untuk WebAssembly, ada baiknya untuk memahami sedikit lebih banyak tentang apa itu WebAssembly.

Untuk mengutip WebAssembly.org:

Saat mengompilasi sepotong kode C atau Rust ke WebAssembly, Anda akan mendapatkan .wasm yang berisi deklarasi modul. Deklarasi ini terdiri dari daftar "impor" yang diharapkan oleh modul dari lingkungannya, daftar ekspor yang menyediakannya untuk {i>host<i} (fungsi, konstanta, potongan memori) dan tentu saja instruksi biner aktual untuk fungsi yang ada di dalamnya.

Sesuatu yang tidak saya sadari sampai saya melihat sesuatu ini: Tumpukan yang membuat WebAssembly adalah "mesin virtual berbasis stack" tidak disimpan dalam potongan memori yang digunakan modul WebAssembly. Tumpukan tersebut sepenuhnya menggunakan VM-internal dan tidak dapat diakses oleh developer web (kecuali melalui DevTools). Dengan demikian sangat mungkin untuk menulis modul WebAssembly yang tidak memerlukan memori tambahan sama sekali dan hanya gunakan stack VM-internal.

Dalam kasus ini, kita perlu menggunakan beberapa memori tambahan untuk memungkinkan akses arbitrer piksel gambar dan menghasilkan versi gambar yang diputar. Ini adalah untuk apa WebAssembly.Memory.

Manajemen memori

Umumnya, setelah Anda menggunakan memori tambahan, Anda akan merasa perlu untuk mengelola memori itu. Bagian memori mana yang digunakan? Mana yang gratis? Di C, misalnya, Anda memiliki fungsi malloc(n) yang menemukan ruang memori dari n byte berturut-turut. Fungsi semacam ini juga disebut "alokator". Tentu saja, implementasi alokator yang digunakan harus disertakan dalam WebAssembly dan akan meningkatkan ukuran file Anda. Ukuran dan performa ini fungsi manajemen memori ini dapat sangat bervariasi tergantung pada algoritma yang digunakan, itulah sebabnya banyak bahasa menawarkan beberapa implementasi yang dapat dipilih ("dmalloc", "emmalloc", "wee_alloc", dll.).

Dalam kasus ini, kita tahu dimensi gambar input (dan karenanya gambar output) sebelum kita menjalankan modul WebAssembly. Kita melihat peluang: Biasanya, kita akan meneruskan buffer RGBA gambar input sebagai ke fungsi WebAssembly dan menampilkan gambar yang diputar sebagai dengan sejumlah nilai. Untuk menghasilkan nilai yang ditampilkan tersebut, kita harus menggunakan alokator. Tapi karena kita mengetahui jumlah total memori yang dibutuhkan (dua kali ukuran gambar, sekali untuk input dan sekali untuk output), kita dapat menempatkan gambar input ke dalam Memori WebAssembly menggunakan JavaScript, jalankan modul WebAssembly untuk membuat Kedua, gambar diputar, lalu gunakan JavaScript untuk membaca kembali hasilnya. Kita bisa mendapatkan tanpa menggunakan manajemen memori sama sekali!

Dimanjakan dengan pilihan

Jika Anda melihat fungsi JavaScript asli yang ingin kita lakukan pada WebAssembly, Anda dapat melihat bahwa ini adalah kode tanpa API khusus JavaScript. Dengan demikian, harus benar-benar lurus meneruskan kode ini ke bahasa apa pun. Kami mengevaluasi 3 bahasa yang berbeda yang dikompilasi ke WebAssembly: C/C++, Rust, dan AssemblyScript. Satu-satunya pertanyaan yang perlu kita jawab untuk setiap bahasa adalah: Bagaimana cara kita mengakses memori mentah tanpa menggunakan fungsi manajemen memori?

C dan Emscripten

Emscripten adalah compiler C untuk target WebAssembly. Tujuan Emscripten adalah untuk berfungsi sebagai pengganti langsung untuk kompilator C terkenal seperti GCC atau clang dan sebagian besar kompatibel dengan flag. Ini adalah bagian inti dari misi Emscripten karena ia ingin mengompilasi kode C dan C++ yang ada ke WebAssembly semudah sebaik mungkin.

Mengakses memori mentah merupakan sifat C dan ada banyak pointer di alasan:

uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;

Di sini kita mengubah angka 0x124 menjadi pointer ke 8-bit yang tidak ditandatangani bilangan bulat (atau byte). Tindakan ini secara efektif mengubah variabel ptr menjadi array dimulai dari alamat memori 0x124, yang dapat kita gunakan seperti array lainnya, memungkinkan kita untuk mengakses {i>byte<i} individual untuk membaca dan menulis. Dalam kasus ini, kita sedang melihat {i>buffer <i}RGBA dari sebuah gambar yang ingin kita susun ulang untuk mencapai kunci. Untuk memindahkan piksel, kita harus memindahkan 4 byte berturut-turut sekaligus (satu byte untuk setiap saluran: R, G, B, dan A). Untuk memudahkannya, kita dapat membuat array bilangan bulat 32-bit yang tidak ditandatangani. Sesuai konvensi, gambar input akan dimulai di alamat 4 dan gambar output kita akan dimulai langsung setelah gambar input berakhir:

int bpp = 4;
int imageSize = inputWidth * inputHeight * bpp;
uint32_t* inBuffer = (uint32_t*) 4;
uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);

for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

Setelah mem-port seluruh fungsi JavaScript ke C, kita dapat mengompilasi file C dengan emcc:

$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c

Seperti biasa, emscripten menghasilkan file kode glue yang disebut c.js dan modul wasm disebut c.wasm. Perhatikan bahwa modul wasm di-gzip menjadi hanya ~260 Byte, sedangkan kode lem sekitar 3,5KB setelah gzip. Setelah sedikit mengutak-atik, kami bisa berhenti kode glue dan buat instance modul WebAssembly dengan API vanilla. Ini sering kali dapat dilakukan dengan Emscripten selama Anda tidak menggunakan apa pun dari library standar C.

Rust

Rust adalah bahasa pemrograman baru dan modern dengan sistem jenis yang lengkap, tanpa runtime dan model kepemilikan yang menjamin keamanan-memori dan keamanan-thread. Karat juga mendukung WebAssembly sebagai fitur inti dan tim Rust telah berkontribusi banyak alat yang sangat bagus untuk ekosistem WebAssembly.

Salah satu alat tersebut adalah wasm-pack, dengan kelompok kerja Rutwasm. wasm-pack mengambil kode Anda dan mengubahnya menjadi modul yang mudah digunakan di web dan berfungsi siap pakai dengan pemaket seperti webpack. wasm-pack sangat pengalaman yang nyaman, tetapi saat ini hanya berfungsi untuk Rust. Grup ini mempertimbangkan untuk menambahkan dukungan untuk bahasa penargetan WebAssembly lainnya.

Di Rust, slice adalah array yang ada di C. Dan seperti di C, kita perlu membuat yang menggunakan alamat awal kita. Hal ini bertentangan dengan model keamanan memori yang diberlakukan Rust, jadi untuk memulai, kita harus menggunakan kata kunci unsafe, memungkinkan kita menulis kode yang tidak sesuai dengan model tersebut.

let imageSize = (inputWidth * inputHeight) as usize;
let inBuffer: &mut [u32];
let outBuffer: &mut [u32];
unsafe {
    inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);
    outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);
}

for d2 in 0..d2Limit {
    for d1 in 0..d1Limit {
    let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier;
    outBuffer[i as usize] = inBuffer[in_idx as usize];
    i += 1;
    }
}

Mengompilasi file Rust menggunakan

$ wasm-pack build

menghasilkan modul wasm 7,6KB dengan sekitar 100 byte kode lem (keduanya setelah gzip).

AssemblyScript

AssemblyScript adalah metode project muda yang ingin menjadi compiler TypeScript-to-WebAssembly. Penting penting untuk dicatat, bagaimanapun, bahwa itu tidak akan hanya memakai TypeScript. AssemblyScript menggunakan sintaksis yang sama dengan TypeScript, tetapi mengganti milik mereka sendiri. Library standar mereka memodelkan kemampuan WebAssembly. Itu berarti Anda tidak bisa begitu saja mengompilasi TypeScript yang Anda miliki ke WebAssembly, tetapi ini berarti Anda tidak perlu mempelajari bahasa pemrograman apa pun untuk menulis WebAssembly!

    for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
      for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
        let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
        store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));
        i += 1;
      }
    }

Mengingat platform tipe kecil yang dimiliki fungsi rotate(), maka cukup mudah untuk memindahkan kode ini ke AssemblyScript. Fungsi load<T>(ptr: usize) dan store<T>(ptr: usize, value: T) disediakan oleh AssemblyScript untuk mengakses memori mentah. Untuk mengompilasi file AssemblyScript, kita hanya perlu menginstal paket npm AssemblyScript/assemblyscript dan jalankan

$ asc rotate.ts -b assemblyscript.wasm --validate -O3

AssemblyScript akan memberi kita modul wasm ~300 Byte dan tanpa kode lem. Modul ini hanya berfungsi dengan WebAssembly API biasa.

Forensik WebAssembly

7.6KB Rust sangat besar jika dibandingkan dengan 2 bahasa lainnya. Ada beberapa alat di ekosistem WebAssembly yang dapat membantu Anda menganalisis file WebAssembly Anda (terlepas dari bahasa yang digunakan untuk membuat) dan memberi tahu Anda apa yang terjadi dan juga membantu Anda memperbaiki situasi.

Berkelok-kelok

Twiggy adalah alat lain dari aplikasi Rust Tim WebAssembly yang mengekstrak banyak data penting dari sebuah WebAssembly ruang lingkup modul ini. Alat ini tidak khusus untuk Rust dan memungkinkan Anda memeriksa hal-hal seperti grafik panggilan modul, menentukan bagian yang tidak terpakai atau berlebihan dan mencari tahu bagian mana yang berkontribusi pada ukuran file total modul Anda. Tujuan terakhir dapat dilakukan dengan perintah top Twiggy:

$ twiggy top rotate_bg.wasm
Screenshot penginstalan Twiggy

Dalam hal ini kita dapat melihat bahwa sebagian besar ukuran file berasal dari alokator. Mengejutkan karena kode kita tidak menggunakan alokasi dinamis. Faktor kontribusi besar lainnya adalah "nama fungsi" subbagian.

wasm-strip

wasm-strip adalah alat dari WebAssembly Binary Toolkit, atau disingkat wabt. File ini berisi alat yang memungkinkan Anda memeriksa dan memanipulasi modul WebAssembly. wasm2wat adalah disassembler yang mengubah modul wasm biner menjadi dapat dibaca manusia. Wabt juga berisi wat2wasm yang memungkinkan Anda mengubah format yang dapat dibaca manusia ke dalam modul {i>binary wasm<i}. Sementara kita menggunakan dua alat pelengkap untuk memeriksa file WebAssembly, kami menemukan wasm-strip menjadi yang paling berguna. wasm-strip menghapus bagian yang tidak diperlukan dan metadata dari modul WebAssembly:

$ wasm-strip rotate_bg.wasm

Ini mengurangi ukuran file modul karat dari 7,5KB menjadi 6,6KB (setelah gzip).

wasm-opt

wasm-opt adalah alat dari Binaryen. Komputer itu membutuhkan modul WebAssembly dan mencoba mengoptimalkannya untuk ukuran dan performa berdasarkan bytecode. Beberapa alat seperti Emscripten sudah berjalan menggunakan {i>tool<i} ini, sementara beberapa yang lain tidak. Biasanya ada baiknya untuk mencoba dan menghemat sejumlah {i>byte<i} tambahan dengan menggunakan alat ini.

wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm

Dengan wasm-opt, kita dapat mengurangi beberapa byte lagi untuk menyisakan total 6,2 KB setelah gzip.

#![no_std]

Setelah berkonsultasi dan melakukan riset, kami menulis ulang kode Rust tanpa menggunakan library standar Rust, dengan menggunakan #![no_std] aplikasi baru. Ini juga menonaktifkan alokasi memori dinamis sepenuhnya, sehingga menghapus kode alokator dari modul kita. Mengompilasi file Rust ini dengan

$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs

menghasilkan modul wasm 1,6 KB setelah wasm-opt, wasm-strip, dan gzip. Meskipun masih lebih besar dari modul yang dihasilkan oleh C dan AssemblyScript, cukup ringan untuk dianggap sebagai perangkat yang ringan.

Performa

Sebelum mengambil kesimpulan hanya berdasarkan ukuran file — kami melakukan perjalanan ini untuk mengoptimalkan kinerja, bukan ukuran file. Jadi bagaimana kami mengukur kinerja dan bagaimana hasilnya?

Cara mengukur tolok ukur

Meskipun WebAssembly merupakan format bytecode tingkat rendah, format ini masih perlu dikirim melalui kompiler untuk menghasilkan kode mesin khusus {i>host<i}. Sama seperti JavaScript, compiler bekerja dalam beberapa tahap. Dikatakan sederhana: Tahap pertama lebih cepat dalam kompilasi tetapi cenderung menghasilkan kode yang lebih lambat. Setelah modul dimulai berjalan, browser mengamati bagian mana yang sering digunakan dan mengirimkannya melalui compiler yang lebih dioptimalkan tetapi lebih lambat.

Kasus penggunaan kita menarik karena kode untuk memutar gambar akan digunakan sekali, mungkin dua kali. Jadi dalam kebanyakan kasus, kita tidak akan pernah mendapatkan manfaat dari kompilator pengoptimalan. Hal ini penting untuk diingat ketika pembuatan tolok ukur. Menjalankan modul WebAssembly 10.000 kali dalam satu loop akan memberikan hasil yang tidak realistis. Untuk mendapatkan angka yang realistis, kita harus menjalankan modul sekali dan membuat keputusan berdasarkan angka-angka dari satu proses itu.

Perbandingan performa

Perbandingan kecepatan per bahasa
Perbandingan kecepatan per browser

Kedua grafik ini adalah tampilan yang berbeda pada data yang sama. Pada grafik pertama, kita bandingkan per browser, pada grafik kedua kami membandingkan per bahasa yang digunakan. Memohon perhatikan bahwa saya memilih skala waktu logaritmik. Penting juga bagi bahwa semua menggunakan gambar uji 16 megapiksel dan host yang sama komputer Anda, kecuali satu {i>browser<i}, yang tidak dapat dijalankan di komputer yang sama.

Tanpa menganalisis grafik ini terlalu banyak, jelas bahwa kita telah memecahkan masalah performa: Semua modul WebAssembly berjalan dalam waktu ~500 md atau kurang. Ini mengonfirmasi hal yang telah kita susun di awal: WebAssembly memberi Anda hasil yang dapat diprediksi tingkat tinggi. Apa pun bahasa yang kita pilih, varians di antara browser dan bahasanya sangat minim. Tepatnya: Standar deviasi JavaScript di semua browser adalah ~400 md, sedangkan standar deviasi semua Modul WebAssembly di semua browser berdurasi ~80 md.

Upaya

Metrik lainnya adalah upaya yang harus kita lakukan untuk membuat dan mengintegrasikan modul WebAssembly ke dalam squoosh. Sulit untuk menetapkan nilai numerik untuk jadi saya tidak akan membuat grafik apa pun tetapi ada beberapa hal yang ingin saya tunjukkan:

AssemblyScript tidak lancar. Tidak hanya memungkinkan Anda menggunakan TypeScript untuk menulis WebAssembly, membuat peninjauan kode sangat mudah bagi kolega saya, tetapi juga menghasilkan modul WebAssembly bebas lem yang sangat kecil dengan tingkat tinggi. Alat dalam ekosistem TypeScript, seperti prettier dan tslint, kemungkinan besar akan berfungsi.

Rust yang dikombinasikan dengan wasm-pack juga sangat praktis, tetapi lebih unggul lebih banyak proyek WebAssembly yang lebih besar adalah binding, dan manajemen memori diperlukan. Kami harus menyimpang sedikit dari {i>happy path<i} untuk mencapai ukuran file.

C dan Emscripten membuat modul WebAssembly yang sangat kecil dan berperforma tinggi perangkat yang tidak aman, tetapi tanpa keberanian untuk masuk ke kode {i>glue<i} dan menguranginya menjadi kebutuhan dasar, ukuran total (modul WebAssembly + kode lem) akhirnya menjadi cukup besar.

Kesimpulan

Jadi bahasa apa yang harus Anda gunakan jika Anda memiliki hot path JS dan ingin membuatnya lebih cepat atau lebih konsisten dengan WebAssembly. Seperti biasa terkait performa pertanyaan, jawabannya adalah: Tergantung. Jadi, apa yang kita kirimkan?

Grafik perbandingan

Membandingkan kompromi ukuran modul / performa dari berbagai bahasa yang kami gunakan, pilihan terbaik adalah C atau AssemblyScript. Kami memutuskan untuk mengirimkan Rust. Ada ada beberapa alasan untuk keputusan ini: Sejauh ini semua codec yang dikirimkan di Squoosh dikompilasi menggunakan Emscripten. Kami ingin memperluas pengetahuan tentang WebAssembly dan menggunakan bahasa berbeda dalam produksi. AssemblyScript adalah alternatif yang kuat, tetapi project ini relatif baru dan kompilator ini tidak setajam Rust compiler.

Sementara perbedaan ukuran file antara Rust dan ukuran bahasa lainnya terlihat cukup drastis pada grafik sebar, kenyataannya tidak terlalu besar: Memuat 500B atau 1.6KB bahkan melalui 2G membutuhkan waktu kurang dari 1/10 detik. Dan Rust diharapkan akan segera menutup kesenjangan dalam hal ukuran modul.

Dalam hal kinerja {i>runtime<i}, Rust memiliki rata-rata yang lebih cepat di seluruh {i>browser<i} daripada AssemblyScript. Terutama pada proyek-proyek yang lebih besar, Rust akan cenderung menghasilkan kode yang lebih cepat tanpa memerlukan pengoptimalan kode secara manual. Tapi itu seharusnya tidak menghalangi Anda menggunakan apa yang paling nyaman bagi Anda.

Itulah sebabnya: AssemblyScript telah menjadi penemuan yang luar biasa. Ini memungkinkan web pengembang untuk menghasilkan modul WebAssembly tanpa harus mempelajari di bahasa target. Tim AssemblyScript sangat responsif dan aktif yang berupaya memperbaiki toolchain mereka. Kami pasti akan mengawasi AssemblyScript pada masa mendatang.

Update: Rust

Setelah memublikasikan artikel ini, Nick Fitzgerald dari tim Rust mengarahkan kami ke buku Rust Wasm mereka yang luar biasa, yang berisi bagian tentang mengoptimalkan ukuran file. Mengikuti petunjuk di sana (terutama mengaktifkan pengoptimalan waktu penautan dan penanganan panik), memungkinkan kami menulis kode Rust “normal” dan kembali menggunakan Cargo (npm Rust) tanpa membuat ukuran file menjadi besar. Modul Rust berakhir hingga 370B setelah {i>gzip<i}. Untuk mengetahui detailnya, lihat PR yang saya buka di Squoosh.

Terima kasih banyak kepada Ashley Williams, Steve Klabnik, Nick Fitzgerald, dan Max Graey atas semua bantuannya dalam perjalanan ini.