Selalu cepat
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 telah 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
mempertimbangkan sandboxing dan penyematan 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 hingga kelipatan 90 derajat. Meskipun OffscreenCanvas akan ideal untuk hal ini, elemen ini tidak didukung di seluruh browser yang kami targetkan, dan sedikit bermasalah di Chrome.
Fungsi ini mengiterasi setiap piksel gambar input dan menyalinnya ke posisi yang berbeda pada gambar output untuk mencapai rotasi. Untuk gambar berukuran 4094x 4096 piksel (16 megapiksel), diperlukan lebih dari 16 juta iterasi blok kode 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. Jadi, jika kita menginginkan 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. Tipe ketat dan arsitektur tingkat rendah memungkinkan compiler membuat jaminan yang lebih kuat sehingga kode WebAssembly hanya perlu 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 lanjut tentang apa sebenarnya WebAssembly.
Untuk mengutip WebAssembly.org:
Saat mengompilasi potongan kode C atau Rust ke WebAssembly, Anda akan mendapatkan file .wasm
yang berisi deklarasi modul. Deklarasi ini terdiri dari daftar
"impor" yang diharapkan modul dari lingkungannya, daftar ekspor yang
modul ini sediakan untuk host (fungsi, konstanta, bagian memori), dan
tentunya petunjuk biner yang sebenarnya 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
Biasanya, setelah menggunakan memori tambahan, Anda akan merasa perlu untuk
mengelola memori tersebut. Bagian memori mana yang sedang 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
modul WebAssembly dan akan meningkatkan ukuran file Anda. Ukuran dan performa ini
dari 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 hasil dengan sejumlah nilai. Untuk menghasilkan nilai yang ditampilkan, kita harus menggunakan alokator. Namun, karena kita mengetahui jumlah total memori yang diperlukan (dua kali ukuran gambar input, sekali untuk input dan sekali untuk output), kita dapat memasukkan gambar input ke dalam memori WebAssembly menggunakan JavaScript, menjalankan modul WebAssembly untuk menghasilkan gambar ke-2 yang diputar, lalu menggunakan JavaScript untuk membaca kembali hasilnya. Kita dapat melakukannya tanpa menggunakan pengelolaan memori sama sekali.
Dimanjakan dengan pilihan
Jika Anda melihat fungsi JavaScript asli yang ingin kita berikan ke WebAssembly, Anda dapat melihat bahwa ini adalah kode tanpa API khusus JavaScript. Dengan demikian, Anda dapat dengan mudah mem-porting 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 ingin membuat kompilasi kode C dan C++ yang ada ke WebAssembly semudah 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 bilangan bulat (atau byte)
8-bit tanpa tanda tangan. Hal ini secara efektif mengubah variabel ptr
menjadi array
yang dimulai dari alamat memori 0x124
, yang dapat kita gunakan seperti array lainnya,
sehingga kita dapat mengakses setiap byte untuk membaca dan menulis. Dalam kasus ini, kita
melihat buffering RGBA dari gambar yang ingin kita urutkan ulang untuk mencapai
rotasi. Untuk memindahkan piksel, kita harus memindahkan 4 byte berturut-turut sekaligus
(satu byte untuk setiap saluran: R, G, B, dan A). Untuk mempermudah, kita dapat membuat
array bilangan bulat 32-bit tanpa tanda. 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-porting 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.
Hal 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 diterapkan Rust, jadi untuk mendapatkan cara kita, kita harus menggunakan kata kunci unsafe
,
yang memungkinkan kita menulis kode yang tidak mematuhi 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 project yang cukup baru yang bertujuan 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 library standar dengan library-nya 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 jenis kecil yang dimiliki fungsi rotate()
, kode ini cukup mudah di-porting 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 menjalankan
$ asc rotate.ts -b assemblyscript.wasm --validate -O3
AssemblyScript akan memberi kita modul wasm ~300 Byte dan tanpa kode lem. Modul ini hanya dapat digunakan dengan WebAssembly API biasa.
Forensik WebAssembly
Ukuran 7.6KB Rust sangat besar jika dibandingkan dengan 2 bahasa lainnya. Ada beberapa alat di ekosistem WebAssembly yang dapat membantu Anda menganalisis file WebAssembly (terlepas dari bahasa yang digunakan untuk membuatnya) 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 Rust dan memungkinkan Anda memeriksa hal-hal seperti
grafik panggilan modul, menentukan bagian yang tidak digunakan atau berlebihan, dan mencari tahu
bagian mana yang berkontribusi pada total ukuran file modul Anda. Tujuan
terakhir dapat dilakukan dengan perintah top
Twiggy:
$ twiggy top rotate_bg.wasm
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. Alat ini berisi
beberapa alat yang memungkinkan Anda memeriksa dan memanipulasi modul WebAssembly.
wasm2wat
adalah disassembler yang mengubah modul wasm biner menjadi
format yang 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.
Alat ini menggunakan modul WebAssembly dan mencoba mengoptimalkannya untuk ukuran dan
performa hanya berdasarkan bytecode. Beberapa alat seperti Emscripten sudah menjalankan
alat ini, beberapa alat lainnya tidak. Biasanya ada baiknya untuk
mencoba dan menghemat sejumlah
byte tambahan dengan
menggunakan alat ini.
wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm
Dengan wasm-opt
, kita dapat memangkas beberapa byte lagi sehingga totalnya
6,2 KB setelah gzip.
#![no_std]
Setelah beberapa konsultasi dan riset, kami menulis ulang kode Rust tanpa menggunakan
library standar Rust, menggunakan
fitur
#![no_std]
. 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, ukurannya cukup
kecil untuk dianggap ringan.
Performa
Sebelum mengambil kesimpulan hanya berdasarkan ukuran file — kami melakukan perjalanan ini untuk mengoptimalkan kinerja, bukan ukuran file. Jadi, bagaimana cara kami mengukur performa dan apa 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. Sederhananya: Tahap pertama jauh lebih cepat dalam mengompilasi, tetapi cenderung menghasilkan kode yang lebih lambat. Setelah modul mulai berjalan, browser mengamati bagian mana yang sering digunakan dan mengirimkannya melalui compiler yang lebih mengoptimalkan, tetapi lebih lambat.
Kasus penggunaan kita menarik karena kode untuk memutar gambar akan digunakan sekali, mungkin dua kali. Jadi, dalam sebagian besar kasus, kita tidak akan pernah mendapatkan manfaat dari compiler pengoptimal. Hal ini penting untuk diingat saat melakukan benchmark. Menjalankan modul WebAssembly 10.000 kali dalam 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
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 antara browser dan bahasa minimal. Tepatnya: Standar deviasi JavaScript di semua browser adalah ~400 md, sedangkan standar deviasi semua Modul WebAssembly di semua browser berjarak sekitar 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 ke upaya, jadi saya tidak akan membuat grafik apa pun, tetapi ada beberapa hal yang ingin saya tunjukkan:
AssemblyScript tidak menimbulkan hambatan. 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 di ekosistem TypeScript, seperti prettier dan tslint, kemungkinan 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 sedikit menyimpang 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 memiliki hot path JS dan ingin membuatnya lebih cepat atau lebih konsisten dengan WebAssembly. Seperti biasa dengan pertanyaan performa, jawabannya adalah: Tergantung. Jadi, apa yang kita kirimkan?
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 compiler-nya tidak sematang compiler Rust.
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 performa runtime, Rust memiliki rata-rata yang lebih cepat di seluruh browser 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 terus memantau AssemblyScript di masa mendatang.
Update: Rust
Setelah memublikasikan artikel ini, Nick Fitzgerald
dari tim Rust mengarahkan kami ke buku Rust Wasm yang bagus, yang berisi
bagian tentang mengoptimalkan ukuran file. Mengikuti
petunjuk di sana (terutama mengaktifkan pengoptimalan waktu penautan dan
penanganan panik), memungkinkan kami untuk menulis
kode Rust “normal” dan kembali menggunakan
Cargo
(npm
Rust) tanpa membuat ukuran file menjadi besar. Modul Rust berakhir
dengan 370B setelah gzip. 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.