Debugowanie WebAssembly za pomocą nowoczesnych narzędzi

Ingvar Stepanyan
Ingvar Stepanyan

Dotychczasowa droga

Rok temu Chrome ogłosił wstępną obsługę natywnego debugowania WebAssembly w Narzędziach deweloperskich w Chrome.

Zademonstrowaliśmy podstawowe wsparcie i rozmawialiśmy o możliwościach, jakie daje nam dostęp do danych DWARF zamiast map źródeł w przyszłości:

  • Rozpoznawanie nazw zmiennych
  • Rodzaje drukowania
  • Ocena wyrażeń w językach źródłowych
  • ...i wiele więcej.

Z przyjemnością przedstawiamy nowe funkcje oraz postępy, jakie w tym roku poczyniły zespoły Emscripten i Chrome DevTools, szczególnie dotyczące aplikacji w językach C i C++.

Zanim zaczniemy, pamiętaj, że jest to wciąż wersja beta nowej usługi. Na własne ryzyko musisz korzystać z najnowszych wersji wszystkich narzędzi. Jeśli napotkasz jakieś problemy, zgłoś je na stronie https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue.

Zaczniemy od tego samego prostego przykładu C, który był używany ostatnio:

#include <stdlib.h>

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

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

Do jego skompilowania używamy latest Emscripten i przekazujemy flagę -g, tak jak w oryginalnym poście, aby uwzględnić informacje na potrzeby debugowania:

emcc -g temp.c -o temp.html

Teraz możemy wyświetlić wygenerowaną stronę z serwera HTTP lokalnego hosta (na przykład z funkcją serve) i otworzyć ją w najnowszej wersji Chrome Canary.

Tym razem będziemy potrzebować też rozszerzenia pomocniczego, które integruje się z Narzędziami deweloperskimi w Chrome i pomaga zrozumieć wszystkie informacje na potrzeby debugowania zakodowane w pliku WebAssembly. Zainstaluj ją, klikając ten link: goo.gle/wasm-debugging-extension

Warto też włączyć debugowanie WebAssembly w sekcji Eksperymenty w narzędziach deweloperskich. Otwórz Narzędzia deweloperskie w Chrome, kliknij ikonę koła zębatego () w prawym górnym rogu panelu Narzędzi deweloperskich, przejdź do panelu Eksperymenty i zaznacz WebAssembly Debugging: Włącz obsługę DWARF.

Okienko Eksperymenty w ustawieniach Narzędzi deweloperskich

Gdy zamkniesz Ustawienia, Narzędzia deweloperskie zasugerują ponowne załadowanie ustawień, żeby zastosować ustawienia. To wszystko, jeśli chodzi o jednorazową konfigurację.

Teraz możemy wrócić do panelu Źródła, włączyć opcję Wstrzymaj przy wyjątkach (ikona ⏸), a następnie zaznaczyć opcję Wstrzymaj przy zarejestrowanych wyjątkach i ponownie załadować stronę. Narzędzia deweloperskie powinny być wstrzymane z powodu wyjątku:

Zrzut ekranu pokazujący panel Źródła pokazujący, jak włączyć opcję „Wstrzymaj przy wykrytych wyjątkach”

Domyślnie zatrzymuje się on na kodzie klejowym wygenerowanym przez Emscripten, ale po prawej stronie zobaczysz widok stosu wywołań, który przedstawia zrzut stosu błędu, i możesz przejść do oryginalnego wiersza C, które wywołało abort:

Narzędzia deweloperskie zostały wstrzymane w funkcji „assert_less” i wyświetlają wartości „x” i „y” w widoku zakresu

Gdy spojrzysz na widok Zakres, zobaczysz pierwotne nazwy i wartości zmiennych w kodzie C/C++ i nie musisz już rozumieć, co oznaczają zniekształcone nazwy, np. $localN, i jak mają się one do napisanego przez Ciebie kodu źródłowego.

Dotyczy to nie tylko wartości podstawowych, takich jak liczby całkowite, ale także typów złożonych, takich jak struktury, klasy, tablice itp.

Obsługa tekstu sformatowanego

Spójrzmy na bardziej skomplikowany przykład. Tym razem narysujemy fraktal Mandelbrota za pomocą następującego kodu C++:

#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();
}

Jak widać, ta aplikacja jest w dalszym ciągu niewielka, ma tylko 1 plik zawierający 50 wierszy kodu, ale tym razem używam też zewnętrznych interfejsów API, takich jak biblioteka SDL do obsługi grafiki oraz złożone liczby ze standardowej biblioteki C++.

Skompiluję ją z tą samą flagą -g co powyżej, aby uwzględnić informacje o debugowaniu. Poproszę też Emscripten o udostępnienie biblioteki SDL2 i zezwolenie na pamięć o dowolnym rozmiarze:

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

Po otwarciu wygenerowanej strony w przeglądarce widać piękny kształt fraktalny w losowych kolorach:

Strona demonstracyjna

Po otwarciu Narzędzi deweloperskich widzę oryginalny plik C++. Tym razem jednak nie ma błędu w kodzie (uff!), więc zamiast tego ustawmy punkt przerwania na początku kodu.

Po ponownym załadowaniu strony debuger zatrzyma się bezpośrednio w źródle C++:

Narzędzia deweloperskie zostały wstrzymane podczas wywołania `SDL_Init`

Po prawej stronie widać już wszystkie zmienne, ale inicjowane są obecnie tylko width i height, więc nie ma co dokładnie sprawdzić.

Ustawmy kolejny punkt przerwania w głównej pętli Mandelbrota i wznów wykonanie, aby przewinąć do przodu.

Narzędzia deweloperskie zostały wstrzymane w zagnieżdżonych pętli

Na tym etapie obiekt palette został wypełniony losowymi kolorami. Możemy rozszerzyć zarówno samą tablicę, jak i poszczególne struktury SDL_Color, a także sprawdzić ich komponenty, aby sprawdzić, czy wszystko wygląda dobrze (np. kanał „alfa” ma zawsze pełną przezroczystość). W ten sam sposób możemy rozwijać i sprawdzać rzeczywiste i wymyślone części liczby zespolonej zapisane w zmiennej center.

Jeśli chcesz uzyskać dostęp do głęboko zagnieżdżonej właściwości, do której trudno jest przejść w widoku Zakres, możesz też skorzystać z oceny w konsoli. Pamiętaj jednak, że bardziej złożone wyrażenia C++ nie są jeszcze obsługiwane.

Panel konsoli przedstawiający wynik polecenia „palette[10].r”

Powtórzmy kilka razy, aby zobaczyć, jak zmienia się wewnętrzny x. Spójrz jeszcze raz na widok Zakres, dodajmy nazwę zmiennej do listy obserwacyjnej, oceniaj ją w konsoli lub najeżdżając kursorem na zmienną w kodzie źródłowym:

Etykietka nad zmienną „x” w źródle pokazującą jej wartość „3”

Tutaj możemy dodawać instrukcje C++ i obserwować, jak zmieniają się inne zmienne:

Etykietki i widok zakresu z wartościami „kolor”, „punkt” i innych zmiennych

Wszystko działa dobrze, gdy dostępne są informacje na temat debugowania, ale co jeśli chcemy debugować kod, który nie został utworzony przy użyciu opcji debugowania?

Debugowanie nieprzetworzonego WebAssembly

Poprosiliśmy np. Emscripten o udostępnienie gotowej biblioteki SDL zamiast kompilowania jej samodzielnie na podstawie źródła, więc debuger nie ma obecnie możliwości znalezienia powiązanych źródeł. Spróbujmy jeszcze raz, aby przejść do SDL_RenderDrawColor:

Narzędzia deweloperskie z widokiem demontażu pliku „mandelbrot.wasm”

Wracamy do surowego procesu debugowania WebAssembly.

Wydaje się to nieco przerażające i większość programistów aplikacji internetowych nie będzie musiała się z tym martwić. Czasami jednak warto debugować bibliotekę stworzoną bez informacji o debugowaniu – może to być spowodowane tym, że jest to biblioteka zewnętrzna, nad którą nie masz kontroli, lub dlatego, że napotkasz jeden z tych błędów, które występują tylko w wersji produkcyjnej.

Aby im to ułatwić, wprowadziliśmy też pewne ulepszenia w podstawowym interfejsie debugowania.

Jeśli wcześniej zdarzyło Ci się używać surowego debugowania WebAssembly, możesz zauważyć, że cały rozkład jest teraz wyświetlany w jednym pliku i nie musisz już zgadywać, której funkcji odpowiada wpis wasm-53834e3e/ wasm-53834e3e-7 w polu Źródła.

Nowy schemat generowania nazw

Poprawiliśmy nazwy w widoku rozkładu. Wcześniej widoczne były tylko indeksy liczbowe, a w przypadku funkcji – w ogóle ich nazwa.

Teraz generujemy nazwy podobnie jak inne narzędzia do demontażu, korzystając ze wskazówek z sekcji nazwy WebAssembly oraz ścieżek importu/eksportu, a jeśli wszystko zawiedzie, generujemy je na podstawie typu i indeksu elementu, np. $func123. Na zrzucie ekranu powyżej widać, jak to pomaga uzyskać nieco bardziej czytelne zrzuty stosu i demontaż.

Gdy nie ma informacji o typie, może być trudno sprawdzić wartości oprócz podstawowych. Na przykład wskaźniki będą wyświetlane jako zwykłe liczby całkowite i nie wiadomo, co jest za nimi przechowywane w pamięci.

Inspekcja pamięci

Wcześniej można było tylko rozwinąć obiekt pamięci WebAssembly reprezentowany przez env.memory w widoku Zakres, aby wyszukać poszczególne bajty. Było to rozwiązanie w kilku prostych scenariuszach, ale jego rozwinięcie nie było szczególnie wygodne i nie umożliwiło reinterpretacji danych w formatach innych niż wartości w bajtach. Dodaliśmy też nową funkcję, która ma pomóc w tej kwestii: liniowy inspektor pamięci.

Po kliknięciu env.memory prawym przyciskiem myszy pojawi się nowa opcja Sprawdź pamięć:

Menu kontekstowe w obszarze „env.memory” w panelu Zakres z elementem „Zbadaj pamięć”

Kliknięcie go spowoduje wyświetlenie Inspektora pamięci, w którym możesz sprawdzić pamięć WebAssembly w widokach szesnastkowych i ASCII, przechodzić do konkretnych adresów oraz interpretować dane w różnych formatach:

Panel Inspektora pamięci w Narzędziach deweloperskich z widokami szesnastkowymi i ASCII pamięci

Zaawansowane scenariusze i ostrzeżenia

Profilowanie kodu WebAssembly

Po otwarciu Narzędzi deweloperskich kod WebAssembly przechodzi na wersję niezoptymalizowaną, co umożliwia debugowanie. Ta wersja działa znacznie wolniej, co oznacza, że nie możesz polegać na metodach console.time, performance.now ani innych metodach pomiaru szybkości kodu, gdy Narzędzia deweloperskie są otwarte, ponieważ widoczne wartości w ogóle nie odzwierciedlają rzeczywistej wydajności.

Zamiast tego korzystaj z panelu wydajności Narzędzi deweloperskich, który uruchamia kod z pełną prędkością i zawiera szczegółowe informacje o czasie spędzonym na korzystaniu z różnych funkcji:

Panel profilowania przedstawiający różne funkcje Wasm

Możesz też uruchomić aplikację po zamknięciu Narzędzi deweloperskich, a potem otworzyć ją, aby przejrzeć konsolę.

W przyszłości będziemy ulepszać scenariusze profilowania, ale na razie trzeba o tym pamiętać. Więcej informacji o scenariuszach tworzenia poziomów WebAssembly znajdziesz w dokumentacji potoku kompilacji WebAssembly.

Kompilowanie i debugowanie na różnych maszynach (w tym na Dockerze / hoście)

Podczas tworzenia Dockera, maszyny wirtualnej lub zdalnego serwera kompilacji często zdarzają się sytuacje, w których ścieżki do plików źródłowych używanych podczas kompilacji nie odpowiadają ścieżek w Twoim własnym systemie plików, w którym działają Narzędzia deweloperskie w Chrome. W takim przypadku pliki pojawią się w panelu Źródła, ale nie będą się wczytywać.

Aby rozwiązać ten problem, zaimplementowaliśmy funkcję mapowania ścieżek w opcjach rozszerzenia C/C++. Możesz go użyć do zmapowania dowolnych ścieżek i pomóc Narzędziom deweloperskim w zlokalizowaniu źródeł.

Jeśli na przykład projekt na hoście maszyny znajduje się pod ścieżką C:\src\my_project, ale został utworzony wewnątrz kontenera Dockera, w którym ta ścieżka jest reprezentowana jako /mnt/c/src/my_project, możesz ją ponownie zmapować podczas debugowania, podając te ścieżki jako prefiksy:

Strona opcji rozszerzenia do debugowania C/C++

Pierwszy dopasowany prefiks „wygrywa”. Jeśli znasz inne debugery C++, ta opcja jest podobna do polecenia set substitute-path w GDB lub do ustawienia target.source-map w LLDB.

Debugowanie kompilacji zoptymalizowanych

Tak jak w przypadku innych języków, debugowanie działa najlepiej, gdy optymalizacje są wyłączone. Optymalizacje mogą się łączyć ze sobą nawzajem, zmieniać kolejność kodów lub całkowicie usuwać fragmenty kodu. Może to powodować dezorientację debugera, a przez to także użytkownika.

Jeśli nie przeszkadza Ci bardziej ograniczone debugowanie i chcesz przeprowadzić debugowanie zoptymalizowanej kompilacji, większość optymalizacji będzie działać zgodnie z oczekiwaniami, z wyjątkiem wbudowania funkcji. W przyszłości zamierzamy rozwiązać pozostałe problemy, ale na razie wyłącz ją podczas kompilacji z optymalizacjami na poziomie -O, np.:-fno-inline

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

Rozdzielanie danych debugowania

Informacje o debugowaniu to szczegółowe dane o kodzie, zdefiniowanych typach, zmiennych, funkcjach, zakresach i lokalizacjach – wszystko, co może być przydatne dla debugera. Dlatego często może być większy niż sam kod.

Aby przyspieszyć wczytywanie i kompilację modułu WebAssembly, możesz podzielić te informacje debugowania do osobnego pliku WebAssembly. Aby to zrobić w Emscripten, przekaż flagę -gseparate-dwarf=… z odpowiednią nazwą pliku:

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

W tym przypadku główna aplikacja przechowuje tylko plik o nazwie temp.debug.wasm, a rozszerzenie pomocnicze będzie mogło go zlokalizować i załadować po otwarciu Narzędzi deweloperskich.

W połączeniu z optymalizacjami takimi jak opisana powyżej funkcja ta może nawet służyć do wysyłania niemal zoptymalizowanych kompilacji produkcyjnych aplikacji, a później debugowania ich za pomocą pliku po stronie lokalnej. W takim przypadku musimy dodatkowo zastąpić zapisany adres URL, aby rozszerzenie mogło znaleźć plik boczny, np.:

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]

Kontynuacja...

To było bardzo dużo nowych funkcji!

Dzięki tym nowym integracjom Narzędzia deweloperskie w Chrome stają się skutecznym narzędziem do debugowania nie tylko w przypadku JavaScriptu, ale również aplikacji w języku C i C++. Dzięki temu korzystanie z aplikacji utworzonych na podstawie różnorodnych technologii i udostępnianie ich we wspólnej witrynie internetowej jest łatwiejsze niż kiedykolwiek wcześniej.

Jednak nasza podróż to jeszcze nie koniec. Oto kilka rzeczy, nad którymi od tej pory będziemy pracować:

  • Wyczyścić ostre krawędzie w interfejsie debugowania.
  • Dodano obsługę formatów niestandardowych.
  • Pracujemy nad ulepszeniem profilowania dla aplikacji WebAssembly.
  • Dodanie obsługi zasięgu kodu w celu ułatwienia znajdowania nieużywanego kodu.
  • Ulepszono obsługę wyrażeń w ocenie konsoli.
  • Dodano obsługę kolejnych języków.
  • …i nie tylko

W tym czasie wypróbuj wersję beta własnego kodu i zgłoś wszelkie znalezione problemy na stronie https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue.

Pobieranie kanałów podglądu

Jako domyślnej przeglądarki dla programistów możesz używać Chrome Canary, Dev lub Beta. Te kanały podglądu dają dostęp do najnowszych funkcji Narzędzi deweloperskich, umożliwiają testowanie najnowocześniejszych interfejsów API platform internetowych oraz wykrywanie problemów w witrynie, zanim zdołają zrobić użytkownicy.

Kontakt z zespołem Narzędzi deweloperskich w Chrome

Użyj poniższych opcji, aby omówić nowe funkcje i zmiany w poście lub wszelkie inne kwestie związane z Narzędziami dla deweloperów.

  • Prześlij nam sugestię lub opinię na stronie crbug.com.
  • Aby zgłosić problem z Narzędziami deweloperskimi, kliknij Więcej opcji   Więcej   > Pomoc > Zgłoś problemy z Narzędziami deweloperskimi.
  • zatweetuj na @ChromeDevTools.
  • Napisz komentarz o nowościach w filmach w YouTube dostępnych w Narzędziach deweloperskich lub z poradami dotyczącymi narzędzi dla deweloperów w filmach w YouTube.