Ни один дракон не может устоять перед загадками и потерянным временем для их решения. ― Дж.Р.Р. Толкин Сегодня мы попытаемся раскрыть пути решения одной из самых больших проблем JavaScript ― CPU-bound заданий. Сделаем мы это в 2 частях. В части I мы поговорим и попробуем использование исключительно CPU-bound заданий. В части II ― модули, которые используются для frontend, CPU+I/O-bound задания и тому подобное. Node.js имеет несколько способов исполнения CPU intensive заданий: Просто запустить CPU-bound задачу в одном процессе, блокируя event loop. Кто-то может возразить, что это совсем не вариант, но если этот процесс был специально создан для этой задачи, то почему бы и нет. Правда не у всех есть пара дополнительных ядер. Создать отдельные процессы (Child Processes), распределить между ними задания. Создать cluster и заставить работать отдельные процессы в нем. Использовать Worker Threads и создать несколько дополнительных потоков исполнения. Попросить C++ разработчика написать C++ Addon, который загадочным образом выполняет CPU-bound задания. В конце концов, думаю все слышали старинные легенды про компилируемые языки программирования и о том, что “нативная” реализация ― это всегда успех (на этой фразе где-то в мире должен заплакать React Native разработчик, смотря на перформанс своего приложения). В этой статье мы не будем обсуждать реализацию каждого их этих методов, поскольку они все уже довольно детально описаны в других статьях и документации. Как пример CPU-bound задач мы возьмем разные хеш-функции. Инструментами станут “нативная” реализация конкретного модуля и версия, написанная на чистом JavaScript. Запускаться код буде на машине со следующими характеристиками: 8-ядерный процессор Intel® Core™ i7–7700HQ @ 2,80 ГГц Последним возникает вопрос ― каким образом донести основную идею, процесс и результаты исследования… И для этого была выбрана самая популярная, крутая и продвинутая игра…  1999-го года ― Heroes of Might and Magic III Наш герой ― Node.js и как у любого героя, у него есть путь. Наш герой крепок и могуч. Уже победил множество врагов, потому решил, что настало время разобраться с самым страшным врагом ― CPU-bound задачами. Наш герой должен иметь команду, так кого он возьмет с собой? Cluster ― это 7 черных драконов, 7 Child Processes ― 7 красных драконов та 1 красный дракон, которого называют JS, потому что он всегда извергает на врагов только одну струю огня. 7 Worker Threads ― 7 молодых зеленых дракона. Неопытных, но готовых к борьбе. 1 C++ Addon ― 1 архангел. Опытный воин, который не раскрывает всех секретов своей силы, но прекрасно показавший себя в предыдущих сражениях. Первая битва И первое зло на нашем пути это 1,400,000 строк (скелетная пехота), и единственный путь, который позволит нам их побороть ― прогнать их через Murmurhash (не криптографическая хеш-функция общего назначения) как можно быстрее. Модуль murmurhash3js будет использован, как чистая js имплементация и murmurhash-native как “нативная”. Имплементация (JS 1 процесс) ― ничего особенного, просто запускаем хеш-функцию в цикле и фиксируем разницу во времени до и после: Имплементация (Child Processes) ― порождаем (spawn) новый процесс и ждем окончания всех подсчетов (событие “close”): Имплементация (Cluster) ― разветвляем (fork) несколько (количество зависит от числа ядер процессора) рабочих процессов и ждем сообщение (messages) от них (про окончание работы) в основном процессе. В нашем случае як “сообщение” было использовано номер процесса: Имплементация (Worker Threads) ― почти та же, что и у “cluster” ― мы создаем несколько воркеров (workers) и ждем в основном потоке (main thread) сообщение (message) об окончании работы: Имплементация (C++ Addon) ― просто используем C++ Addon (модуль) в основном потоке (так же просто, как в JS имплементации): …and “Action!” Результаты первой битвы: Как мы видим, C++ Addon ― это самое быстрое решение в этом случае. Child Processes/Cluster/Worker Threads показали почти одинаковый результат. Второй раунд Следующее зло на нашем пути ― это 140 строк (скелетные драконы) и мы можем победить их только с помощью Bcrypt (sync) ― адаптивной криптографической функции формирования ключа, что используется для безопасного хранения паролей. Функция основана на шифре Blowfish. Имплементация абсолютно такая же, как и для Murmurhash. Отличаются только модули ― bcryptjs and bcrypt соответственно. Пример использования: Результаты второго раунда: В этом случае (sync) лучший вариант решения ― это разделить задание и выполнить параллельно, потому Child Processes/Cluster/Worker Threads справились лучше. Финальная битва Следующее зло ― это 140 строк (более мощные скелетные драконы) и мы точно сможем их победить, но на этот раз необходимо использовать асинхронный вариант Bcrypt. Имплементация аналогична предыдущей битве (даже модули те же). Пример: Результаты финальной битвы: Мы не блокировали Event Loop и использовали UV thread pool, потому в этом случае C++ Addon вновь на коне. …и результаты предыдущих битв: Необходимо отметить, что есть одно секретное оружие для нашего архангела. С его помощью он может сражаться даже еще эффективнее. Это количество потоков в UV thread pool (UV_TREADPOOL_SIZE=size), которые может быть увеличено (имеет значение 4 по умолчанию). В нашем случае bycrypt использует crypto.randomBytes(), потому это помогло уменьшить время исполнения bcrypt async почти в 2 раза (при установке значения 8). Крепость Argon 2 Вторая часть нашей эпической истории начинается возле замка “Argon 2”. Его назвали в честь функции формирование ключа, которая была выбрана победителем Password Hashing Competition в июле 2015. Функция имеет 3 версии: Argon2d подходит для защиты цифровой валюты и информационных систем, не подверженных атакам по сторонним каналам. Argon2i обеспечивает высокую защиту от trade-off атак, но работает медленнее версии d из-за нескольких проходов по памяти. Argon2id это гибридная версия. Node.js с командой должен взять эту крепость, используя argon2-browser (js) и hash-wasm (нативный) модули. Пример использования: Результаты битвы: C++ Addon снова на вершине решения чистых CPU-bound заданий. Восстановление замка Пока все битвы завершены. Мы должны отстроить город и заручится поддержкой местного населения. Для этого мы ознакомимся со всеми местными законами и традициями. К счастью, все находится в xlsx формате в 7 файлах, которые содержат по 5000 строк в каждом (js xlsx и нативный xlsx-util модули будут использованы для “магического чтения”). Пример использования (чтение и парсинг файла): Результаты чтения и парсинга: В этом случае мы имеем смешанное I/O (чтение файла) и CPU (парсинг) интенсивное задание. C++ Addon справился быстрее именно из-за второй составляющей этой работы. Время перемен Наконец-то мы можем изменять старые законы и построить новое процветающее общество. Для этого мы используем jsonnet язык темплейтов. Он поможет нам при необходимости: Сгенерировать данные для конфигурации. Управлять расширенной конфигурацией. Не иметь побочных эффектов. Организовать, упростить и унифицировать наш код. Модули @rbicker/jsonnet (js) и @unboundedsystems/jsonnet (нативный). Пример использования: Немного странный результат, который скорее всего зависит от внутренней реализации модулей. В любом случае у нас есть финальные результаты: И финальные результаты Части II: Из нашего исследования можно вынести следующее: Необходимо ответственно относиться к выбору модулей. Читать код библиотек и учитывать среду, в которой приложение будет запущено. Популярность модуля ― далеко не самый важный критерий выбора. Необходимо выбирать специфическое решение для специфической задачи. Child Processes, Cluster, Worker Threads ― каждый из этих инструментов имеет собственные характеристики и возможности использования. Не можно забывать о других языках программирования, которые могут помочь решить некоторые из ваших задач (C++ Addons, Node-API, Neon library). Необходимо планировать использование ресурсов (количество CPU или GPU ядер). Принимайте рациональные архитектурные решения (имплементируйте собственный thread pool, запускайте CPU-bound задания в отдельном микросервисе и тд). Найдите лучшую возможную комбинацию (C/C++/Rust/Go могут быть использованы не в основном потоке и event loop не будет заблокирован) и вы получите что-то вроде этого: Спасибо за прочтение. Надеюсь вам понравилась эта эпическая история и вы почувствовали себя частью легенды. Для дополнительной информации и возможности проверить результаты, пожалуйста, посетите мой github. Software Engineer