Debugowanie WebAssembly za pomocą nowoczesnych narzędzi

Ingvar Stepanyan
Ingvar Stepanyan

Do tej pory

Rok temu w Chrome ogłosiliśmy początkową obsługę natywnego debugowania WebAssembly w narzędziach deweloperskich Chrome.

Omówiliśmy podstawowe możliwości obsługi kroków i omówiliśmy możliwości, jakie otwiera przed nami korzystanie z informacji DWARF zamiast map źródłowych:

  • Rozwiązywanie nazw zmiennych
  • Typy formatowania stylistycznego
  • Ocenianie wyrażeń w języku źródłowym
  • …i wiele innych.

Dzisiaj z dużą przyjemnością prezentujemy obiecane funkcje w praktyce oraz postępy, jakie w tym roku poczyniły zespoły Emscripten i Chrome DevTools, zwłaszcza w przypadku aplikacji w językach C i C++.

Zanim zaczniemy, pamiętaj, że to wciąż wersja beta nowej funkcji. Musisz używać najnowszej wersji wszystkich narzędzi na własne ryzyko. Jeśli napotkasz jakieś problemy, zgłoś je na stronie https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Zacznijmy od tego samego prostego przykładu w języku C, co 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 skompilowania kodu używamy najnowszej wersji Emscripten i przekazujemy flagę -g, tak jak w pierwotnym poście, aby uwzględnić informacje debugowania:

emcc -g temp.c -o temp.html

Teraz możemy wyświetlić wygenerowaną stronę z serwera HTTP localhost (np. za pomocą polecenia serve) i otworzyć ją w najnowszej wersji Chrome Canary.

Tym razem potrzebujemy też pomocnego rozszerzenia, które zintegruje się z Chrome DevTools i pomoże w zrozumieniu wszystkich informacji debugowania zakodowanych w pliku WebAssembly. Aby zainstalować rozszerzenie, kliknij ten link: goo.gle/wasm-debugging-extension

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

Panel Eksperymenty w ustawieniach Narzędzi deweloperskich

Gdy zamkniesz Ustawienia, Narzędzia deweloperskie zaproponują ponowne załadowanie, aby zastosować ustawienia. Zrób to. To wszystko, jeśli chodzi o jednorazową konfigurację.

Teraz możesz wrócić do panelu Źródła, włączyć opcję Wstrzymaj w przypadku wyjątków (ikona ⏸), a potem zaznaczyć opcję Wstrzymaj w przypadku wykrytych wyjątków i ponownie załadować stronę. Narzędzia deweloperskie powinny zostać wstrzymane przy wyjątku:

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

Domyślnie zatrzymuje się na kodzie łączącym wygenerowanym przez Emscripten, ale po prawej stronie możesz zobaczyć widok zbiór wywołań przedstawiający ścieżkę błędu. Możesz też przejść do oryginalnej linii kodu C, która wywołała tę funkcję:abort

Narzędzia dla programistów wstrzymane w funkcji assert_less i wyświetlające wartości x i y w widoku zakresu

Teraz, jeśli spojrzysz w widoku Zakres, możesz zobaczyć oryginalne nazwy i wartości zmiennych w kodzie C/C++, dzięki czemu nie musisz już domyślać się, co oznaczają zmienione nazwy, takie jak $localN, i jak się one odnoszą do napisanego przez Ciebie kodu źródłowego.

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

Obsługa typu rozszerzonego

Aby to zilustrować, przyjrzyjmy się bardziej skomplikowanemu przykładowi. Tym razem narysujemy fraktal Mandelbrota za pomocą tego 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ć, aplikacja jest nadal dość mała – to pojedynczy plik zawierający 50 wierszy kodu. Tym razem używam też niektórych zewnętrznych interfejsów API, takich jak bibliotek SDL do grafiki oraz liczbami zespolonymi z biblioteki standardowej C++.

Skompiluję go z tą samą flagą -g, aby zawierał informacje debugowania, a także poproszę 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

Gdy otwieram wygenerowaną stronę w przeglądarce, widzę piękny kształt fraktalny w losowych kolorach:

Strona demonstracyjna

Gdy otwieram ponownie Narzędzia deweloperskie, widzę pierwotny plik C++. Tym razem jednak nie mamy błędu w kodzie (uff!), więc zamiast tego ustawmy punkt przerwania na początku kodu.

Gdy ponownie załadujemy stronę, debuger zatrzyma się w źródle kodu C++:

Narzędzia deweloperskie zostały wstrzymane na wywołaniu SDL_Init

Wszystkie zmienne są już widoczne po prawej stronie, ale w tej chwili zainicjowane są tylko zmienne widthheight, więc nie ma zbyt wiele do sprawdzenia.

Ustawmy kolejny punkt przerwania w głównej pętli Mandelbrota i wróćmy do wykonywania kodu, aby pominąć trochę kod.

Narzędzia deweloperskie wstrzymane wewnątrz zagnieżdżonych pętli

W tym momencie nasza tablica palette jest wypełniona losowymi kolorami. Możemy rozwinąć zarówno tablicę, jak i poszczególne struktury SDL_Color, i sprawdzać ich komponenty, aby upewnić się, że wszystko wygląda dobrze (np. czy kanał „alpha” jest zawsze ustawiony na pełną przezroczystość). Podobnie możemy rozwinąć i sprawdzać części rzeczywiste i urojone liczby zespolonej przechowywanej w zmiennej center.

Jeśli chcesz uzyskać dostęp do głęboko zagnieżdżonej usługi, do której trudno się dostać w widoku Zakres, możesz też użyć oceny w Konsoli. Pamiętaj jednak, że bardziej złożone wyrażenia C++ nie są jeszcze obsługiwane.

Panel konsoli z wynikiem funkcji palette[10].r

Przerwij kilkakrotnie wykonywanie programu, aby zobaczyć, jak zmienia się wewnętrzna zmienna x. Możesz to zrobić, ponownie otwierając widok Zakres, dodając nazwę zmiennej do listy obserwowanych zmiennych, oceniając ją w konsoli lub najeżdżając na nią kursorem w źródle kodu:

Informacje kontekstowe zmiennej „x” w źródle, które pokazują jej wartość „3”.

Możemy tu wejść do instrukcji C++ lub je pominąć i obserwować, jak zmieniają się inne zmienne:

Wskaźniki i widok zakresu pokazujące wartości zmiennych „color”, „point” i innych

Wszystko działa świetnie, gdy dostępne są informacje debugowania, ale co, jeśli chcemy debugować kod, który nie został skompilowany z opcjami debugowania?

Debugowanie nieprzetworzonego WebAssembly

Na przykład poprosiliśmy Emscripten o udostępnienie gotowej biblioteki SDL, zamiast samodzielnego kompilowania jej z źródła. Dlatego obecnie debugger nie może znaleźć powiązanych źródeł. Aby dowiedzieć się więcej o SDL_RenderDrawColor, zapoznaj się z tymi informacjami:

Narzędzia deweloperskie wyświetlające widok kodu po deasemblacji pliku „mandelbrot.wasm”

Wracamy do debugowania w czystym środowisku WebAssembly.

Wygląda to trochę przerażająco i nie jest to coś, z czym większość programistów internetowych będzie musiała się kiedykolwiek mierzyć, ale od czasu do czasu może być konieczne debugowanie biblioteki utworzonej bez informacji debugujących – czy to dlatego, że jest to biblioteka zewnętrzna, nad którą nie masz kontroli, czy dlatego, że napotykasz błąd występujący tylko w wersji produkcyjnej.

W takich przypadkach ulepszyliśmy też podstawowe funkcje debugowania.

Jeśli wcześniej korzystałeś(-aś) z debugowania w czystym formacie WebAssembly, możesz zauważyć, że cały deasembler jest teraz wyświetlany w jednym pliku – nie musisz już zgadywać, do której funkcji może pasować wpis wasm-53834e3e/ wasm-53834e3e-7 w sekcji Źródła.

Nowy schemat generowania nazw

Poprawiliśmy też nazwy w widoku demontażu. Wcześniej były widoczne tylko indeksy liczbowe, a w przypadku funkcji w ogóle nie było nazwy.

Obecnie generujemy nazwy w sposób podobny do innych narzędzi do demontażu, korzystając z wskazówek z sekcji dotyczącej nazw WebAssembly, ścieżek importowania/eksportowania i w końcu, jeśli wszystko inne zawiedzie, generując je na podstawie typu i indeksu elementu, np. $func123. Na powyższym zrzucie ekranu widać, że dzięki temu informacje o wykonaniu kodu i rozbieżność są nieco czytelniejsze.

Gdy nie ma dostępnych informacji o typie, może być trudno sprawdzić wartości inne niż prymitywne. Na przykład wskaźniki będą wyświetlane jako zwykłe liczby całkowite, a nie będzie możliwości sprawdzenia, co jest przechowywane w pamięci.

Sprawdzanie pamięci

Wcześniej, aby wyświetlić poszczególne bajty, można było rozwinąć tylko obiekt pamięci WebAssembly, reprezentowany przez env.memory w widoku Zakres. Takie rozwiązanie sprawdzało się w niektórych prostych scenariuszach, ale nie było szczególnie wygodne w rozwijaniu i nie pozwalało na reinterpretację danych w formatach innych niż wartości bajtów. Aby ułatwić Ci to zadanie, dodaliśmy nową funkcję – inspektor pamięci liniowej.

Jeśli klikniesz env.memory prawym przyciskiem myszy, zobaczysz nową opcję Sprawdź pamięć:

Menu kontekstowe „env.memory” w panelu Zakres, w którym widoczny jest element „Sprawdź pamięć”

Po kliknięciu tego przycisku otworzy się kontroler pamięci, w którym możesz sprawdzić pamięć WebAssembly w widoku szesnastkowym i ASCII, przejść do określonych adresów oraz interpretować dane w różnych formatach:

Panel narzędzia do inspekcji pamięci w Narzędziach deweloperskich, który zawiera widok pamięci w systemie szesnastkowym i ASCII

Zaawansowane scenariusze i ostrzeżenia

Profilowanie kodu WebAssembly

Gdy otworzysz Narzędzia deweloperskie, kod WebAssembly zostanie „zmniejszony” do wersji nieoptymalizowanej, aby umożliwić debugowanie. Ta wersja jest znacznie wolniejsza, co oznacza, że nie możesz polegać na console.time, performance.nowi innych metodach pomiaru szybkości kodu, gdy DevTools są otwarte, ponieważ uzyskane liczby w żaden sposób nie odzwierciedlają rzeczywistej wydajności.

Zamiast tego użyj panelu wydajności w DevTools, który uruchomi kod z pełną prędkością i zapewni szczegółowy podział czasu spędzonego na różnych funkcjach:

Panel profilowania pokazujący różne funkcje Wasm

Możesz też uruchomić aplikację z zamkniętymi narzędziami deweloperskimi i otworzyć je po zakończeniu, aby sprawdzić konsolę.

W przyszłości będziemy ulepszać scenariusze profilowania, ale na razie należy mieć na uwadze ten warunek. Jeśli chcesz dowiedzieć się więcej o scenariuszach warstwowania WebAssembly, zapoznaj się z dokumentacją na temat przepływu kompilacji WebAssembly.

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

Podczas kompilowania w Dockerze, maszynie wirtualnej lub na zdalnym serwerze kompilacji możesz napotkać sytuacje, w których ścieżki do plików źródłowych użytych podczas kompilacji nie będą pasować do ścieżek w Twoim własnym systemie plików, w którym działają narzędzia programistyczne Chrome. W takim przypadku pliki będą widoczne w panelu Źródła, ale nie uda się ich załadować.

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

Jeśli np. projekt na maszynie hosta znajduje się pod ścieżką C:\src\my_project, ale został utworzony w kontenerze Dockera, w którym ta ścieżka jest reprezentowana jako /mnt/c/src/my_project, możesz ją ponownie przypisać podczas debugowania, podając te ścieżki jako prefiksy:

Strona opcji rozszerzenia do debugowania kodu C/C++

Wygrywa pierwszy pasujący prefiks. Jeśli znasz inne debugery C++, ta opcja jest podobna do polecenia set substitute-path w GDB lub ustawienia target.source-map w LLDB.

Debugowanie zoptymalizowanych kompilacji

Podobnie jak w przypadku innych języków, debugowanie działa najlepiej, gdy optymalizacje są wyłączone. Optymalizacje mogą wstawiać funkcje w inne, zmieniać kolejność kodu lub usuwać fragmenty kodu. Wszystko to może dezorientować debuger, a w konsekwencji także Ciebie jako użytkownika.

Jeśli nie przeszkadza Ci ograniczona funkcjonalność debugowania i nadal chcesz debugować zoptymalizowaną wersję, większość optymalizacji będzie działać zgodnie z oczekiwaniami (z wyjątkiem wstawiania funkcji). Pozostałe problemy planujemy rozwiązać w przyszłości, ale na razie zalecamy użycie opcji -fno-inline, aby wyłączyć kompilację z jakąkolwiek optymalizacją na poziomie -O, np.:

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

Oddzielanie danych debugowania

Informacje debugowania zawierają wiele szczegółów dotyczących kodu, zdefiniowanych typów, zmiennych, funkcji, zakresów i miejsc – wszystkiego, co może być przydatne dla debugera. W rezultacie może ona być często większa niż sam kod.

Aby przyspieszyć wczytywanie i kompilowanie modułu WebAssembly, możesz rozdzielić te informacje debugowania na osobny plik WebAssembly. Aby to zrobić w Emscripten, przekaż parametr -gseparate-dwarf=… z wybraną nazwą pliku:

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

W takim przypadku aplikacja główna będzie przechowywać tylko nazwę pliku temp.debug.wasm, a rozszerzenie pomocnicze będzie mogło ją zlokalizować i wczytać po otwarciu DevTools.

W połączeniu z optymalizacjami opisanymi powyżej możesz używać tej funkcji nawet do wysyłania prawie zoptymalizowanych wersji produkcyjnych aplikacji, a potem debugować je za pomocą pliku po stronie klienta. W takim przypadku musimy dodatkowo zastąpić zapisany adres URL, aby pomóc rozszerzeniu w znalezieniu pliku pobocznego, na przykład:

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]

To be continued…

To było sporo nowych funkcji.

Dzięki tym wszystkim nowym integracjom Narzędzia deweloperskie w Chrome stają się praktycznym, potężnym debugerem nie tylko dla kodu JavaScript, ale też aplikacji w językach C i C++. Dzięki temu łatwiej niż kiedykolwiek wcześniej można przenosić aplikacje zbudowane przy użyciu różnych technologii do wspólnej, wieloplatformowej sieci.

Nasza podróż jednak jeszcze się nie skończyła. Oto kilka kwestii, nad którymi będziemy pracować w przyszłości:

  • Poprawki w debugowaniu.
  • Dodaliśmy obsługę formaterów niestandardowych typów.
  • Pracujemy nad ulepszeniami profilowania w przypadku aplikacji WebAssembly.
  • Dodawanie obsługi zasięgu kodu, aby ułatwić znajdowanie nieużywanego kodu.
  • Ulepszona obsługa wyrażeń w ocenie konsoli.
  • Dodawanie obsługi kolejnych języków.
  • …i nie tylko

Tymczasem pomóż nam, testując najnowszą wersję beta na swoim kodzie i zgłaszając znalezione problemy na stronie https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Pobieranie kanałów podglądu

Rozważ użycie jako domyślnej przeglądarki deweloperskiej wersji Canary, Dev lub Beta przeglądarki Chrome. Te kanały wersji wstępnej zapewniają dostęp do najnowszych funkcji DevTools, umożliwiają testowanie najnowocześniejszych interfejsów API platformy internetowej i pomagają znaleźć problemy w witrynie, zanim zrobią to użytkownicy.

Kontakt z zespołem Narzędzi deweloperskich w Chrome

Aby omówić nowe funkcje, aktualizacje lub inne kwestie związane z Narzędziami deweloperskimi, skorzystaj z tych opcji.