Выбор подходящих зависимостей для проекта может быть сложной задачей. При принятии решения здесь необходимо учитывать множество аспектов — начиная от производительности и заканчивая стилем оформления кода. Своевременный выбор подходящих зависимостей может сэкономить программистам кучу времени, которое иначе пришлось бы тратить на исправления, доработки и рефакторинг. Одна из важных составляющих при работе с устройствами Интернета вещей — это размер двоичного кода приложения. О нём и пойдёт речь далее.
Раньше в качестве серверных и клиентских платформ на Rust мы использовали actix-web
и reqwest
, одни из самых популярных в своих сегментах платформ с широким функционалом. В какой-то момент мы посчитали, что у этих платформ слишком уж много разных зависимостей, и решили поменять клиентскую платформу reqwest
на awc
, надеясь таким образом уменьшить общий размер двоичного кода.
Меньший размер двоичного кода — это та цель, к которой всегда нужно стремиться, ведь известно, что HTTP клиент-сервер — это компонент, многократно переиспользуемый в приложениях, где задействована технология Интернета вещей. Однако после полной миграции общий размер двоичного кода у нас увеличился примерно на 3%.
Тогда мы решили оценить, каким будет размер двоичного кода при разных сочетаниях пар этих платформ. Таким образом мы сможем выбрать наиболее подходящие зависимости для наших текущих и будущих проектов.
Сначала мы определили базовые требования к каждой платформе, основываясь на типичном примере использования. Затем выбрали для тестирования несколько популярных пар приложений. После этого мы определились с тем, какие аспекты приложений было бы интересно проанализировать и сравнить. И, наконец, перешли к делу.
Кроме того, было создано интерактивное веб-приложение для наглядного представления собранных нами данных, чтобы не только мы, но и все желающие могли проанализировать результаты. Помимо общего размера двоичного кода, было проанализировано предположительное влияние каждой из его зависимостей. Благодаря всему этому мы можем определиться с новой парой платформ (сервер + клиент), которую будем применять дальше.
При определении правил тестирования, мы руководствовались потребностями HTTP клиент-сервера в нашем Updatehub Agent, где запускалась периодическая задача — проверять и, возможно, устанавливать обновления. Важно также, чтобы Agent всегда выдавал быстрый отклик на запросы других приложений или самого пользователя. Через небольшой HTTP API Agent предлагает набор конечных точек, к которым локальные приложения могут получить доступ, либо запрашивая информацию о выполнении агента, либо для запуска каких-то действий, например отмены загрузки.
С учётом всего этого мы установили следующие требования:
- Асинхронный API: за что мы так любим Rust? За текущее состояние поддержки экосистемы
async/await
. Язык предлагает очень хороший и простой способ реализации параллельного выполнения, обеспечивая всегда быстрый отклик Agent на запросы. - Общий доступ к внутреннему состоянию: в процессе обновления имеется несколько переменных, которые будут храниться в памяти и могут быть запрошены в любое время в Agent. Именно поэтому нам нужно, чтобы был протестирован и общий доступ к внутреннему состоянию между клиентом, используемым для отправки запросов в облако, и сервером, используемым для отправки ответов на локальный API.
- Один маршрут на сервер и на запросы: мы решили ограничиться только одним маршрутом при тестировании. Надеемся, что этого будет достаточно, чтобы связать соответствующие функции библиотек с конечным двоичным кодом — тогда можно будет их сравнить.
- Работа с OpenSSL при возможности: в Updatehub Agent мы уже используем OpenSSL для проверки цифровых подписей. Мы использовали OpenSSL по умолчанию всякий раз, когда это было возможно, поскольку в нашем сценарии применения remote link обычно использует протокол HTTPS.
Установив требования к приложениям, мы определились с участниками нашего тестирования:
Реализация Клиент Сервер
dummy - -
actix_full awc actix-web
actix_reqwest reqwest actix-web
gotham_reqwest reqwest gotham
warp_reqwest reqwest warp
hyper_reqwest reqwest hyper
hyper_full hyper hyper
tide_surf surf tide
warp_surf surf warp
При проведении анализа данных мы использовали cargo-bloat для оценки веса каждой зависимости в конечном двоичном коде. Он не даёт 100% точности, тем не менее, он проясняет «неизвестные» части в двоичном коде (обозначенные как крейт с именем «[Unknown]»). В наших сценариях использования их всегда было небольшое количество.
Для всех проектов мы также протестировали несколько различных флагов компиляции, чтобы проверить возможность оптимизации размера двоичного кода. В следующей таблице показаны все задействованные параметры:
Параметр Значения
Оптимизация [0; 1; 2; 3; s; z]
Оптимизация во время компоновки [thin; fat]
Модули кодогенерации [1; 16]
Мы создали интерактивную информационную панель для отображения интерактивных графиков, используемых в приложении shinyapp. В этом приложении вы можете более детально просмотреть все собранные нами данные. Давайте посмотрим на самые важные.
Начинаем разбор результатов со сравнения между actix-reqwest
и actix-full
, первыми участниками нашего тестирования. Даже с усовершенствованной оптимизацией контекст, который мы настроили для тестирования размеров двоичного кода, всё равно показывает преимущество reqwest
над awc
.
Ещё один важный момент, который мы хотели бы подчеркнуть: модель микрозависимости, которой отдаётся предпочтение в Rust, не слишком сильно влияет на размер двоичного кода. На следующей диаграмме мы показываем распределение по размерам всех крейтов, используемых во всех платформах-участниках нашего тестирования. Эта блочная диаграмма указывает на то, что большинство крейтов вносит очень незначительный вклад в общий размер двоичного кода и что на самом деле за его увеличение ответственны несколько более крупных крейтов:
Пока мы тестировали флаги оптимизации, никаких сюрпризов не возникло. Установка уровня оптимизации на z
, оптимизации во время компоновки на fat
и количества модулей кодогенерации на 1
привела к улучшению результатов у всех участников тестирования. На следующей диаграмме показана оптимизация у tide-surf
:
async-std
и tide-surf
— наша новая пара HTTP клиент-сервер для встроенных приложений. Несмотря на наличие высокоуровневого API, у этой пары результаты оказались лучше, чем у более низкоуровневых API, таких как hyper_full
.
Если бы мы выбирали пару, полностью основанную на tokio, ещё одной основной среде выполнения для асинхронной экосистемы rust, мы безусловно остановились бы на warp. Оказалось, что у этого крейта небольшой по сравнению с hyper расход вычислительных ресурсов в виде увеличения размера двоичного кода. Причём такой расход обусловлен созданием HTTP-серверов с гораздо более простым API.
Если хотите быть в курсе будущих обновлений нашего тестирования, обязательно следите за репозиторием на GitHub. Для отслеживания изменений результатов во времени будут использоваться теги версий. Более подробно ознакомиться с собранными нами данными можно, заглянув в интерактивное приложение shinny app.
Читайте также:
Перевод статьи Jonathas Conceição: Benchmarking HTTP Client-Server Binary Size in Rust