Mengganti hot path di JavaScript aplikasi Anda dengan WebAssembly

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
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. 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

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 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?

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 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.