Menggunakan API web asinkron dari WebAssembly

I/O API di web bersifat asinkron, namun sinkron di sebagian besar bahasa sistem. Saat mengompilasi kode ke WebAssembly, Anda harus menjembatani satu jenis API ke jenis API lainnya—dan penghubung ini adalah Asyncify. Dalam postingan ini, Anda akan mempelajari kapan dan cara menggunakan Asinkron dan cara kerjanya di balik layar.

I/O dalam bahasa sistem

Saya akan mulai dengan contoh sederhana di C. Misalnya, Anda ingin membaca nama pengguna dari sebuah file, dan sapa mereka dengan pesan "Halo, (nama pengguna)!":

#include <stdio.h>

int main() {
    FILE *stream = fopen("name.txt", "r");
    char name[20+1];
    size_t len = fread(&name, 1, 20, stream);
    name[len] = '\0';
    fclose(stream);
    printf("Hello, %s!\n", name);
    return 0;
}

Meskipun contoh ini tidak memiliki banyak manfaat, contoh ini sudah menunjukkan sesuatu yang akan Anda temukan dalam aplikasi dari berbagai ukuran: contoh ini membaca beberapa input dari dunia eksternal, memprosesnya secara internal, dan menulis output ke dunia eksternal. Semua interaksi dengan dunia luar terjadi melalui beberapa fungsi yang biasa disebut fungsi input-output, yang juga disingkat menjadi I/O.

Untuk membaca nama dari C, Anda memerlukan setidaknya dua panggilan I/O penting: fopen, untuk membuka file, dan fread untuk membaca data dari file tersebut. Setelah mengambil data, Anda dapat menggunakan fungsi I/O printf lainnya untuk mencetak hasilnya ke konsol.

Fungsi tersebut terlihat cukup sederhana pada awalnya dan Anda tidak perlu berpikir dua kali tentang mesin yang diperlukan untuk membaca atau menulis data. Namun, bergantung pada lingkungannya, mungkin ada banyak hal yang terjadi di dalamnya:

  • Jika file input berada di drive lokal, aplikasi perlu melakukan serangkaian akses memori dan disk untuk menemukan file tersebut, memeriksa izin, membukanya untuk dibaca, lalu membaca blok demi blok hingga jumlah byte yang diminta diambil. Proses ini bisa sangat lambat, tergantung pada kecepatan {i>disk<i} dan ukuran yang diminta.
  • Atau, file input mungkin berada di lokasi jaringan yang terpasang, yang dalam hal ini, stack jaringan sekarang juga akan terlibat, sehingga meningkatkan kompleksitas, latensi, dan jumlah percobaan ulang potensial untuk setiap operasi.
  • Terakhir, bahkan printf tidak dijamin akan mencetak sesuatu ke konsol dan mungkin dialihkan ke file atau lokasi jaringan, sehingga harus melalui langkah-langkah yang sama di atas.

Singkatnya, I/O bisa berjalan lambat dan Anda tidak dapat memprediksi berapa lama waktu yang diperlukan untuk panggilan tertentu secara sekilas pada kodenya. Saat operasi tersebut berjalan, seluruh aplikasi Anda akan tampak berhenti berfungsi dan tidak responsif bagi pengguna.

Ini tidak terbatas pada C atau C++. Sebagian besar bahasa sistem menampilkan semua I/O dalam bentuk API sinkron. Misalnya, jika Anda menerjemahkan contoh ke Rust, API mungkin terlihat lebih sederhana, tetapi prinsip yang sama berlaku. Anda cukup melakukan panggilan dan menunggu secara sinkron hingga menampilkan hasilnya, selagi melakukan semua operasi yang mahal, dan pada akhirnya menampilkan hasilnya dalam satu pemanggilan:

fn main() {
    let s = std::fs::read_to_string("name.txt");
    println!("Hello, {}!", s);
}

Namun, apa yang terjadi jika Anda mencoba mengompilasi salah satu contoh tersebut ke WebAssembly dan menerjemahkannya ke web? Atau, untuk memberikan contoh spesifik, apa yang bisa diterjemahkan oleh operasi "pembacaan file"? Aplikasi tersebut perlu membaca data dari beberapa penyimpanan.

Model web asinkron

Web memiliki berbagai opsi penyimpanan yang dapat Anda petakan, seperti penyimpanan dalam memori (objek JS), localStorage, IndexedDB, penyimpanan sisi server, dan File System Access API baru.

Namun, hanya dua dari API tersebut—penyimpanan dalam memori dan localStorage—yang dapat digunakan secara sinkron, dan keduanya merupakan opsi yang paling membatasi terkait apa yang dapat Anda simpan dan durasinya. Semua opsi lainnya hanya menyediakan API asinkron.

Ini adalah salah satu properti inti dari mengeksekusi kode di web: setiap operasi yang memakan waktu, termasuk semua I/O, harus bersifat asinkron.

Alasannya karena web secara historis merupakan thread tunggal, dan setiap kode pengguna yang menyentuh UI harus dijalankan di thread yang sama dengan UI. Aplikasi harus bersaing dengan tugas penting lainnya seperti tata letak, rendering, dan penanganan peristiwa untuk waktu CPU. Anda tentu tidak ingin bagian JavaScript atau WebAssembly dapat memulai operasi "pembacaan file" dan memblokir hal lainnya—seluruh tab, atau, di masa lalu, seluruh browser—selama rentang dari milidetik hingga beberapa detik, hingga selesai.

Sebaliknya, kode hanya diperbolehkan untuk menjadwalkan operasi I/O bersama dengan callback yang akan dieksekusi setelah selesai. Callback tersebut dijalankan sebagai bagian dari loop peristiwa browser. Saya tidak akan membahas detailnya di sini, tetapi jika Anda tertarik untuk mempelajari cara kerja loop peristiwa di balik layar, lihat Tugas, tugas mikro, antrean, dan jadwal yang menjelaskan topik ini secara mendalam.

Versi singkatnya adalah browser menjalankan semua bagian kode secara berurutan tanpa henti, dengan mengambilnya dari antrean satu per satu. Saat peristiwa tertentu dipicu, browser akan mengantrekan pengendali yang sesuai, dan pada iterasi loop berikutnya, peristiwa tersebut akan dikeluarkan dari antrean dan dieksekusi. Mekanisme ini memungkinkan simulasi konkurensi dan menjalankan banyak operasi paralel hanya dengan menggunakan satu thread.

Hal penting yang perlu diingat tentang mekanisme ini adalah, meskipun kode JavaScript (atau WebAssembly) kustom Anda dieksekusi, loop peristiwa diblokir dan, meskipun memang demikian, tidak ada cara untuk bereaksi terhadap pengendali eksternal, peristiwa, I/O, dll. Satu-satunya cara untuk mendapatkan kembali hasil I/O adalah dengan mendaftarkan callback, menyelesaikan eksekusi kode, dan memberikan kontrol kembali ke browser agar dapat terus memproses setiap tugas yang tertunda. Setelah I/O selesai, pengendali Anda akan menjadi salah satu tugas tersebut dan akan dieksekusi.

Misalnya, jika Anda ingin menulis ulang contoh di atas dalam JavaScript modern dan memutuskan untuk membaca nama dari URL jarak jauh, Anda akan menggunakan Fetch API dan sintaksis async-await:

async function main() {
  let response = await fetch("name.txt");
  let name = await response.text();
  console.log("Hello, %s!", name);
}

Meskipun terlihat sinkron, pada dasarnya setiap await adalah sugar sintaksis untuk callback:

function main() {
  return fetch("name.txt")
    .then(response => response.text())
    .then(name => console.log("Hello, %s!", name));
}

Dalam contoh de-sugaring ini, yang sedikit lebih jelas, permintaan dimulai dan respons berlangganan dengan callback pertama. Setelah menerima respons awal—hanya header HTTP—browser akan memanggil callback ini secara asinkron. Callback mulai membaca isi sebagai teks menggunakan response.text(), dan berlangganan hasilnya dengan callback lain. Terakhir, setelah mengambil semua konten, fetch akan memanggil callback terakhir, yang mencetak "Hello, (username)!" ke konsol.

Berkat sifat asinkron dari langkah-langkah tersebut, fungsi asalnya dapat mengembalikan kontrol ke browser segera setelah I/O dijadwalkan, dan membuat seluruh UI tetap responsif serta tersedia untuk tugas lain, termasuk rendering, scroll, dan sebagainya, saat I/O dijalankan di latar belakang.

Sebagai contoh terakhir, bahkan API sederhana seperti "sleep", yang membuat aplikasi menunggu selama beberapa detik, juga merupakan bentuk operasi I/O:

#include <stdio.h>
#include <unistd.h>
// ...
printf("A\n");
sleep(1);
printf("B\n");

Tentu, Anda dapat menerjemahkannya dengan cara yang sangat mudah yang akan memblokir thread saat ini hingga waktunya berakhir:

console.log("A");
for (let start = Date.now(); Date.now() - start < 1000;);
console.log("B");

Itulah yang sebenarnya dilakukan Emscripten dalam implementasi default "sleep", tetapi hal ini sangat tidak efisien, karena akan memblokir seluruh UI dan sementara itu tidak akan mengizinkan peristiwa lain ditangani. Umumnya, jangan lakukan itu dalam kode produksi.

Sebagai gantinya, versi "sleep" yang lebih idiomatis di JavaScript akan melibatkan pemanggilan setTimeout(), dan berlangganan dengan pengendali:

console.log("A");
setTimeout(() => {
    console.log("B");
}, 1000);

Apa yang umum untuk semua contoh dan API ini? Dalam setiap kasus, kode idiomatis dalam bahasa sistem asli menggunakan API pemblokiran untuk I/O, sedangkan contoh yang setara untuk web menggunakan API asinkron. Saat mengompilasi ke web, Anda perlu mengubah antara kedua model eksekusi tersebut, dan WebAssembly belum memiliki kemampuan bawaan untuk melakukannya.

Menjembatani kesenjangan dengan Asinkron

Di sinilah Asyncify berperan. Asyncify adalah fitur waktu kompilasi yang didukung oleh Emscripten yang memungkinkan jeda seluruh program dan melanjutkannya secara asinkron nanti.

Grafik panggilan
yang menjelaskan JavaScript -> WebAssembly -> web API -> pemanggilan tugas asinkron, tempat Asyncify menghubungkan kembali hasil tugas asinkron ke WebAssembly

Penggunaan pada C / C++ dengan Emscripten

Jika ingin menggunakan Asyncify untuk menerapkan tidur asinkron pada contoh terakhir, Anda dapat melakukannya seperti ini:

#include <stdio.h>
#include <emscripten.h>

EM_JS(void, async_sleep, (int seconds), {
    Asyncify.handleSleep(wakeUp => {
        setTimeout(wakeUp, seconds * 1000);
    });
});
…
puts("A");
async_sleep(1);
puts("B");

EM_JS adalah makro yang memungkinkan penentuan cuplikan JavaScript seolah-olah cuplikan tersebut adalah fungsi C. Di dalamnya, gunakan fungsi Asyncify.handleSleep() yang memberi tahu Emscripten untuk menangguhkan program dan menyediakan pengendali wakeUp() yang harus dipanggil setelah operasi asinkron selesai. Pada contoh di atas, pengendali diteruskan ke setTimeout(), tetapi dapat digunakan dalam konteks lain yang menerima callback. Terakhir, Anda dapat memanggil async_sleep() di mana pun yang Anda inginkan, seperti sleep() biasa atau API sinkron lainnya.

Saat mengompilasi kode tersebut, Anda harus memberi tahu Emscripten untuk mengaktifkan fitur Asyncify. Lakukan hal tersebut dengan meneruskan -s ASYNCIFY serta -s ASYNCIFY_IMPORTS=[func1, func2] dengan daftar fungsi seperti array yang mungkin bersifat asinkron.

emcc -O2 \
    -s ASYNCIFY \
    -s ASYNCIFY_IMPORTS=[async_sleep] \
    ...

Hal ini memberi tahu Emscripten bahwa setiap panggilan ke fungsi tersebut mungkin memerlukan penyimpanan dan pemulihan status, sehingga compiler akan memasukkan kode pendukung pada panggilan tersebut.

Sekarang, saat mengeksekusi kode ini di browser, Anda akan melihat log output yang lancar seperti yang Anda harapkan, dengan B muncul setelah penundaan singkat setelah A.

A
B

Anda juga dapat menampilkan nilai dari fungsi Asyncify. Yang perlu Anda lakukan adalah menampilkan hasil handleSleep(), dan meneruskan hasilnya ke callback wakeUp(). Misalnya, jika ingin mengambil nomor dari resource jarak jauh, alih-alih membaca dari file, Anda dapat menggunakan cuplikan seperti di bawah ini untuk mengajukan permintaan, menangguhkan kode C, dan melanjutkan setelah isi respons diambil—semuanya dilakukan dengan lancar seolah-olah panggilan sinkron.

EM_JS(int, get_answer, (), {
     return Asyncify.handleSleep(wakeUp => {
        fetch("answer.txt")
            .then(response => response.text())
            .then(text => wakeUp(Number(text)));
    });
});
puts("Getting answer...");
int answer = get_answer();
printf("Answer is %d\n", answer);

Bahkan, untuk API berbasis Promise seperti fetch(), Anda bahkan dapat menggabungkan Asyncify dengan fitur async-await JavaScript, bukan menggunakan API berbasis callback. Untuk itu, panggil Asyncify.handleAsync(), bukan Asyncify.handleSleep(). Kemudian, daripada harus menjadwalkan callback wakeUp(), Anda dapat meneruskan fungsi JavaScript async dan menggunakan await dan return di dalamnya, membuat kode terlihat lebih alami dan sinkron, tanpa kehilangan manfaat I/O asinkron.

EM_JS(int, get_answer, (), {
     return Asyncify.handleAsync(async () => {
        let response = await fetch("answer.txt");
        let text = await response.text();
        return Number(text);
    });
});

int answer = get_answer();

Menunggu nilai kompleks

Namun, contoh ini masih membatasi Anda hanya pada angka. Bagaimana jika Anda ingin menerapkan contoh asli, ketika saya mencoba mendapatkan nama pengguna dari file sebagai string? Kamu juga bisa melakukannya!

Emscripten menyediakan fitur bernama Embind yang memungkinkan Anda menangani konversi antara nilai JavaScript dan C++. Metode ini juga memiliki dukungan untuk Asyncify, sehingga Anda dapat memanggil await() pada Promise eksternal dan akan bertindak seperti await dalam kode JavaScript async-await:

val fetch = val::global("fetch");
val response = fetch(std::string("answer.txt")).await();
val text = response.call<val>("text").await();
auto answer = text.as<std::string>();

Saat menggunakan metode ini, Anda bahkan tidak perlu meneruskan ASYNCIFY_IMPORTS sebagai flag kompilasi, karena sudah disertakan secara default.

Oke, jadi semuanya bekerja dengan baik di Emscripten. Bagaimana dengan toolchain dan bahasa lain?

Penggunaan dari bahasa lain

Misalnya, Anda memiliki panggilan sinkron yang serupa di suatu tempat dalam kode Rust yang ingin dipetakan ke API asinkron di web. Ternyata, kamu juga bisa melakukannya!

Pertama, Anda harus menentukan fungsi seperti impor reguler melalui blok extern (atau sintaksis bahasa pilihan Anda untuk fungsi asing).

extern {
    fn get_answer() -> i32;
}

println!("Getting answer...");
let answer = get_answer();
println!("Answer is {}", answer);

Dan kompilasi kode Anda ke WebAssembly:

cargo build --target wasm32-unknown-unknown

Sekarang Anda perlu melengkapi file WebAssembly dengan kode untuk menyimpan/memulihkan stack. Untuk C/C++, Emscripten akan melakukannya untuk kita, tetapi tidak digunakan di sini sehingga prosesnya sedikit lebih manual.

Untungnya, transformasi Asyncify sendiri sepenuhnya agnostik toolchain. API ini dapat mengubah file WebAssembly arbitrer, terlepas dari compiler yang dihasilkan. Transformasi disediakan secara terpisah sebagai bagian dari pengoptimal wasm-opt dari Binaryen toolchain dan dapat dipanggil seperti ini:

wasm-opt -O2 --asyncify \
      --pass-arg=asyncify-imports@env.get_answer \
      [...]

Teruskan --asyncify untuk mengaktifkan transformasi, lalu gunakan --pass-arg=… untuk menyediakan daftar fungsi asinkron yang dipisahkan koma, tempat status program harus ditangguhkan dan kemudian dilanjutkan.

Langkah selanjutnya adalah menyediakan kode runtime pendukung yang benar-benar akan melakukannya—menangguhkan dan melanjutkan kode WebAssembly. Sekali lagi, dalam kasus C / C++, hal ini akan disertakan oleh Emscripten, tetapi sekarang Anda memerlukan kode glue JavaScript kustom yang akan menangani file WebAssembly arbitrer. Kita telah membuat {i>library <i}hanya untuk itu.

Anda dapat menemukannya di GitHub di https://github.com/GoogleChromeLabs/asyncify atau npm dengan nama asyncify-wasm.

Library ini menyimulasikan API pembuatan instance WebAssembly standar, tetapi dengan namespace-nya sendiri. Satu-satunya perbedaan adalah, pada WebAssembly API biasa, Anda hanya dapat menyediakan fungsi sinkron sebagai impor, sedangkan dengan wrapper Asyncify, Anda juga dapat menyediakan impor asinkron:

const { instance } = await Asyncify.instantiateStreaming(fetch('app.wasm'), {
    env: {
        async get_answer() {
            let response = await fetch("answer.txt");
            let text = await response.text();
            return Number(text);
        }
    }
});
…
await instance.exports.main();

Setelah Anda mencoba memanggil fungsi asinkron tersebut - seperti get_answer() dalam contoh di atas - dari sisi WebAssembly, library akan mendeteksi Promise yang ditampilkan, menangguhkan dan menyimpan status aplikasi WebAssembly, berlangganan ke penyelesaian promise, dan kemudian, setelah di-resolve, pulihkan stack dan status panggilan dengan lancar dan melanjutkan eksekusi seolah-olah tidak ada yang terjadi.

Karena fungsi apa pun dalam modul dapat melakukan panggilan asinkron, semua ekspor juga akan berpotensi asinkron, sehingga ekspor juga akan digabungkan. Dalam contoh di atas, Anda mungkin telah melihat bahwa Anda perlu await hasil instance.exports.main() untuk mengetahui kapan eksekusi benar-benar selesai.

Bagaimana semua ini bekerja di balik layar?

Saat mendeteksi panggilan ke salah satu fungsi ASYNCIFY_IMPORTS, Asyncify akan memulai operasi asinkron, menyimpan seluruh status aplikasi, termasuk stack panggilan dan lokasi sementara apa pun, dan kemudian, setelah operasi tersebut selesai, memulihkan semua stack memori dan panggilan serta dilanjutkan dari tempat yang sama serta dengan status yang sama seolah-olah program tidak pernah berhenti.

Fitur ini sangat mirip dengan fitur async-await di JavaScript yang saya tampilkan sebelumnya, tetapi tidak seperti fitur JavaScript, fitur ini tidak memerlukan dukungan sintaksis atau runtime khusus dari bahasa tersebut, dan berfungsi dengan mengubah fungsi sinkron biasa pada waktu kompilasi.

Saat mengompilasi contoh tidur asinkron yang ditampilkan sebelumnya:

puts("A");
async_sleep(1);
puts("B");

Asyncify mengambil kode ini dan mengubahnya menjadi kurang lebih seperti kode berikut (kode pseudo, transformasi nyata lebih terlibat daripada ini):

if (mode == NORMAL_EXECUTION) {
    puts("A");
    async_sleep(1);
    saveLocals();
    mode = UNWINDING;
    return;
}
if (mode == REWINDING) {
    restoreLocals();
    mode = NORMAL_EXECUTION;
}
puts("B");

Awalnya, mode disetel ke NORMAL_EXECUTION. Sejalan dengan itu, saat pertama kali kode yang ditransformasi tersebut dieksekusi, hanya bagian yang mengarah ke async_sleep() yang akan dievaluasi. Segera setelah operasi asinkron dijadwalkan, Asyncify akan menyimpan semua pengguna lokal, dan melepaskan tumpukan dengan kembali dari setiap fungsi hingga ke atas. Dengan cara ini, kontrol akan kembali ke loop peristiwa browser.

Kemudian, setelah async_sleep() di-resolve, kode dukungan Asyncify akan mengubah mode menjadi REWINDING, dan memanggil fungsi tersebut lagi. Kali ini, cabang "eksekusi normal" dilewati - karena sudah melakukan tugas terakhir kali dan saya ingin menghindari pencetakan "A" dua kali - dan langsung menuju ke cabang "rewinding". Setelah tercapai, kode ini akan memulihkan semua lokal yang tersimpan, mengubah mode kembali ke "normal" dan melanjutkan eksekusi seolah-olah kode tidak pernah dihentikan sejak awal.

Biaya transformasi

Sayangnya, transformasi Asyncify tidak sepenuhnya gratis karena harus memasukkan cukup banyak kode pendukung untuk menyimpan dan memulihkan semua lokal tersebut, menavigasi stack panggilan dalam mode yang berbeda, dan sebagainya. Library ini akan mencoba mengubah hanya fungsi yang ditandai sebagai asinkron pada command line, serta salah satu pemanggil potensialnya, tetapi overhead ukuran kode mungkin masih bertambah hingga sekitar 50% sebelum kompresi.

Grafik yang menunjukkan overhead ukuran
kode untuk berbagai tolok ukur, dari mendekati 0% dalam kondisi yang disesuaikan hingga lebih dari 100% dalam kasus
terburuk

Hal ini tidak ideal, tetapi dalam banyak kasus dapat diterima jika alternatif tidak memiliki fungsi sama sekali atau harus membuat penulisan ulang yang signifikan pada kode aslinya.

Pastikan untuk selalu mengaktifkan pengoptimalan untuk build akhir agar tidak menjadi lebih tinggi. Anda juga dapat memeriksa opsi pengoptimalan khusus Asyncify untuk mengurangi overhead dengan membatasi transformasi hanya ke fungsi yang ditentukan dan/atau hanya panggilan fungsi langsung. Performa runtime juga akan sangat sedikit, tetapi terbatas pada panggilan asinkron itu sendiri. Namun, dibandingkan dengan biaya pekerjaan yang sebenarnya, biasanya dapat diabaikan.

Demo di dunia nyata

Setelah Anda melihat contoh sederhana, saya akan beralih ke skenario yang lebih rumit.

Seperti yang disebutkan di awal artikel, salah satu opsi penyimpanan di web adalah File System Access API asinkron. Protokol ini menyediakan akses ke sistem file {i>host<i} sesungguhnya dari aplikasi web.

Di sisi lain, ada standar de-facto yang disebut WASI untuk I/O WebAssembly di konsol dan sisi server. Objek ini dirancang sebagai target kompilasi untuk bahasa sistem, dan mengekspos semua jenis sistem file dan operasi lain dalam bentuk sinkron tradisional.

Bagaimana jika Anda dapat memetakan satu ke yang lain? Selanjutnya, Anda dapat mengompilasi aplikasi dalam bahasa sumber apa pun dengan toolchain yang mendukung target WASI, dan menjalankannya di sandbox di web, sambil tetap mengizinkannya untuk beroperasi pada file pengguna yang sebenarnya. Dengan Asyncify, Anda dapat melakukannya.

Dalam demo ini, saya telah mengompilasi coreutils Rust dengan beberapa patch kecil ke WASI, diteruskan melalui transformasi Asyncify, dan menerapkan binding asinkron dari WASI ke File System Access API pada sisi JavaScript. Setelah dikombinasikan dengan komponen terminal Xterm.js, kode ini akan menyediakan shell realistis yang berjalan di tab browser dan beroperasi pada file pengguna sungguhan - seperti terminal yang sebenarnya.

Lihat secara langsung di https://wasi.rreverser.com/.

Kasus penggunaan Asyncify tidak terbatas hanya pada timer dan sistem file. Anda dapat melangkah lebih jauh dan menggunakan API yang lebih khusus di web.

Misalnya, dengan bantuan Asyncify juga, Anda dapat memetakan libusb—yang mungkin merupakan library native yang paling populer untuk digunakan dengan perangkat USB—ke WebUSB API, yang memberikan akses asinkron ke perangkat tersebut di web. Setelah dipetakan dan dikompilasi, saya mendapatkan pengujian dan contoh libusb standar untuk dijalankan pada perangkat yang dipilih langsung di sandbox halaman web.

Screenshot output debug
libusb di halaman web, yang menampilkan informasi tentang kamera Canon yang terhubung

Ini mungkin sebuah cerita untuk postingan blog lainnya.

Contoh-contoh tersebut menunjukkan betapa andalnya Asyncify dalam menjembatani kesenjangan dan mem-porting semua jenis aplikasi ke web, sehingga Anda dapat memperoleh akses lintas platform, sandboxing, dan keamanan yang lebih baik, semuanya tanpa kehilangan fungsi.