В этой статье рассматривается кейс по ускорению браузерного приложения через замену вычислений JavaScript на WebAssembly.
WebAssembly — что это такое?
Если коротко, то это это бинарный формат инструкций для стековой виртуальной машины. Часто Wasm (сокращенное название) называют языком программирования, но это не так. Формат инструкций исполняется в браузере наряду с JavaScript.
Важно, что WebAssembly можно получить при компиляции исходников на таких языках, как C/C++, Rust, Go. Здесь применяется статическая типизация и так называемая плоская модель памяти. Код, как сказано выше, хранится в компактном бинарном формате, благодаря чему выполняется почти так же быстро, как если бы приложение было запущено с помощью командной строки. Эти возможности и привели к росту популярности WebAssembly.
Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».
Skillbox рекомендует: Практический курс «Мобильный разработчик PRO».
На данный момент Wasm используется во многих приложениях, от игр вроде Doom 3 до портированных в веб приложений типа Autocad и Figma. Wasm применяется и в такой сфере, как serverless вычисления.
В этой статье приведен пример использования Wasm для ускорения аналитического веб-сервиса. Для наглядности мы взяли работающее приложение, написанное на С, которое скомпилируется в WebAssembly. Результат будет использован для замены малопроизводительных участков JS.
Трансформация приложения
В примере будет использоваться браузерный сервис fastq.bio, который предназначен для генетиков. Инструмент позволяет оценить качество секвенирования (расшифровки) ДНК.
Вот пример приложения в работе:
Подробности процесса не стоит приводить, поскольку они довольно сложны для неспециалистов, но если вкратце, то ученые по указанной выше инфографике могут понять, прошел ли процесс секвенирования ДНК гладко и какие возникли проблемы.
У этого сервиса есть альтернативы, десктопные программы. Но fastq.bio позволяет ускорить работу, визуализируя данные. В большинстве других случаев нужно уметь работать с командной строкой, но не у всех генетиков есть нужный опыт.
Работает все просто. На входе — данные, представляемые в виде текстового файла. Этот файл генерируется специализированными инструментами для секвенирования. В файле размещается список последовательностей ДНК и оценка качества для каждого нуклеотида. Формат файла .fastq, поэтому сервис и получил такое название.
Реализация на JavaScript
Первый шаг пользователя при работе с fastq.bio — выбор соответствующего файла. Используя объект File, приложение считывает случайную выборку данных из файла и обрабатывает этот пакет. Задача JavaScript здесь — выполнение несложных строковых операций и подсчет показателей. Один из них — количество нуклеотидов A, C, G и T на разных фрагментах ДНК.
После просчета нужных показателей они визуализируются при помощи Plotly.js, а сервис начинает работать с новой выборкой данных. Разделение на фрагменты сделано для повышения качества UX. Если работать со всеми данными сразу, процесс зависнет на какое-то время, поскольку файлы с результатами секвенирования занимают сотни гигабайтов файлового пространства. Сервис же берет участки данных размером от 0,5 до 1 Мб и работает с ними шаг за шагом, выстраивая графические данные.
Вот как это работает:
В красном прямоугольнике размещается алгоритм строковых преобразований для получения визуализации. Это наиболее нагруженная с точки зрения вычислений часть сервиса. Стоит попробовать заменить ее на Wasm.
Тестируем WebAssembly
Для оценки возможности использования Wasm команда проекта занялась поиском готовых решений для создания QC-метрики (QC — quality control) на основе файлов fastq. Поиск велся среди инструментов, написанных на С, С++ или Rust, чтобы была возможность портировать код на WebAssembly. Кроме того, инструмент не должен быть «сырым», требовался сервис, уже проверенный учеными.
В результате выбор был сделан в пользу seqtk. Приложение довольно популярно, оно open-source, исходный язык — С.
Перед преобразованием в Wasm стоит посмотреть принцип компиляции seqtk для десктопа. Согласно Makefile, вот то, что нужно:
# Compile to binary
$ gcc seqtk.c \
-o seqtk \
-O2 \
-lm \
-lz
В принципе, скомпилировать seqtk можно при помощи Emscripten. Если его нет, обходимся образом Docker.
$ docker pull robertaboukhalil/emsdk:1.38.26
$ docker run -dt --name wasm-seqtk robertaboukhalil/emsdk:1.38.26
При желании собрать его можно и самостоятельно, но на это нужно время.
Внутри контейнера без проблем можно взять emcc в качестве альтернативы gcc:
# Compile to WebAssembly
$ emcc seqtk.c \
-o seqtk.js \
-O2 \
-lm \
-s USE_ZLIB=1 \
-s FORCE_FILESYSTEM=1
Изменений минимум:
Вместо вывода в бинарный файл Emscripten для генерации файлов используется .wasm и .js, который применяется для запуска модуля WebAssemby.
Для поддержки библиотеки zlib используется флаг USE_ZLIB. Библиотека распространена и портирована на WebAssembly, а Emscripten включает ее в проект.
Активируется виртуальная файловая система Emscrippten. Это POSIX-подобная ФС, работающая в оперативной памяти внутри браузера. Когда страница обновляется, память очищается.
Чтобы понять, зачем нужна виртуальная файловая система, стоит сравнить способ запуска seqtk из командной строки со способом запуска скомпилированного модуля WebAssembly.
# On the command line
$ ./seqtk fqchk data.fastq
# In the browser console
> Module.callMain(["fqchk", "data.fastq"])
Получение доступа к виртуальной файловой системе нужно, чтобы не переписывать seqtk под строковый, а не файловый ввод. В этом случае фрагмент данных отображается как файл data.fastq в виртуальной ФС с вызовом на нем main() seqtk.
Вот новая архитектура:
Рисунок демонстрирует, что вместо вычислений в основном потоке браузера используется WebWorkers. Этот способ дает возможность выполнять вычисления в фоновом потоке, не ухудшая отзывчивость браузера. Ну а контроллер WebWorker запускает Worker, управляя его взаимодействием с основным потоком.
Команда seqtk запускается при помощи Worker на примонтированном файле. После завершения выполнения Worker выдает результат в виде Promise. Когда сообщение получено главным потоком, результат используется для обновления графиков. И так в несколько итераций.
Что насчет производительности WebAssembly?
Для того, чтобы оценить изменение производительности, команда проекта воспользовалась параметром количества операций чтения в секунду. Время построения интерактивных графиков не учитывается, поскольку в обеих имплементациях используется JavaScript.
При использовании решения «из коробки» прирост производительности составил девять раз.
Это отличный результат, но, как оказалось, есть возможность оптимизировать и его. Дело в том, что большое количество результатов QC-анализа не используется seqtk, поэтому их можно удалить. Если это сделать, результат по сравнению с JS улучшается в 13 раз.
Достичь его удалось простым комментированием команд printf().
Но и это не все. Дело в том, что на этом этапе fastq.bio получает результаты анализа с вызовом разных функций С. Любая из них вычисляет свой набор характеристик, так что каждый фрагмент файла читался по два раза.
Для того, чтобы обойти это проблему, было решено совместить две функции в одну. В результате производительность увеличилась в 20 раз.
Стоит отметить, что такого выдающегося результата можно достичь далеко не всегда. В некоторых случаях производительность падает, так что стоит оценивать каждый конкретный случай.
В качестве вывода можно сказать, что Wasm действительно дает возможность улучшить производительность приложения, но использовать его нужно с умом.
Skillbox рекомендует:
- Двухлетний практический курс «Я — веб-разработчик PRO».
- Онлайн-курс «С#-разработчик с 0».
- Практический годовой курс «PHP-разработчик с 0 до PRO».