Na konferencji Chrome Dev Summit 2020 po raz pierwszy zaprezentowaliśmy obsługę debugowania aplikacji WebAssembly w internecie. Od tego czasu zespół włożył dużo pracy w udoskonalanie narzędzi programistycznych w przypadku dużych, a nawet ogromnych zastosowań. W tym poście pokażemy, które z nich dodaliśmy (lub wzięliśmy do nich pracę), a także o tym, jak z nich korzystać.
Skalowalne debugowanie
Zacznijmy od tego samego miejsca w poście z 2020 roku. Oto przykład, który omawialiśmy wtedy:
#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 wprawdzie niewielki przykład i prawdopodobnie nie zauważysz żadnych rzeczywistych problemów, które pojawiłyby się w bardzo dużej aplikacji. Mimo to możemy zaprezentować nowe funkcje. Konfiguracja jest prosta i łatwa do skonfigurowania, a także można ją wypróbować samodzielnie.
W poprzednim poście omówiliśmy, jak kompilować i debugować ten przykład. Zróbmy to jeszcze raz, ale zerknijmy też na parametr //performance//:
$ emcc -sUSE_SDL=2 -g -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH
To polecenie generuje plik binarny wasm o rozmiarze 3 MB. Większość z nich, jak można się spodziewać, to informacje o debugowaniu. 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 w wygenerowanym pliku Wasm – większość z nich to standardowe sekcje WebAssembly, ale jest też kilka sekcji niestandardowych, których nazwy zaczynają się od .debug_
. To tam plik binarny zawiera dane debugowania. Po zsumowaniu wszystkich rozmiarów okazuje się, że dane debugowania zajmują około 2,3 MB z 3 MB. Jeśli dodatkowo użyjemy polecenia emcc
w wersji time
, zobaczymy, że uruchomienie na komputerze trwało około 1,5 s. Te liczby to fajna baza, ale są tak małe, że nikt by się ich nie zauważył. W prawdziwych aplikacjach plik binarny debugowania może jednak z łatwością osiągnąć rozmiar w GB, a jego utworzenie zajmuje kilka minut.
Pomijam plik binarny
Jednym z ostatnich etapów tworzenia aplikacji Wasm za pomocą Emscripten jest uruchomienie optymalizatora Binaryen. Binaryen to zestaw narzędzi do kompilacji, który zarówno optymalizuje, jak i legalizuje pliki binarne WebAssembly. Uruchomienie Binaryen w ramach kompilacji jest dość kosztowne, ale jest wymagane tylko pod pewnymi warunkami. W przypadku kompilacji do debugowania możemy znacznie skrócić czas kompilacji, jeśli unikniemy konieczności korzystania z karnetów Binaryen. Najczęściej wymagana jest przepustka Binaryen do legalizacji podpisów funkcji obejmujących 64-bitowe wartości całkowite. Możemy tego uniknąć, włączając integrację WebAssembly BigInt za pomocą -sWASM_BIGINT
.
$ emcc -sUSE_SDL=2 -g -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK
Dodaliśmy flagę -sERROR_ON_WASM_CHANGES_AFTER_LINK
na wszelki wypadek. Pomaga wykryć, czy plik Binaryen jest uruchomiony, i nieoczekiwanie przepisywać plik binarny. W ten sposób możemy mieć pewność, że nie spieszymy się.
Mimo że nasz przykład jest dość mały, nadal możemy zaobserwować efekty pominięcia Binaryen! Według time
to polecenie działa nieco poniżej 1 s, czyli o pół sekundy szybciej niż wcześniej.
Zaawansowane ulepszenia
Pomijam skanowanie plików wejściowych
Standardowo podczas łączenia projektu Emscripten emcc
skanuje wszystkie pliki i biblioteki obiektów wejściowych. Ma to na celu zaimplementowanie precyzyjnych zależności między funkcjami biblioteki JavaScript a symbolami natywnymi w programie. W przypadku większych projektów to dodatkowe skanowanie plików wejściowych (za pomocą llvm-nm
) może znacznie wydłużyć czas łączenia.
Możesz też użyć polecenia -sREVERSE_DEPS=all
, które informuje emcc
, że ma uwzględniać wszystkie możliwe natywne zależności funkcji JavaScript. Wiąże się to z niewielkim narzutem rozmiaru kodu, ale może skrócić czas potrzebny na łączenie i przydaje się podczas debugowania kompilacji.
W przypadku tak małego projektu jak w naszym przykładzie nie ma to znaczenia, ale jeśli masz w projekcie setki, a nawet tysiące plików obiektów, może to znacznie skrócić czas trwania linków.
Z usunięcia sekcji „name”
W dużych projektach, zwłaszcza takich, w których często jest używany szablon C++, sekcja „name” WebAssembly 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 duża. Jeśli sekcja „name” aplikacji jest bardzo duża, a dodatkowe informacje na temat debugowania wystarczą, aby zaspokoić potrzeby debugowania, korzystne może być usunięcie sekcji „name”:
$ emstrip --no-strip-all --remove-section=name mandelbrot.wasm
Spowoduje to usunięcie sekcji „name” WebAssembly przy 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. „Jaki jest typ zmiennej lokalnej x?”.
Funkcja Debugowanie rozszczepienia umożliwia podzielenie informacji o debugowaniu pliku binarnego na 2 części: jedną, która pozostaje w pliku binarnym, i drugą znajdującą się w osobnym pliku DWARF (.dwo
). Aby go włączyć, przekaż flagę -gsplit-dwarf
do Emscripten:
$ 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 znajdziesz różne polecenia oraz pliki generowane przez kompilację bez danych debugowania, dane debugowania oraz zarówno dane debugowania, jak i rozbicie danych na potrzeby debugowania.
Podczas podziału danych DWARF oprócz pliku binarnego znajduje się część danych debugowania, 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 zazwyczaj projekty są większe i zawierają więcej niż 1 plik. Debugowanie rozszczepienia generuje plik .dwo
dla każdego z nich. Aby debuger w bieżącej wersji beta (0.1.6.1615) mógł załadować te podzielone dane debugowania, musimy je spakować w tak zwany pakiet DWARF (.dwp
):
$ emdwp -e mandelbrot.wasm -o mandelbrot.dwp
Stworzenie pakietu DWARF z pojedynczych obiektów ma tę zaletę, że wystarczy udostępnić tylko 1 dodatkowy plik. Obecnie pracujemy nad załadowaniem wszystkich pojedynczych obiektów w kolejnej wersji.
Co to jest DWARF 5?
Jak już pewnie widzisz, w poleceniu emcc
powyżej (-gdwarf-5
) umieściliśmy kolejną flagę. Włączenie funkcji DWARF w wersji 5, która obecnie nie jest ustawieniem domyślnym, to kolejna sztuczka, która pomaga nam szybciej rozpocząć debugowanie. W takim przypadku w głównym pliku binarnym są przechowywane pewne informacje, które zostały pominięte przez domyślną wersję 4. W szczególności możemy określić pełny zestaw plików źródłowych tylko 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 pełnych danych symboli. Dzięki temu debugowanie z użyciem podzielonych symboli jest znacznie szybsze, dlatego zawsze używamy flag wiersza poleceń -gsplit-dwarf
i -gdwarf-5
.
Format debugowania DWARF5 daje również dostęp do innej przydatnej funkcji. Zawiera on indeks nazw w danych debugowania, które będą generowane po przekazaniu 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
Podczas sesji debugowania wyszukiwanie symboli odbywa się często 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 daną nazwę. Bez indeksu nazw potrzebne byłoby wyczerpujące przeszukanie wszystkich danych debugowania, aby znaleźć odpowiednią jednostkę kompilacji, która definiuje poszukiwany element nazwany.
Dla ciekawostek: dane debugowania
Możesz użyć funkcji llvm-dwarfdump
, aby zobaczyć dane DWARF. Spróbujmy:
llvm-dwarfdump mandelbrot.wasm
W ten sposób uzyskasz ogólne informacje o „Kompilowaniu jednostek” (w przybliżeniu plików źródłowych), w przypadku których mamy informacje na temat debugowania. W tym przykładzie mamy tylko dane debugowania dotyczące: mandelbrot.cc
. Ogólne informacje poinformują nas, że mamy jednostkę szkieletową. Oznacza to, że dane w tym pliku są niekompletne i że istnieje oddzielny plik .dwo
, który zawiera pozostałe informacje debugowania:
Możesz też spojrzeć na inne tabele w tym pliku, np. w tabeli liniowej, która przedstawia mapowanie kodu bajtowego Wasm na wiersze C++ (spróbuj użyć llvm-dwarfdump -debug-line
).
Możesz też przejrzeć informacje na temat debugowania w osobnym pliku .dwo
:
llvm-dwarfdump mandelbrot.dwo
TL;DR: Jaka jest zaleta użycia rozszczepienia debugowania?
Podział informacji debugowania ma kilka zalet w przypadku pracy z dużymi aplikacjami:
Szybsze łączenie: tag łączący nie musi już analizować wszystkich informacji debugowania. Łączniki zwykle muszą przeanalizować całe dane DWARF, które znajdują się w pliku binarnym. Dzięki wyodrębnianiu dużych części danych debugowania do osobnych plików stosują one obsługę mniejszych plików binarnych, co przyspiesza łączenie (zwłaszcza w przypadku dużych aplikacji).
Szybsze debugowanie: w przypadku niektórych wyszukiwań symboli debuger może pominąć analizowanie dodatkowych symboli w plikach
.dwo
/.dwp
. W przypadku niektórych wyszukiwań (np. żądań w mapowaniu wierszy plików Wasm-to-C++) nie musimy uwzględniać dodatkowych danych debugowania. Dzięki temu nie musimy już wczytywać ani analizować dodatkowych danych debugowania.
1: jeśli nie masz w systemie najnowszej wersji usługi llvm-objdump
, ale korzystasz z wersji emsdk
, znajdziesz ją w katalogu emsdk/upstream/bin
.
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 > 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.