Szybsze debugowanie WebAssembly

Philip Pfaffe
Kim-Anh Tran
Kim-Anh Tran
Eric Leese
Sam Clegg

Na konferencji Chrome Dev Summit 2020 po raz pierwszy zaprezentowaliśmy w internecie obsługę debugowania w Chrome dla aplikacji WebAssembly. Od tego czasu zespół zainwestował dużo energii w rozwój środowiska programistycznego na potrzeby dużych, a nawet bardzo dużych aplikacji. W tym poście pokażemy, które elementy zostały dodane (lub wprowadzone) w różnych narzędziach, i jak z nich korzystać.

Skalowalne debugowanie

Zacznijmy od tego samego miejsca w poście z 2020 roku. Oto przykład:

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

To nadal niewielki przykład i prawdopodobnie nie spotkasz się z rzeczywistymi problemami, które pojawiłyby się w bardzo dużych aplikacjach. Możemy jednak pokazać Ci, jakie to nowe funkcje. Konfiguracja jest szybka i łatwa, a Ty możesz ją wypróbować.

W poprzednim poście omówiliśmy, jak kompilować i debugować ten przykład. Zróbmy to jeszcze raz, ale przyjrzyjmy się też //performance//:

$ emcc -sUSE_SDL=2 -g -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH

To polecenie tworzy plik binarny Wasm o rozmiarze 3 MB. Większość z nich, jak można by oczekiwać, to informacje na potrzeby debugowania. Możesz to sprawdzić za pomocą narzędzia llvm-objdump [1], na przykład:

$ llvm-objdump -h mandelbrot.wasm

mandelbrot.wasm:        file format wasm

Sections:
Idx Name          Size     VMA      Type
  0 TYPE          0000026f 00000000
  1 IMPORT        00001f03 00000000
  2 FUNCTION      0000043e 00000000
  3 TABLE         00000007 00000000
  4 MEMORY        00000007 00000000
  5 GLOBAL        00000021 00000000
  6 EXPORT        0000014a 00000000
  7 ELEM          00000457 00000000
  8 CODE          0009308a 00000000 TEXT
  9 DATA          0000e4cc 00000000 DATA
 10 name          00007e58 00000000
 11 .debug_info   000bb1c9 00000000
 12 .debug_loc    0009b407 00000000
 13 .debug_ranges 0000ad90 00000000
 14 .debug_abbrev 000136e8 00000000
 15 .debug_line   000bb3ab 00000000
 16 .debug_str    000209bd 00000000

Te dane wyjściowe pokazują wszystkie sekcje znajdujące się w wygenerowanym pliku Wasm. Większość z nich to standardowe sekcje WebAssembly, ale jest też kilka sekcji niestandardowych, których nazwa zaczyna się od .debug_. W tym pliku binarnym znajdują się dane debugowania. Jeśli zsumujemy wszystkie rozmiary, zobaczymy, że dane debugowania stanowią około 2,3 MB z pliku 3 MB. Jeśli użyjemy również polecenia time emcc, zobaczymy, że uruchomienie na naszym komputerze zajęło około 1,5 s. Te liczby stanowią sporą wartość bazową, ale są tak małe, że prawdopodobnie nikt nie zauważy ich. Jednak w prawdziwych aplikacjach plik binarny debugowania może łatwo osiągnąć rozmiar w GB, a kompilacja trwa kilka minut.

Pomijam plik Binaryen

Podczas tworzenia aplikacji Wasm za pomocą narzędzia Emscripten jednym z ostatnich etapów kompilacji jest uruchomienie optymalizatora Binaryen. Binaryen to zestaw narzędzi do kompilacji, który optymalizuje i legalizuje pliki binarne WebAssembly. Uruchomienie Binaryen w ramach kompilacji jest dość kosztowne, ale wymagane tylko pod pewnymi warunkami. W przypadku kompilacji przeznaczonych do debugowania możemy znacznie skrócić czas kompilacji, jeśli unikniemy stosowania kart Binaryen. Najczęściej wymagana czynność Binaryen służy do legalności podpisów funkcji obejmujących 64-bitowe wartości całkowite. Możemy tego uniknąć, włączając integrację WebAssembly BigInt za pomocą interfejsu -sWASM_BIGINT.

$ emcc -sUSE_SDL=2 -g -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK

W trosce o bezpieczeństwo wprowadziliśmy flagę -sERROR_ON_WASM_CHANGES_AFTER_LINK. Pomaga wykryć, kiedy plik Binaryen jest uruchomiony, i nieoczekiwanie przepisywać plik binarny. W ten sposób możemy być na bieżąco.

Mimo że nasz przykład jest stosunkowo mały, i tak możemy zauważyć efekt pomijania Binaryen. Zgodnie z informacjami w usłudze time to polecenie działa poniżej 1 s, czyli o pół sekundy szybciej niż wcześniej.

Sztuczki zaawansowane

Pomijam skanowanie plików wejściowych

Zwykle podczas łączenia z projektem Emscripten usługa emcc skanuje wszystkie biblioteki i pliki wejściowych obiektów. Pozwala to wdrożyć w programie dokładne zależności między funkcjami biblioteki JavaScript a symbolami natywnymi. W przypadku większych projektów to dodatkowe skanowanie plików wejściowych (przy użyciu funkcji llvm-nm) może znacznie wydłużyć czas połączenia.

Możesz zamiast tego użyć polecenia -sREVERSE_DEPS=all, który informuje element emcc o uwzględnieniu wszystkich możliwych natywnych zależności funkcji JavaScript. Wiąże się to z niewielkim narzutem kodu, ale może skrócić czasy połączeń i może być przydatne przy debugowaniu kompilacji.

W przypadku tak małego projektu, jak nasz przykład, nie ma to znaczącej różnicy, ale jeśli masz w projekcie setki, a nawet tysiące plików obiektów, może to znacznie skrócić czasy linku.

Usuwanie sekcji „name”

W dużych projektach, zwłaszcza tych z dużym wykorzystaniem szablonów w C++, sekcja WebAssembly „name” może być bardzo duża. W naszym przykładzie jest to tylko niewielka część całkowitego rozmiaru pliku (zobacz dane wyjściowe funkcji llvm-objdump powyżej), ale w niektórych przypadkach może być bardzo znacząca. Jeśli sekcja „name” aplikacji jest bardzo duża, a dane debugowania DARF są wystarczające do celów debugowania, warto usunąć sekcję „name”:

$ emstrip --no-strip-all --remove-section=name mandelbrot.wasm

Spowoduje to usunięcie sekcji „name” WebAssembly przy jednoczesnym zachowaniu sekcji debugowania DWARF.

Debugowanie rozszczepienia

Pliki binarne z dużą ilością danych debugowania wywierają presję nie tylko na czas kompilacji, ale także na debugowanie. Debuger musi wczytać dane i utworzyć dla nich indeks, by szybko odpowiadać na zapytania, np. „Jakiego typu jest zmienna lokalna x?”.

Funkcja debugowania umożliwia podzielenie danych debugowania na potrzeby pliku binarnego na dwie części: pierwszą, która znajduje się w pliku binarnym, i drugą, która znajduje się w osobnym pliku danych typu DWARF (.dwo). Aby go włączyć, przekaż do Emscripten flagę -gsplit-dwarf:

$ emcc -sUSE_SDL=2 -g -gsplit-dwarf -gdwarf-5 -O0 -o mandelbrot.html mandelbrot.cc  -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK

Poniżej pokazujemy różne polecenia i informacje o tym, jakie pliki są generowane w wyniku kompilacji bez danych debugowania, z danymi debugowania, a na końcu z danymi i danymi debugowania.

jakie są generowane polecenia
i jakie pliki są generowane.

Podczas dzielenia danych DWARF część danych debugowania jest przechowywana razem z plikiem binarnym, a duża część – w pliku mandelbrot.dwo (jak pokazano powyżej).

W przypadku usługi mandelbrot mamy tylko 1 plik źródłowy, ale zwykle projekty są większe i zawierają więcej niż 1 plik. Narzędzie debugowania generuje plik .dwo dla każdej z nich. Aby bieżąca wersja beta debugera (0.1.6.1615) mogła wczytać dane debugowania, musimy je zebrać w tak zwany pakiet DWARF (.dwp):

$ emdwp -e mandelbrot.wasm -o mandelbrot.dwp

połącz pliki DW w pakiecie DWARF

Utworzenie pakietu DWARF z pojedynczych obiektów ma tę zaletę, że wystarczy przesłać 1 dodatkowy plik. Obecnie pracujemy nad wczytaniem wszystkich pojedynczych obiektów w przyszłej wersji.

O co chodzi z DWARF 5?

Jak pewnie zauważyliście, do polecenia emcc powyżej dodaliśmy kolejną flagę: -gdwarf-5. Włączenie wersji 5 symboli DWARF, która obecnie nie jest ustawieniem domyślnym, to kolejna sztuczka, która pomoże nam szybciej rozpocząć debugowanie. Dzięki temu pewne informacje są przechowywane w głównym pliku binarnym, który został pominięty w domyślnej wersji 4. W szczególności możemy określić pełny zbiór plików źródłowych na podstawie głównego pliku binarnego. Dzięki temu debuger może wykonywać podstawowe działania, takie jak wyświetlanie pełnego drzewa źródłowego i ustawianie punktów przerwania bez wczytywania i analizowania danych pełnych symboli. Dzięki temu debugowanie z użyciem symboli podzielonych trwa o wiele szybciej, dlatego zawsze używamy flag wiersza poleceń -gsplit-dwarf i -gdwarf-5 razem.

Format debugowania DWARF5 zapewnia również dostęp do innej przydatnej funkcji. Wprowadza indeks nazw do danych debugowania, które są generowane podczas przekazywania flagi -gpubnames:

$ emcc -sUSE_SDL=2 -g -gdwarf-5 -gsplit-dwarf -gpubnames -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK

W trakcie sesji debugowania wyszukiwanie symboli często odbywa się poprzez wyszukiwanie encji według nazwy, np. podczas szukania zmiennej lub typu. Indeks nazw przyspiesza wyszukiwanie, wskazując bezpośrednio jednostkę kompilacji, która określa tę nazwę. Bez indeksu nazw niezbędne byłoby wyczerpujące przeszukiwanie wszystkich danych debugowania, aby znaleźć odpowiednią jednostkę kompilacji, która określa pożądaną nazwę.

Dla ciekawych: dane debugowania

Aby mieć wgląd w dane DWARF, użyj narzędzia llvm-dwarfdump. Wypróbujmy to:

llvm-dwarfdump mandelbrot.wasm

Tutaj znajdziesz omówienie „jednostek kompilacji” (czyli plików źródłowych), dla których dostępne są informacje o debugowaniu. W tym przykładzie mamy tylko dane debugowania dla: mandelbrot.cc. Informacje ogólne dadzą nam znać, że mamy szkielet jednostki, co oznacza, że mamy niepełne dane w tym pliku i że istnieje oddzielny plik .dwo zawierający pozostałe informacje debugowania:

mandelbrot.wasm i dane debugowania

Możesz też wyświetlić inne tabele w tym pliku, np. w tabeli wierszy, które pokazują mapowanie kodu bajtowego Wasm na wiersze C++ (spróbuj użyć llvm-dwarfdump -debug-line).

Znajdziesz tu też informacje debugowania w osobnym pliku .dwo:

llvm-dwarfdump mandelbrot.dwo

mandelbrot.wasm i dane debugowania

TL;DR: Jaka jest zaleta używania debugowania?

Rozdzielenie informacji na potrzeby debugowania w przypadku dużych aplikacji ma kilka zalet:

  1. Szybsze linki: tag łączący nie musi już analizować wszystkich informacji na potrzeby debugowania. Łączniki zwykle analizują wszystkie dane DWARF zawarte w pliku binarnym. Dzięki wydzieleniu dużych części informacji o debugowaniu do osobnych plików tagi łączące obsługują mniejsze pliki binarne, co pozwala skrócić czas łączenia (zwłaszcza w przypadku dużych aplikacji).

  2. Szybsze debugowanie: w przypadku niektórych wyszukiwań symboli debuger może pominąć analizę dodatkowych symboli w plikach .dwo/.dwp. W przypadku niektórych wyszukiwań (np. żądań dotyczących mapowania wierszy plików Wasm-na-C++) nie musimy analizować dodatkowych danych debugowania. Dzięki temu oszczędzamy czas i nie musimy wczytywać i analizować dodatkowych danych debugowania.

1. Jeśli używasz emsdk i nie masz w systemie najnowszej wersji llvm-objdump, znajdziesz ją w katalogu emsdk/upstream/bin.

Pobierz kanały podglądu

Zastanów się, czy nie ustawić Chrome w wersji Canary, Dev lub beta jako domyślnej przeglądarki do programowania. Te kanały wersji testowej dają dostęp do najnowszych funkcji Narzędzi deweloperskich, umożliwiają testowanie najnowocześniejszych interfejsów API platformy internetowej i wykrywanie problemów w witrynie, zanim użytkownicy ją zobaczą.

Kontakt z zespołem ds. Narzędzi deweloperskich w Chrome

Użyj poniższych opcji, aby porozmawiać o nowych funkcjach i zmianach w poście lub o innych kwestiach związanych z Narzędziami deweloperskimi.

  • Prześlij nam sugestię lub opinię na crbug.com.
  • Aby zgłosić problem z Narzędziami deweloperskimi, kliknij Więcej opcji   Więcej > Pomoc > Zgłoś problemy z Narzędziami deweloperskimi w Narzędziach deweloperskich.
  • Opublikuj tweeta na stronie @ChromeDevTools.
  • Napisz komentarz pod filmem dotyczącym nowości w Narzędziach deweloperskich w Narzędziach deweloperskich w YouTube lub filmach w YouTube ze wskazówkami dotyczącymi Narzędzi deweloperskich.