[Перевод] Angular на стероидах: наращиваем производительность при помощи WebAssembly26.02.2024 16:45
В этом посте продемонстрировано, как с лёгкостью использовать WebAssembly внутри приложения, написанного на Angular. Иногда в приложении на Angular требуется выполнить задачу, которая в JavaScript завершается не слишком быстро. Конечно, можно переписать алгоритм на другом языке, например, AssemblyScript и Rust — и код станет эффективнее. Затем можно скомпилировать получившиеся фрагменты кода в файле WASM и потоком передать двоичные данные в приложение, чтобы можно было вызывать из него функции WASM. Бывает и так, что разработчику не удаётся найти в реестре NPM опенсорсные библиотеки, нужные для решения задачи. В таком случае можно написать пакет не на JS, а на каком-нибудь другом языке, затем скомпилировать этот пакет в WASM и опубликовать код WASM в реестре NPM. Angular-разработчики устанавливают новый пакет как зависимость и выполняют WASM-функции внутри приложения.
В следующем демонстрационном примере я напишу на AssemblyScript несколько функций для работы с простыми числами, а затем опубликую файл индекса в формате WASM. Затем скопирую файл WASM в приложение Angular, потоком отправлю двоичные данные через WebAssembly API и, наконец, стану вызывать эти функции, чтобы с их помощью выполнять различные действия над простыми числами.
Что такое WebAssembly?
WebAssembly — это составное слово, которое делится на 2 части: Web (Веб) и Assembly (Ассемблер). Когда пишешь на высокоуровневых языках, например, на AssemblyScript и Rust, получается код, который компилируется в ассемблер при помощи специальных инструментов. После этого разработчик может нативно выполнять ассемблерный код прямо в браузере, то есть, в вебе.
Как этот демо-пример может использоваться на практике
С этим демо-примером связаны 2 github-репозитория. В первом используется язык AssemblyScript, на котором пишется TypeScript-подобный код, компилируемый в Wasm. Во втором репозитории лежит простое приложение на Angular, в котором при помощи функций Wasm исследуются некоторые любопытные свойства простых чисел.
В индексе репозитория с AssemblyScript содержится 3 функции для работы с простыми числами:
isPrime — Определить, является ли данное целое число простым
findFirstNPrimes — Найти первые N простых чисел, где N — целое число
optimizedSieve — Найти все простые числа менее N, где N — целое число
AssemblyScript долбавляет в package.json скрипты, генерирующие, соответственно, debug.wasm и release.wasm.
Я скопировала release.wasm в каталог assets приложения Angular, написала загрузчик WebAssembly, чтобы организовать потоковую передачу двоичного файла и вернуть экземпляр WebAssembly. Главный компонент связывает экземпляр с компонентами, где он выступает в качестве ввода. Эти компоненты используют экземпляр для выполнения Wasm и вспомогательных функций для получения результатов операций над простыми числами.
Пишем WebAssembly на AssemblyScript
AssemblyScript — это TypeScript-подобный язык, на котором можно писать код, в дальнейшем компилируемый в WebAssembly.
Начинаем новый проект
npm init
Устанавливаем зависимость
npm install --save-dev assemblyscript
Выполняем команду для добавления скриптов в package.json и файлов скаффолда
npx asinit .
Наши собственные скрипты для генерации файлов debug.wasm и release.wasm
Реализуем алгоритм для работы над простыми числами на AssemblyScript
// assembly/index.ts
// Запись в файле, в этой записи указан модуль WebAssembly
// импорт модуля
declare function primeNumberLog(primeNumber: i32): void;
export function isPrime(n: i32): bool {
if (n <= 1) {
return false;
} else if (n === 2 || n === 3) {
return true;
} else if (n % 2 === 0 || n % 3 === 0) {
return false;
}
for (let i = 5; i <= Math.sqrt(n); i = i + 6) {
if (n % i === 0 || n % (i + 2) === 0) {
return false;
}
}
return true;
}
export function findFirstNPrimes(n: i32): Array {
let primes = new Array(n);
for (let i = 0; i < n; i++) {
primes[i] = 0;
}
primes[0] = 2;
primeNumberLog(primes[0]);
let num = 3;
let index = 0;
while(index < n - 1) {
let isPrime = true;
for (let i = 0; i <= index; i++) {
if (num % primes[i] === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
primeNumberLog(num);
primes[index + 1] = num;
index = index + 1;
}
num = num + 2;
}
return primes;
}
const MAX_SIZE = 1000001;
export function optimizedSieve(n: i32): Array {
const isPrime = new Array(MAX_SIZE);
isPrime.fill(true, 0, MAX_SIZE);
const primes = new Array();
const smallestPrimeFactors = new Array(MAX_SIZE);
smallestPrimeFactors.fill(1, 0, MAX_SIZE);
isPrime[0] = false;
isPrime[1] = false;
for (let i = 2; i < n; i++) {
if (isPrime[i]) {
primes.push(i);
smallestPrimeFactors[i] = i;
}
for (let j = 0; j < primes.length && i * primes[j] < n && primes[j] <= smallestPrimeFactors[i]; j++) {
const nonPrime = i * primes[j];
isPrime[nonPrime] = false;
smallestPrimeFactors[nonPrime] = primes[j];
}
}
const results = new Array();
for (let i = 0; i < primes.length && primes[i] <= n; i++) {
results.push(primes[i]);
}
return results;
}
primeNumberLog — это внешняя функция, которая логирует простые числа в findFirstNPrimes. У этой функции нет тела, и именно приложение Angular отвечает за то, чтобы предоставить для неё детали реализации.
После выполнения скрипта npm run asbuild в каталоге builds/ будут содержаться файлы debug.wasm и release.wasm. Та часть работы, которая касается WebAssembly, уже готова, и далее мы будем работать только с приложением Angular.
Комбинируем сильные стороны WebAssembly и Angular
В WebAssembly не переносятся такие высокоуровневые типы данных, как, например, массивы и булевы значения. Поэтому я установила загрузчик assemblyscript и с помощью содержащихся в нём вспомогательных функций привожу к корректным типам значения, возвращаемые функциями Wasm.
Установка зависимости
npm i @assemblyscript/loader
Сборка загрузчика WebAssembly
Далее, методом проб и ошибок я смогла импортировать в приложение Angular функции Wasm, организовав потоковую передачу release.wasm при помощи загрузчика assemblyscript.
Я инкапсулировала загрузчик WebAssembly в загрузочный сервис, тем самым позволив всем компонентам Angular многократно использовать функционал потоковой передачи данных. Если в браузере поддерживается функция instantiateStreaming, то возвращается экземпляр WebAssembly. Если функция instantiateStreaming не поддерживается, то будет вызвана резервная функция. Резервная функция преобразует ответ в массив ArrayBuffer и собирает экземпляр WebAssembly.
DEFAULT_IMPORTS также предоставляет реализацию primeNumberLog. primeNumberLog объявляется в файле index.ts репозитория AssemblyScript. Следовательно, ключом объекта служит его позиция в индексе без файлового расширения.
IsPrimeComponent вызывает функцию isPrime, определяющую, является ли данное целое число простым. isPrime возвращает 1, если перед нами простое число, в противном случае возвращает 0. Следовательно, оператор === сравнивает целочисленные значения, чтобы в результате вернуть булево.
FindFirstNPrimesComponent вызывает функцию findFirstNPrimes, чтобы получить первые N простых чисел. Функция findFirstNPrimes не может перенести целочисленный массив, поэтому я пользуюсь вспомогательной функцией __getArray, имеющейся в загрузчике, и с её помощью преобразую целочисленное значение в корректный целочисленный массив.
OptimizedSieveComponent вызывает функцию optimizedSieve, чтобы получить все простые числа, которые меньше to obtain N. Функция optimizedSieve также не может перенести целочисленный массив, и я пользуюсь вспомогательной функцией __getArray, с её помощью преобразую целочисленное значение в корректный целочисленный массив.