Тяжело в учении, сложно при внедрении, быстро в использовании
Павел Дадыкин, ROGII
Тяжело в учении,
сложно при внедрении,
быстро в использовании
Павел Дадыкин
ROGII
Обо мне
Павел Дадыкин
- 9 лет во фронтенде
- 2 года тимлид на проекте StarLite Web
- Первый раз на FrontendConf
О чём доклад
- История внедрения WebAssembly на реальном проекте
- Подводные камни и решение проблем
- Стоит ли вам использовать WebAssembly в своих проектах?
- Разработка продуктов для нефтегазовой индустрии
- Geoscience-решения
- Много математических расчётов
- Desktop, Web, iOS, Android, SDK, Public API
Что за расчёты?
Эволюция продуктов ROGII
- Расчёты написаны для Desktop App (на C++)
- Хранение данных в Cloud
- Повторение расчётов в Web, iOS, Android
Проблемы
- Дублирование расчётов на разных платформах
- Низкая производительность при вычислениях
- Сложности добавления/изменения расчётов
Цель
- Внедрить в приложение расчёты, написанные на С++
- Сохранить при этом текущий стек технологий:
- React
- Typescript
- Webpack
- Web workers
- Canvas
- WebGL
WebAssembly (WASM)
- Формат байт-кода, исполняемого современными браузерами
- Может взаимодействовать с JavaScript
Плюсы WASM
- Обеспечивает высокую скорость исполнения
- Поддержка всеми браузерами
- Компиляция в WASM доступна для множества языков
(C, C++, C#, Rust, Elixir, Erlang, Go, TypeScript, D, Kotlin)
Минусы WASM
- Чёрный ящик
- Внешняя типизация
- Конвертация данных JS → WASM → JS
Тяжело в учении
Примеры использования WASM в сети (конец 2021 года):
- A + B
- Factorial
- Fibonacci
Как же скомпилировать С++ в WASM?
Emscripten
- Компиляция из C/C++/LLVM-language в WebAssembly
- Запуск этого кода в Web/Node.js
- Поддержка библиотек C/C++
Документация Emscripten
Дефолтной конфигурации Emscripten
не существует!
Флаги компиляции
-s DYNAMIC_EXECUTION=0 -s ENVIRONMENT=worker
-s MODULARIZE=1 -s ALLOW_MEMORY_GROWTH=1 --bind
- Выключаем использование eval
- Задаем выполнение в веб-воркере
- Оборачиваем в модульный код (без глобальных переменных)
- Разрешаем автоматический рост памяти
- Говорим привязать все функции к js
Где же взять описание функций?
Как сделать удобной отладку ошибок?
Разные сборки (через cmake)
Debug
- Большой вес файла
- Работает медленнее
- Детальное описание ошибок
Release
- Маленький вес файла
- Работает быстро
- Сложно отлаживать
Утечки памяти
- Добавили дополнительный флаг в сборку
- Он передаёт флаг
-fsanitize=address –g2
компилятору
- Вызов функции для принудительного сбора статистики
Как реализовать версионирование?
Версионирование
Emscripten на выходе даёт 2 файла:
- Бинарный .wasm
- Обвязка в виде .js-файла
Версионирование
Добавить в начало JS-файла комментарий
/*
Version: 1.3.0
Commit: 681def4aefb99c9de5357617dc138b66e6ae1d0b
Pipeline: 103990
*/
Версионирование
- Используем
Nexus
- Собираем в npm-пакет
- Учитываем вариант сборки (Debug/Release)
"dependencies": {
"@nexus-npm/wasm": "0.9.0-Release",
}
Подключение WASM к проекту
Webpack 4 file-loader
{
test: /\.wasm$/,
type: "javascript/auto",
use: [{
loader: "file-loader",
options: { name: "wasm/[name].[hash].[ext]" }
}]
}
Загрузка WASM в приложение
import wasm from 'math-js.wasm';
const response = await client.get(wasm, {
responseType: 'arraybuffer'
});
const wasmBinary = response.data;
Typescript
-
@types/emscripten
для вспомогательных методов
-
Описание структур и функций вручную, используя Doxygen
-
Автоматическая генерация .ts из .hpp
CSP Error
Content-Security-Policy: script-src 'wasm-unsafe-eval'
Как конвертировать данные JS ↔ WASM?
JS
- Массивы объектов
- Автоматическое выделение памяти
- Автоматическая сборка мусора
C/C++
- std::vector / TypedArray / Динамические массивы
- Требуется выделение памяти
- Нужно освобождать выделенную память
Конвертация JS ↔ WASM
const pointer = instance._malloc(size * Float64Array.BYTES_PER_ELEMENT);
instance.HEAPF64.set(
new Float64Array(size),
pointer / Float64Array.BYTES_PER_ELEMENT,
);
instance._free(trajectory.mdArray.pointer);
Добавление обёртки
- Добавили метаданные для функций конвертации и высвобождения памяти
- Emscripten сам конвертирует типы в C-структуры
- Передаём сразу массив объектов
Удобные функции
convertToJSObject<WasmType, JSArrayType>(data)
convertFromJSObject<JSArrayType, WasmType>(data)
freeCollection(data)
Undefined / NaN
В TypeScript мы можем написать
type X = number | undefined
В C/C++ так нельзя!
Из функций расчёта в этом случае нам возвращается NaN
Undefined → NaN
point.data ?? NaN
if (!isNaN(point.data))
Batch запросов
- Большая часть времени тратится на преобразования из структур данных JS в WASM и обратно
- Лучше выполнить одну большую операцию, чем много маленьких
Batch запросов
for (const element of elements) {
const point = calculatePoint(element.x);
points.push(point);
}
const xArray = elements.map((element) => element.x);
const points = wasm.calculatePoints(xArray);
Как всё подружить с веб-воркерами?
Web workers
- Механизм, который позволяет скрипту выполняться в фоновом потоке
- Основной поток без блокировки и замедления
- Worker'ы могут запускать другие worker'ы
- Они общаются друг с другом через postMessage
Web workers в StarLite
Web workers & WASM
Web workers & WASM
Web workers & WASM
Тестирование
- Unit-тесты на стороне С++
- Интеграционные тесты QAA
Jest
import { readFileSync } from 'fs';
const wasmBinary = readFileSync(
'node_modules/path-to-wasm/math.wasm',
);
instance = await createModule({ wasmBinary });
Плюсы
- Скорость расчётов выросла (в 2 – 10 раз)
- Расчёты на разных платформах совпадают
- Нет дублирования математических расчётов
Минусы
- Сложность отладки
- Зависимость от другой команды
- Долгая итерация внедрения
Когда стоит использовать WASM?
- Нужные функции уже написаны на другом языке
- У вас большие объёмы данных, и вам нужен прирост в скорости
- У вас есть ресурсы на внедрение
Пожалуйста, оставьте свой отзыв
Павел Дадыкин
ROGII
https://meloman4eg.github.io/wasm-fc-2024/
@meloman4eg