Отладка WebAssembly быстрее

Philip Pfaffe
Ким-Ань Тран
Kim-Anh Tran
Eric Leese
Sam Clegg

На Chrome Dev Summit 2020 мы впервые продемонстрировали поддержку отладки Chrome для приложений WebAssembly в Интернете. С тех пор команда вложила много энергии в расширение возможностей разработчиков для больших и даже огромных приложений. В этом посте мы покажем вам ручки, которые мы добавили (или заставили работать) в различных инструментах и ​​как их использовать!

Масштабируемая отладка

Давайте продолжим с того места, на котором мы остановились в нашем посте 2020 года. Вот пример, который мы тогда рассматривали:

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

Это все еще довольно небольшой пример, и вы, скорее всего, не увидите никаких реальных проблем, которые можно увидеть в действительно большом приложении, но мы все равно можем показать вам, каковы новые функции. Это быстро и легко настроить и попробовать самому!

В последнем посте мы обсуждали, как скомпилировать и отладить этот пример. Давайте сделаем это еще раз, но давайте также взглянем на //производительность// :

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

Эта команда создает двоичный файл Wasm размером 3 МБ. И основная часть этой информации, как и следовало ожидать, — это отладочная информация. Вы можете проверить это с помощью инструмента llvm-objdump [1], например:

$ 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

Этот вывод показывает нам все разделы, которые есть в сгенерированном файле Wasm, большинство из них являются стандартными разделами WebAssembly, но есть также несколько пользовательских разделов, имена которых начинаются с .debug_ . Вот где двоичный файл содержит нашу отладочную информацию! Если мы сложим все размеры, мы увидим, что отладочная информация занимает примерно 2,3 МБ из нашего 3-МБ файла. Если мы также time команды emcc , мы увидим, что на нашей машине ее выполнение заняло примерно 1,5 секунды. Эти цифры представляют собой небольшой базовый показатель, но они настолько малы, что, вероятно, никто на них не обратит внимания. Однако в реальных приложениях двоичный файл отладки может легко достигать размера в ГБ, а его сборка может занять несколько минут!

Пропуск Бинариена

При создании приложения Wasm с помощью Emscripten одним из последних этапов сборки является запуск оптимизатора Binaryen . Binaryen — это набор инструментов компилятора, который оптимизирует и легализует двоичные файлы, подобные WebAssembly. Использование Binaryen в составе сборки обходится довольно дорого, но требуется только при определенных условиях. Для отладочных сборок мы можем значительно ускорить время сборки, если избежим необходимости в проходах Binaryen. Наиболее распространенный проход Binaryen предназначен для легализации сигнатур функций, включающих 64-битные целочисленные значения. Выбрав интеграцию WebAssembly BigInt с использованием -sWASM_BIGINT мы можем избежать этого.

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

На всякий случай мы добавили флаг -sERROR_ON_WASM_CHANGES_AFTER_LINK . Это помогает обнаружить, когда Binaryen запущен, и неожиданно перезаписать двоичный файл. Таким образом, мы можем быть уверены, что остаемся на быстром пути.

Несмотря на то, что наш пример довольно небольшой, мы все равно можем увидеть эффект пропуска Binaryen! По time эта команда выполняется чуть меньше 1 с, то есть на полсекунды быстрее, чем раньше!

Расширенные настройки

Пропуск сканирования входного файла

Обычно при связывании проекта Emscripten emcc сканирует все входные объектные файлы и библиотеки. Это делается для того, чтобы реализовать точные зависимости между функциями библиотеки JavaScript и собственными символами в вашей программе. Для более крупных проектов дополнительное сканирование входных файлов (с использованием llvm-nm ) может значительно увеличить время компоновки.

Вместо этого можно запустить параметр -sREVERSE_DEPS=all , который указывает emcc включить все возможные собственные зависимости функций JavaScript. Это требует небольшого размера кода, но может ускорить время компоновки и может быть полезно для отладочных сборок.

Для такого маленького проекта, как наш пример, это не имеет особого значения, но если в вашем проекте сотни или даже тысячи объектных файлов, это может значительно улучшить время компоновки.

Удаление раздела «имя»

В крупных проектах, особенно в тех, в которых часто используются шаблоны C++, раздел «имя» WebAssembly может быть очень большим. В нашем примере это лишь небольшая часть общего размера файла (см. вывод llvm-objdump выше), но в некоторых случаях он может быть очень значительным. Если раздел «имя» вашего приложения очень велик и отладочной информации достаточно для ваших нужд отладки, может быть полезно удалить раздел «имя»:

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

Это удалит раздел «имя» WebAssembly, сохранив при этом разделы отладки DWARF.

Отладка деления

Двоичные файлы с большим количеством отладочных данных увеличивают не только время сборки, но и время отладки. Отладчику необходимо загрузить данные и построить для них индекс, чтобы он мог быстро отвечать на запросы, например «Какой тип локальной переменной x?».

Разделение отладки позволяет нам разделить отладочную информацию для двоичного файла на две части: одна, которая остается в двоичном файле, и одна, которая содержится в отдельном, так называемом объектном файле DWARF ( .dwo ). Его можно включить, передав флаг -gsplit-dwarf в 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

Ниже мы показываем различные команды и файлы, генерируемые при компиляции без отладочных данных, с отладочными данными и, наконец, как с отладочными данными, так и с отладочным разделением.

различные команды и какие файлы генерируются

При разделении данных DWARF часть отладочных данных сохраняется вместе с двоичными файлами, тогда как большая часть помещается в файл mandelbrot.dwo (как показано выше).

Для mandelbrot у нас есть только один исходный файл, но обычно проекты больше этого и включают более одного файла. Отладка деления генерирует файл .dwo для каждого из них. Чтобы текущая бета-версия отладчика (0.1.6.1615) могла загружать эту разделенную отладочную информацию, нам необходимо объединить все это в так называемый пакет DWARF ( .dwp ), например:

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

объединить два файла в пакет DWARF

Преимущество сборки пакета DWARF из отдельных объектов заключается в том, что вам нужно обслуживать только один дополнительный файл! В настоящее время мы работаем над загрузкой всех отдельных объектов в будущем выпуске.

Что с DWARF 5?

Возможно, вы заметили, что в приведенную выше команду emcc мы добавили еще один флаг -gdwarf-5 . Включение версии 5 символов DWARF, которая в настоящее время не используется по умолчанию, — это еще один трюк, который поможет нам быстрее начать отладку. При этом в основном двоичном файле сохраняется определенная информация, которая в версии 4 по умолчанию не учитывается. В частности, мы можем определить полный набор исходных файлов только из основного двоичного файла. Это позволяет отладчику выполнять основные действия, такие как отображение полного дерева исходного кода и установка точек останова, без загрузки и анализа полных данных символов. Это значительно ускоряет отладку с помощью символов разделения, поэтому мы всегда используем флаги командной строки -gsplit-dwarf и -gdwarf-5 вместе!

Благодаря формату отладки DWARF5 мы также получаем доступ к еще одной полезной функции. Он вводит индекс имени в отладочные данные, который будет сгенерирован при передаче флага -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

Во время сеанса отладки поиск символов часто происходит путем поиска объекта по имени, например, при поиске переменной или типа. Индекс имени ускоряет этот поиск, указывая непосредственно на единицу компиляции, которая определяет это имя. Без индекса имени потребовался бы исчерпывающий поиск по всем отладочным данным, чтобы найти правильную единицу компиляции, определяющую искомую именованную сущность.

Для любопытных: просмотр отладочных данных

Вы можете использовать llvm-dwarfdump , чтобы просмотреть данные DWARF. Давайте попробуем:

llvm-dwarfdump mandelbrot.wasm

Это дает нам обзор «модулей компиляции» (грубо говоря, исходных файлов), для которых у нас есть отладочная информация. В этом примере у нас есть только отладочная информация для mandelbrot.cc . Общая информация сообщит нам, что у нас есть скелет модуля , что просто означает, что у нас есть неполные данные в этом файле, и что существует отдельный файл .dwo , который содержит оставшуюся отладочную информацию:

mandelbrot.wasm и информация об отладке

Вы также можете просмотреть другие таблицы в этом файле, например, таблицу строк, которая показывает сопоставление байт-кода Wasm со строками C++ (попробуйте использовать llvm-dwarfdump -debug-line ).

Мы также можем просмотреть отладочную информацию, содержащуюся в отдельном файле .dwo :

llvm-dwarfdump mandelbrot.dwo

mandelbrot.wasm и информация об отладке

TL;DR: В чем преимущество использования отладочного деления?

Разделение отладочной информации дает несколько преимуществ при работе с большими приложениями:

  1. Более быстрое связывание: компоновщику больше не нужно анализировать всю отладочную информацию . Линкерам обычно необходимо проанализировать все данные DWARF, находящиеся в двоичном формате. Выделяя большие части отладочной информации в отдельные файлы, компоновщики работают с меньшими двоичными файлами, что приводит к сокращению времени компоновки (особенно актуально для больших приложений).

  2. Ускоренная отладка. Отладчик может пропустить анализ дополнительных символов в файлах .dwo / .dwp для поиска некоторых символов . Для некоторых поисков (например, запросов на сопоставление строк файлов wasm-to-C++) нам не нужно просматривать дополнительные данные отладки. Это экономит нам время, поскольку нет необходимости загружать и анализировать дополнительные данные отладки.

1 : Если в вашей системе нет последней версии llvm-objdump и вы используете emsdk , вы можете найти ее в каталоге emsdk/upstream/bin .

Загрузите предварительный просмотр каналов

Рассмотрите возможность использования Chrome Canary , Dev или Beta в качестве браузера для разработки по умолчанию. Эти каналы предварительного просмотра дают вам доступ к новейшим функциям DevTools, тестируют передовые API-интерфейсы веб-платформы и находят проблемы на вашем сайте раньше, чем это сделают ваши пользователи!

Свяжитесь с командой Chrome DevTools

Используйте следующие параметры, чтобы обсудить новые функции и изменения в публикации или что-либо еще, связанное с DevTools.