Men-debug WebAssembly dengan alat modern

Ingvar Stepanyan
Ingvar Stepanyan

Jalan sejauh ini

Setahun yang lalu, Chrome mengumumkan dukungan awal untuk proses debug WebAssembly native di Chrome DevTools.

Kami telah menunjukkan dukungan langkah dasar dan membahas peluang penggunaan informasi DWARF, bukan peta sumber yang terbuka untuk kita di masa mendatang:

  • Me-resolve nama variabel
  • Jenis pencetakan yang rapi
  • Mengevaluasi ekspresi dalam bahasa sumber
  • …dan banyak lagi.

Hari ini, kami dengan senang hati menampilkan fitur yang dijanjikan yang telah diwujudkan dan progres yang telah dibuat oleh tim Emscripten dan Chrome DevTools selama tahun ini, khususnya, untuk aplikasi C dan C++.

Sebelum memulai, perlu diingat bahwa ini masih merupakan versi beta dari pengalaman baru. Anda harus menggunakan versi terbaru dari semua alat dengan risiko Anda sendiri, dan jika Anda mengalami masalah, laporkan ke https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Mari kita mulai dengan contoh C sederhana yang sama seperti sebelumnya:

#include <stdlib.h>

void assert_less(int x, int y) {
  if (x >= y) {
    abort();
  }
}

int main() {
  assert_less(10, 20);
  assert_less(30, 20);
}

Untuk mengompilasi, kita menggunakan Emscripten terbaru dan meneruskan flag -g, seperti dalam postingan asli, untuk menyertakan informasi debug:

emcc -g temp.c -o temp.html

Sekarang kita dapat menayangkan halaman yang dihasilkan dari server HTTP localhost (misalnya, dengan serve), dan membukanya di Chrome Canary terbaru.

Kali ini, kita juga memerlukan ekstensi helper yang terintegrasi dengan Chrome DevTools dan membantunya memahami semua informasi proses debug yang dienkode dalam file WebAssembly. Instal ekstensi ini dengan membuka link ini: goo.gle/wasm-debugging-extension

Anda juga dapat mengaktifkan proses debug WebAssembly di Eksperimen DevTools. Buka Chrome DevTools, klik ikon roda gigi () di sudut kanan atas panel DevTools, buka panel Eksperimen, lalu centang WebAssembly Debugging: Enable DWARF support.

Panel eksperimen di setelan DevTools

Saat Anda menutup Settings, DevTools akan menyarankan untuk memuat ulang itself untuk menerapkan setelan, jadi mari kita lakukan hal itu. Itu saja untuk pengaturan satu kali saja.

Sekarang kita dapat kembali ke panel Sumber, mengaktifkan Jeda pada pengecualian (ikon ⏸), lalu mencentang Jeda pada pengecualian yang tertangkap dan memuat ulang halaman. Anda akan melihat DevTools dijeda pada pengecualian:

Screenshot panel Sumber yang menunjukkan cara mengaktifkan &#39;Jeda pada pengecualian yang tertangkap&#39;

Secara default, kode ini berhenti pada kode glue yang dihasilkan Emscripten, tetapi di sebelah kanan, Anda dapat melihat tampilan Call Stack yang mewakili stacktrace error, dan dapat membuka baris C asli yang memanggil abort:

DevTools dijeda di fungsi `assert_less` dan menampilkan nilai `x` dan `y` di tampilan Cakupan

Pada tampilan Cakupan, Anda dapat melihat nama asli dan nilai variabel dalam kode C/C++, serta tidak lagi harus mencari tahu arti nama yang rusak seperti $localN dan kaitannya dengan kode sumber yang telah Anda tulis.

Hal ini tidak hanya berlaku untuk nilai primitif seperti bilangan bulat, tetapi juga untuk jenis gabungan seperti struktur, class, array, dll.

Dukungan jenis multimedia

Mari kita lihat contoh yang lebih rumit untuk menunjukkannya. Kali ini, kita akan menggambar fraktal Mandelbrot dengan kode C++ berikut:

#include <SDL2/SDL.h>
#include <complex>

int main() {
  // Init SDL.
  int width = 600, height = 600;
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* window;
  SDL_Renderer* renderer;
  SDL_CreateWindowAndRenderer(width, height, SDL_WINDOW_OPENGL, &window,
                              &renderer);

  // Generate a palette with random colors.
  enum { MAX_ITER_COUNT = 256 };
  SDL_Color palette[MAX_ITER_COUNT];
  srand(time(0));
  for (int i = 0; i < MAX_ITER_COUNT; ++i) {
    palette[i] = {
        .r = (uint8_t)rand(),
        .g = (uint8_t)rand(),
        .b = (uint8_t)rand(),
        .a = 255,
    };
  }

  // Calculate and draw the Mandelbrot set.
  std::complex<double> center(0.5, 0.5);
  double scale = 4.0;
  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      std::complex<double> point((double)x / width, (double)y / height);
      std::complex<double> c = (point - center) * scale;
      std::complex<double> z(0, 0);
      int i = 0;
      for (; i < MAX_ITER_COUNT - 1; i++) {
        z = z * z + c;
        if (abs(z) > 2.0)
          break;
      }
      SDL_Color color = palette[i];
      SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
      SDL_RenderDrawPoint(renderer, x, y);
    }
  }

  // Render everything we've drawn to the canvas.
  SDL_RenderPresent(renderer);

  // SDL_Quit();
}

Anda dapat melihat bahwa aplikasi ini masih cukup kecil-satu file berisi 50 baris kode-tetapi kali ini saya juga menggunakan beberapa API eksternal, seperti library SDL untuk grafis serta angka kompleks dari library standar C++.

Saya akan mengompilasi dengan flag -g yang sama seperti di atas untuk menyertakan informasi debug, dan juga saya akan meminta Emscripten untuk menyediakan library SDL2 dan mengizinkan memori berukuran arbitrer:

emcc -g mandelbrot.cc -o mandelbrot.html \
     -s USE_SDL=2 \
     -s ALLOW_MEMORY_GROWTH=1

Saat mengunjungi halaman yang dihasilkan di browser, saya dapat melihat bentuk fraktal yang indah dengan beberapa warna acak:

Halaman demo

Saat membuka DevTools, sekali lagi, saya dapat melihat file C++ asli. Namun, kali ini kita tidak memiliki error dalam kode (wow!), jadi mari kita tetapkan beberapa titik henti sementara di awal kode.

Saat kita memuat ulang halaman lagi, debugger akan dijeda tepat di dalam sumber C++ kita:

DevTools dijeda pada panggilan `SDL_Init`

Kita sudah dapat melihat semua variabel di sebelah kanan, tetapi saat ini hanya width dan height yang diinisialisasi, sehingga tidak banyak yang perlu diperiksa.

Mari kita tetapkan titik henti sementara lain di dalam loop Mandelbrot utama, dan lanjutkan eksekusi untuk melompat sedikit ke depan.

DevTools dijeda di dalam loop bertingkat

Pada tahap ini, palette telah diisi dengan beberapa warna acak, dan kita dapat memperluas array itu sendiri, serta setiap struktur SDL_Color dan memeriksa komponennya untuk memverifikasi bahwa semuanya terlihat bagus (misalnya, saluran "alpha" selalu ditetapkan ke opasitas penuh). Demikian pula, kita dapat memperluas dan memeriksa bagian riil dan imajiner dari bilangan kompleks yang disimpan dalam variabel center.

Jika ingin mengakses properti bertingkat yang sulit diakses melalui tampilan Cakupan, Anda juga dapat menggunakan evaluasi Konsol. Namun, perhatikan bahwa ekspresi C++ yang lebih kompleks belum didukung.

Panel konsol yang menampilkan hasil `palette[10].r`

Mari kita lanjutkan eksekusi beberapa kali dan kita dapat melihat bagaimana x bagian dalam juga berubah dengan melihat di tampilan Cakupan lagi, menambahkan nama variabel ke daftar pantauan, mengevaluasinya di konsol, atau dengan mengarahkan kursor ke variabel dalam kode sumber:

Tooltip di atas variabel `x` di sumber yang menampilkan nilainya `3`

Dari sini, kita dapat menjalankan pernyataan C++ secara bertahap, dan mengamati bagaimana variabel lain juga berubah:

Tampilan Tooltip dan Cakupan yang menampilkan nilai `color`, `point`, dan variabel lainnya

Baik, jadi semua ini berfungsi dengan baik jika informasi debug tersedia, tetapi bagaimana jika kita ingin men-debug kode yang tidak di-build dengan opsi proses debug?

Proses debug WebAssembly mentah

Misalnya, kami meminta Emscripten untuk menyediakan library SDL bawaan bagi kami, bukan mengompilasi sendiri dari sumber, sehingga-setidaknya saat ini-debugger tidak dapat menemukan sumber terkait. Mari kita masuk lagi untuk masuk ke SDL_RenderDrawColor:

DevTools menampilkan tampilan disassembly `mandelbrot.wasm`

Kita kembali ke pengalaman proses debug WebAssembly mentah.

Sekarang, hal ini terlihat agak menakutkan dan bukan sesuatu yang perlu ditangani oleh sebagian besar developer Web, tetapi terkadang Anda mungkin ingin men-debug library yang dibuat tanpa informasi debug, baik karena library tersebut adalah library pihak ketiga yang tidak dapat Anda kontrol, atau karena Anda menemukan salah satu bug yang hanya terjadi pada produksi.

Untuk membantu dalam kasus tersebut, kami juga telah melakukan beberapa peningkatan pada pengalaman proses debug dasar.

Pertama-tama, jika sebelumnya Anda menggunakan proses debug WebAssembly mentah, Anda mungkin melihat bahwa seluruh proses disassembly kini ditampilkan dalam satu file. Tidak perlu lagi menebak fungsi mana yang mungkin sesuai dengan entri Sumber wasm-53834e3e/ wasm-53834e3e-7.

Skema pembuatan nama baru

Kami juga memperbaiki nama di tampilan pembongkaran. Sebelumnya, Anda hanya akan melihat indeks numerik, atau, dalam kasus fungsi, tidak ada nama sama sekali.

Sekarang kita membuat nama yang mirip dengan alat disassembly lainnya, dengan menggunakan petunjuk dari bagian nama WebAssembly, jalur impor/ekspor, dan terakhir, jika semuanya gagal, membuat nama berdasarkan jenis dan indeks item seperti $func123. Anda dapat melihat bagaimana, dalam screenshot di atas, hal ini sudah membantu mendapatkan stacktrace dan disassembly yang sedikit lebih mudah dibaca.

Jika tidak ada informasi jenis yang tersedia, mungkin sulit untuk memeriksa nilai apa pun selain primitif-misalnya, pointer akan muncul sebagai bilangan bulat biasa, tanpa cara untuk mengetahui apa yang disimpan di belakangnya dalam memori.

Pemeriksaan memori

Sebelumnya, Anda hanya dapat meluaskan objek memori WebAssembly, yang diwakili oleh env.memory di tampilan Cakupan untuk mencari setiap byte. Hal ini berfungsi dalam beberapa skenario sederhana, tetapi tidak sangat mudah untuk diperluas dan tidak memungkinkan untuk menafsirkan ulang data dalam format selain nilai byte. Kami juga telah menambahkan fitur baru untuk membantu hal ini: inspector memori linear.

Jika mengklik kanan env.memory, Anda akan melihat opsi baru yang disebut Periksa memori:

Menu konteks pada `env.memory` di panel Cakupan yang menampilkan item &#39;Periksa Memori&#39;

Setelah diklik, Memory Inspector akan muncul, tempat Anda dapat memeriksa memori WebAssembly dalam tampilan heksadesimal dan ASCII, membuka alamat tertentu, serta menafsirkan data dalam format yang berbeda:

Panel Pemeriksa Memori di DevTools menampilkan tampilan heksadesimal dan ASCII memori

Skenario dan peringatan lanjutan

Membuat profil kode WebAssembly

Saat Anda membuka DevTools, kode WebAssembly akan "dikelompokkan ke bawah" ke versi yang tidak dioptimalkan untuk mengaktifkan proses debug. Versi ini jauh lebih lambat, yang berarti Anda tidak dapat mengandalkan console.time, performance.now, dan metode lain untuk mengukur kecepatan kode saat DevTools terbuka, karena angka yang Anda dapatkan tidak akan mewakili performa di dunia nyata sama sekali.

Sebagai gantinya, Anda harus menggunakan panel Performa DevTools yang akan menjalankan kode dengan kecepatan penuh dan memberi Anda perincian mendetail tentang waktu yang dihabiskan dalam berbagai fungsi:

Panel pembuatan profil yang menampilkan berbagai fungsi Wasm

Atau, Anda dapat menjalankan aplikasi dengan DevTools tertutup, dan membukanya setelah selesai untuk memeriksa Konsol.

Kami akan meningkatkan kualitas skenario pembuatan profil pada masa mendatang, tetapi untuk saat ini, hal ini merupakan peringatan yang harus diperhatikan. Jika Anda ingin mempelajari lebih lanjut skenario tingkatan WebAssembly, lihat dokumen kami tentang pipeline kompilasi WebAssembly.

Membangun dan men-debug di berbagai komputer (termasuk Docker / host)

Saat mem-build di Docker, virtual machine, atau di server build jarak jauh, Anda mungkin akan mengalami situasi saat jalur ke file sumber yang digunakan selama build tidak cocok dengan jalur di sistem file Anda sendiri tempat Chrome DevTools berjalan. Dalam hal ini, file akan muncul di panel Sources tetapi gagal dimuat.

Untuk memperbaiki masalah ini, kami telah menerapkan fungsi pemetaan jalur di opsi ekstensi C/C++. Anda dapat menggunakannya untuk memetakan ulang jalur arbitrer dan membantu DevTools menemukan sumber.

Misalnya, jika project di mesin host Anda berada di jalur C:\src\my_project, tetapi di-build di dalam container Docker tempat jalur tersebut diwakili sebagai /mnt/c/src/my_project, Anda dapat memetakan ulang jalur tersebut selama proses debug dengan menentukan jalur tersebut sebagai awalan:

Halaman opsi ekstensi proses debug C/C++

Awalan pertama yang cocok akan "menang". Jika Anda sudah terbiasa dengan debug C++ lainnya, opsi ini mirip dengan perintah set substitute-path pada GDB atau setelan target.source-map dalam LLDB.

Men-debug build yang dioptimalkan

Seperti bahasa lainnya, proses debug berfungsi paling baik jika pengoptimalan dinonaktifkan. Pengoptimalan dapat menyelaraskan fungsi satu sama lain, mengurutkan ulang kode, atau menghapus bagian kode sepenuhnya-dan semua ini memiliki peluang untuk membingungkan debugger dan, akibatnya, Anda sebagai pengguna.

Jika Anda tidak keberatan dengan pengalaman proses debug yang lebih terbatas dan masih ingin men-debug build yang dioptimalkan, sebagian besar pengoptimalan akan berfungsi seperti yang diharapkan, kecuali untuk penggabungan fungsi. Kami berencana mengatasi masalah yang tersisa di masa mendatang. Namun, untuk saat ini, gunakan -fno-inline untuk menonaktifkannya saat melakukan kompilasi dengan pengoptimalan level -O, misalnya:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline

Memisahkan informasi debug

Informasi debug menyimpan banyak detail tentang kode Anda, jenis, variabel, fungsi, cakupan, dan lokasi yang ditentukan-apa pun yang mungkin berguna bagi debugger. Akibatnya, sering kali bisa lebih besar dari kode itu sendiri.

Untuk mempercepat pemuatan dan kompilasi modul WebAssembly, sebaiknya pisahkan informasi debug ini ke dalam file WebAssembly terpisah. Untuk melakukannya di Emscripten, teruskan tanda -gseparate-dwarf=… dengan nama file yang diinginkan:

emcc -g temp.c -o temp.html \
     -gseparate-dwarf=temp.debug.wasm

Dalam hal ini, aplikasi utama hanya akan menyimpan nama file temp.debug.wasm, dan ekstensi helper akan dapat menemukan dan memuatnya saat Anda membuka DevTools.

Jika digabungkan dengan pengoptimalan seperti yang dijelaskan di atas, fitur ini bahkan dapat digunakan untuk mengirimkan build produksi yang hampir dioptimalkan dari aplikasi Anda, lalu men-debugnya dengan file sisi lokal. Dalam hal ini, kita juga harus mengganti URL yang disimpan untuk membantu ekstensi menemukan file samping, misalnya:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline \
     -gseparate-dwarf=temp.debug.wasm \
     -s SEPARATE_DWARF_URL=file://[local path to temp.debug.wasm]

Lanjutkan…

Wah, banyak sekali fitur barunya!

Dengan semua integrasi baru tersebut, Chrome DevTools menjadi debugger yang efektif dan canggih, tidak hanya untuk JavaScript, tetapi juga untuk aplikasi C dan C++, sehingga memudahkan Anda untuk mengambil aplikasi yang dibuat dalam berbagai teknologi dan membawanya ke Web lintas platform bersama.

Namun, perjalanan kita belum berakhir. Beberapa hal yang akan kami kerjakan mulai sekarang:

  • Membersihkan bagian tepi yang kasar dalam pengalaman proses debug.
  • Menambahkan dukungan untuk pemformat jenis kustom.
  • Meningkatkan pembuatan profil untuk aplikasi WebAssembly.
  • Menambahkan dukungan untuk cakupan kode agar lebih mudah menemukan kode yang tidak digunakan.
  • Meningkatkan dukungan untuk ekspresi dalam evaluasi konsol.
  • Menambahkan dukungan untuk lebih banyak bahasa.
  • …dan lainnya!

Sementara itu, bantu kami dengan mencoba versi beta saat ini pada kode Anda sendiri dan laporkan masalah apa pun yang ditemukan ke https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Mendownload saluran pratinjau

Sebaiknya gunakan Chrome Canary, Dev, atau Beta sebagai browser pengembangan default Anda. Saluran pratinjau ini memberi Anda akses ke fitur DevTools terbaru, memungkinkan Anda menguji API platform web canggih, dan membantu Anda menemukan masalah di situs sebelum pengguna melakukannya.

Hubungi tim Chrome DevTools

Gunakan opsi berikut untuk membahas fitur baru, update, atau apa pun yang berkaitan dengan DevTools.