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