Информационный портал по безопасности » Программирование » Как мы внедряли WebAssembly в Яндекс.Картах и почему оставили JavaScript

 

Как мы внедряли WebAssembly в Яндекс.Картах и почему оставили JavaScript

Автор: admin от 19-11-2019, 08:30, посмотрело: 89

Меня зовут Валерий Шавель, я из команды разработки векторного движка Яндекс.Карт. Недавно мы внедряли в движок технологию WebAssembly. Ниже я расскажу, почему мы её выбрали, какие результаты получили и как вы можете использовать эту технологию в своём проекте.



Как мы внедряли WebAssembly в Яндекс.Картах и почему оставили JavaScript


WebAssembly (Wasm).



Использование WebAssembly в картах



Сейчас большая часть обработки примитивов происходит в фоновом потоке (Web Worker), который живёт отдельной жизнью. Это сделано для того, чтобы максимально разгрузить главный поток. Таким образом, когда код для показа карты встраивается на страницу сервиса, который может сам добавлять существенную нагрузку, тормозов будет меньше. Минус в том, что нужно правильно настраивать обмен сообщениями между главным потоком и Web Worker’ом.



Часть обработки, происходящая в фоновом потоке, состоит, по сути, из двух шагов:

1. Декодируется формат protobuf, который приходит с сервера.

2. Геометрии генерируются и записываются в буферы.



На втором шаге формируются вершинный и индексный буферы для WebGL. Эти буферы применяются при рендеринге следующим образом. Вершинный буфер содержит для каждой вершины её параметры, которые необходимы для определения её положения на экране в конкретный момент. Индексный буфер состоит из троек индексов. Каждая тройка означает, что на экране должен быть отображён треугольник с вершинами из вершинного буфера под указанными индексами. Поэтому примитив необходимо разбить на треугольники, что тоже может быть трудоёмкой задачей:



Как мы внедряли WebAssembly в Яндекс.Картах и почему оставили JavaScript



Очевидно, что во время второго шага происходит немало манипуляций с памятью и математических расчётов, потому что для правильного рендеринга примитивов нужно много информации о каждой вершине примитива:



Как мы внедряли WebAssembly в Яндекс.Картах и почему оставили JavaScript



Нас не устраивала производительность нашего кода на javascript’е. В это время все стали писать о WebAssembly, технология постоянно развивалась и улучшалась. Почитав исследования, мы предположили, что Wasm может ускорить наши операции. Хотя мы не были полностью в этом уверены: оказалось сложно найти данные о применении Wasm’а в столь большом проекте.



Также Wasm кое в чём очевидно хуже, чем TypeScript:

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

2. Намного больший, чем в TS’е, размер исходного кода при компиляции Wasm’а.

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



Однако, несмотря на всё это, мы рискнули переписать часть своего кода с использованием Wasm’а.



Общая информация о WebAssembly





Как мы внедряли WebAssembly в Яндекс.Картах и почему оставили JavaScript


Wasm — это бинарный формат; можно скомпилировать в него разные языки, а затем запустить код в браузере. Часто такой заранее скомпилированный код оказывается быстрее классического javascript’a. Код в формате WebAssembly не имеет доступа к DOM-элементам страницы и, как правило, применяется для выполнения на клиенте трудоёмких вычислительных задач.



В качестве компилируемого языка мы выбрали C++, поскольку он достаточно удобный и быстрый.



Для компиляции C++ в WebAssembly мы применяли emscripten. После его установки и добавления в проект на C++, чтобы получить модуль, нужно написать главный файл проекта определённым образом. Например, он может выглядеть вот так:



```cpp
#include <emscripten/bind.h>
#include <emscripten.h>
#include <math.h>

struct Point {
    double x;
    double y;
};

double sqr(double x) {
    return x * x;
}

EMSCRIPTEN_BINDINGS(my_value_example) {
    emscripten::value_object<Point>("Point")
        .field("x", &Point::x)
        .field("y", &Point::y)
    ;

    emscripten::register_vector<Point>("vector<Point>");

    emscripten::function("distance", emscripten::optional_override(
        [](Point point1, Point point2) {
            return sqrt(sqr(point1.x - point2.x) + sqr(point1.y - point2.y)) ;
    }));
}
```


Далее я опишу, как вы можете использовать этот код в своём проекте на TypeScript.



В коде мы определяем структуру Point и ставим ей в соответствие интерфейс Point в TypeScript, в котором будет два поля — x и y, они соответствуют полям структуры.



Далее, если мы захотим вернуть стандартный контейнер vector из C++ в TypeScript, то понадобится зарегистрировать его для типа Point. Тогда в TypeScript ему будет соответствовать интерфейс с нужными функциями.



И наконец, в коде показано, как зарегистрировать свою функцию, чтобы вызвать её из TypeScript по соответствующему имени.



Скомпилируйте файл с помощью emscripten и добавьте получившийся модуль в свой проект на TypeScript’е. Теперь мы можем для произвольного модуля emscripten написать общий файл d.ts, в котором заранее определены полезные функции и типы:



```javascript
declare module "emscripten_module" {
    interface EmscriptenModule {
        readonly wasmMemory: WebAssembly.Memory;
        readonly HEAPU8: Uint8Array;
        readonly HEAPF64: Float64Array;

        locateFile: (path: string) => string;
        onRuntimeInitialized: () => void;
        _malloc: (size: size_t) => uintptr_t;
        _free: (addr: size_t) => uintptr_t;
    }

    export default EmscriptenModule;
    export type uintptr_t = number;
    export type size_t = number;
}
```


И можем написать файл d.ts для нашего модуля:



```javascript
declare module "emscripten_point" {
    import EmscriptenModule, {uintptr_t, size_t} from 'emscripten_module';

    interface NativeObject {
        delete: () => void;
    }

    interface Vector<T> extends NativeObject {
        get(index: number): T;
        size(): number;
    }

    interface Point {
        readonly x: number;
        readonly y: number;
    }

    interface PointModule extends EmscriptenModule {
        distance: (point1: Point, point2: Point) => number;
    }

    type PointModuleUninitialized = Partial<PointModule>;

    export default function createModuleApi(Module: Partial<PointModule>): PointModule;
}
```




Теперь мы можем написать функцию, которая создаст Promise на инициализацию модуля, и воспользоваться ей:



[code]```javascript
import EmscriptenModule from 'emscripten_module';
import createPointModuleApi, {PointModule} from 'emscripten_point';
import * as pointModule from 'emscripten_point.wasm';

/**
* Promisifies initialization of emscripten module.
*
* @param moduleUrl URL to wasm file, it could be encoded data URL.
* @param moduleInitializer Escripten module factory,
* see https://emscripten.org/docs/compiling/WebAssembly.html#compiler-output.
*/
export default function initEmscriptenModule(
moduleUrl: string,
moduleInitializer: (module: Partial) => ModuleT
): Promise {
return new Promise((resolve) => {
const module = moduleInitializer({
locateFile: () => moduleUrl,
onRuntimeInitialized: function (): void {
// module itself is thenable, to prevent infinite promise resolution
delete (ссылке доступен проект-пример из статьи, там вы можете посмотреть настройку webpack.config и CMakeLists.



Результаты





Итак, мы переписали часть своего кода и запустили эксперимент, чтобы рассмотреть парсинг ломаных и многоугольников. На диаграмме продемонстрированы медианные результаты по одному тайлу для Wasm’а и javascript’а:



Как мы внедряли WebAssembly в Яндекс.Картах и почему оставили JavaScript


В итоге мы получили такие относительные коэффициенты по каждой метрике:



Как мы внедряли WebAssembly в Яндекс.Картах и почему оставили JavaScript


Как видно по чистому времени парсинга примитивов и времени декодирования тайла, Wasm быстрее более чем в четыре раза. Если же смотреть общее время парсинга, то здесь разница тоже значительная, однако она всё-таки немного меньше. Это связано с затратами на то, чтобы передать данные в Wasm и забрать результат. Также стоит отметить, что на первых тайлах общий выигрыш очень высок (на первых десяти — больше чем в пять раз). Однако потом относительный коэффициент уменьшается примерно до трёх.



В итоге всё это вместе помогло на 20–25% снизить время обработки одного тайла в фоновом потоке. Конечно, эта разница не так велика, как предыдущие, однако нужно понимать, что парсинг ломаных и многоугольников — это далеко не вся обработка тайла.



Если говорить о необходимости инициализации модуля, то из-за неё примерно у половины пользователей произошла задержка перед парсингом первого тайла. Медиана задержки — 188 мс. Задержка случается только перед первым тайлом, а выигрыш в парсинге постоянный, так что можно смириться с небольшой паузой на старте и не считать её серьёзной проблемой.



Ещё одна отрицательная сторона — размер файла с исходным кодом. Сжатый gzip’ом минифицированный код всего векторного движка карты без Wasm’а — 85 КБ, с Wasm’ом — 191 КБ. При этом в Wasm’е реализован только парсинг ломаных и прямоугольников, а не всех примитивов, которые могут быть в тайле. Более того, для декодирования protobuf’а пришлось выбрать реализацию библиотеки на чистом C, с реализацией на C++ размер был ещё больше. Эту разницу можно несколько уменьшить, если при компиляции C++ использовать флаг компилятора -Oz вместо -O3, но она по-прежнему существенна. Кроме того, при такой замене мы теряем в производительности.



Тем не менее размер исходника на скорость инициализации карты повлиял незначительно. Wasm хуже только на медленных устройствах, и разница — менее 2%. А вот изначальный видимый набор векторных тайлов в реализации с Wasm’ом был показан пользователям немного быстрее, чем с JS’ной реализацией. Это связано с большим выигрышем на первых обработанных тайлах, пока JS ещё не оптимизирован.



Таким образом, Wasm сейчас — вполне достойный вариант, если вас не устраивает производительность кода на javascript. В то же время вы можете получить меньший выигрыш в производительности, чем мы, либо не получить его вообще. Это связано с тем, что иногда javascript сам работает достаточно быстро, а в Wasm требуется передавать данные и забирать результат.



В наших картах сейчас работает обычный javascript. Это связано с тем, что выигрыш в парсинге не так велик на общем фоне, и с тем, что в Wasm’е реализован парсинг только некоторых типов примитивов. Если это изменится — возможно, мы станем применять Wasm. Ещё один весомый аргумент против — сложность сборки и отладки: поддерживать проект на двух языках имеет смысл только тогда, когда выигрыш в производительности того стоит.

Источник: Хабр / Интересные публикации

Категория: Программирование / Яндекс

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
  • bowtiesmilelaughingblushsmileyrelaxedsmirk
    heart_eyeskissing_heartkissing_closed_eyesflushedrelievedsatisfiedgrin
    winkstuck_out_tongue_winking_eyestuck_out_tongue_closed_eyesgrinningkissingstuck_out_tonguesleeping
    worriedfrowninganguishedopen_mouthgrimacingconfusedhushed
    expressionlessunamusedsweat_smilesweatdisappointed_relievedwearypensive
    disappointedconfoundedfearfulcold_sweatperseverecrysob
    joyastonishedscreamtired_faceangryragetriumph
    sleepyyummasksunglassesdizzy_faceimpsmiling_imp
    neutral_faceno_mouthinnocent