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

Раньше в качестве серверных и клиентских платформ на 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.

Сравнение awc с reqwest (интерактивный график).

Ещё один важный момент, который мы хотели бы подчеркнуть: модель микрозависимости, которой отдаётся предпочтение в Rust, не слишком сильно влияет на размер двоичного кода. На следующей диаграмме мы показываем распределение по размерам всех крейтов, используемых во всех платформах-участниках нашего тестирования. Эта блочная диаграмма указывает на то, что большинство крейтов вносит очень незначительный вклад в общий размер двоичного кода и что на самом деле за его увеличение ответственны несколько более крупных крейтов:

Распределение крейтов по размерам во всех проектах (интерактивная диаграмма).

Пока мы тестировали флаги оптимизации, никаких сюрпризов не возникло. Установка уровня оптимизации на z, оптимизации во время компоновки на fat и количества модулей кодогенерации на 1 привела к улучшению результатов у всех участников тестирования. На следующей диаграмме показана оптимизация у tide-surf:

Параметры оптимизации у 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

Предыдущая статьяКак создать конвейер автоматизированных сборок для проекта в Arduino Часть 2/2
Следующая статьяЧто нового в системной трассировке Android Studio