Rush.js — как можно значительно ускорить сборку ваших проектов, используя кеширование
Кеширование сборок — это экспериментальная функция, позволяющая сохранять результаты последней успешной сборки и использовать их в качестве основы для последующих сборок. Это значительно ускоряет процесс, поскольку не пересобираются проекты, которые не изменились с момента последней сборки. Давайте посмотрим, как это работает.
В Rush по умолчанию реализован механизм инкрементной (ускоренной) сборки. При повторном вызове команды rush build
пропускаются уже обновлённые проекты. Инкрементный анализ Rush опирается на хеши зависимостей. Их можно найти в файле project>/.rush/temp/shrinkwrap-deps.json внутри каждого проекта. Но результаты сборок никуда не сохраняются и поэтому, как правило, при переключении на другую ветку потребуется сделать полный rebuild
.
Проект считается обновлённым в следующих случаях:
проект создан локально;
с момента последней сборки не изменились исходные файлы и npm-зависимости;
проект зависит от других локальных проектов из монорепозитория и эти проекты обновлены;
параметры командной строки не изменялись. Например, если сначала была выполнена команда
rush build
, а затемrush build --production
, то в таком случае проекты требуют пересборки.
Механизм выбора проектов для rebuild
Предположим, что у нас есть монорепозиторий, состоящий из семи проектов: A, B, C, D, E, F, G.
Эти кружки представляют локальные проекты монорепозитория, а не внешние npm-пакеты. Проект D зависит от C и G, и это означает, что С и G нужно собрать до сборки D.
Если мы внесём изменения в проект B, то на момент вызова последующего build
произойдёт rebuild
сначала проекта B, потом C, и в последнюю очередь D, так как они зависят друг от друга. Все остальные проекты останутся без изменений.
В основе кеширования сборок лежит похожий механизм определения проектов, которые необходимо пересобрать. Разница в том, что результат успешной сборки помещается в tar-архив. Имя файла с архивом содержит специальный хеш. Созданный архив кешируется и помещается в хранилище.
Перед сборкой проекта вычисляется его хеш, который далее запрашивается в кеше. Если запись кеша есть в хранилище, то все существующие выходные папки удаляются, tar-архив из предыдущей сборки извлекается в папку проекта, а сборка пропускается. Кешируются не все папки проекта, а только те, которые указаны в конфигурации, например, папка dist.
Есть два варианта хранения закешированных архивов:
В папке кеша на локальном диске. Расположение по умолчанию — common/temp/build-cache.
В контейнере облачного хранилища. По умолчанию система CI будет настроена на запись в облачное хранилище, а отдельным пользователям будет предоставлен доступ только для чтения. Например, каждый раз, когда PR объединяется с основной ветвью, система CI получает архивы сборок и загружает их в облачное хранилище. И поэтому даже самая первая сборка после команды
git clone
будет очень быстрой.
Хеш формируется следующим образом. Создаётся хэш SHA1, и в него в определённом порядке добавляются данные:
список названий output-папок в JSON-формате, которые должны быть закешированы;
название последней введённой команды;
хеш каждой зависимости проекта.
Сформированный хеш помещается в название файла с tar-архивом, который будет сохранён в хранилище. Исходный код получения хеша:
Функция _getCacheId
Включение функции кеширования сборок
Для того, чтобы кеширование заработало, необходимо добавить файл с конфигурацией build-cache.json. Он помещается в корень папки common/config/rush.
/**
* Пример конфигурации build-cache.json
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/build-cache.schema.json",
/**
* Для в включения функции кеширования параметр buildCacheEnabled должен иметь значение true
*/
"buildCacheEnabled": true,
/**
* (Обязательный параметр) определяется где будет храниться результат сборки проекта
*
* Доступные значения: "local-only", "azure-blob-storage", "amazon-s3"
*/
"cacheProvider": "local-only",
/**
* Переопределение параметра cacheEntryNamePattern позволяет изменить формат названия файлов с кэшом проектов. По дефолту добавляется только hash
*/
//"cacheEntryNamePattern": "[projectName:normalize]-[phaseName:normalize]-[hash]"
/**
* azureBlobStorageConfiguration необходимо добавить, если "cacheProvider"="azure-blob-storage"
*/
"azureBlobStorageConfiguration": {
/**
* (Required) The name of the the Azure storage account to use for build cache.
*/
// "storageAccountName": "example",
/**
* (Required) The name of the container in the Azure storage account to use for build cache.
*/
// "storageContainerName": "my-container",
/**
* The Azure environment the storage account exists in. Defaults to AzurePublicCloud.
*
* Possible values: "AzurePublicCloud", "AzureChina", "AzureGermany", "AzureGovernment"
*/
// "azureEnvironment": "AzurePublicCloud",
/**
* An optional prefix for cache item blob names.
*/
// "blobPrefix": "my-prefix",
/**
* If set to true, allow writing to the cache. Defaults to false.
*/
// "isCacheWriteAllowed": true
},
/**
* amazonS3Configuration необходимо добавить, если "cacheProvider"="amazon-s3"
*/
"amazonS3Configuration": {
/**
* (Required unless s3Endpoint is specified) The name of the bucket to use for build cache.
* Example: "my-bucket"
*/
// "s3Bucket": "my-bucket",
/**
* (Required unless s3Bucket is specified) The Amazon S3 endpoint of the bucket to use for build cache.
* This should not include any path; use the s3Prefix to set the path.
* Examples: "my-bucket.s3.us-east-2.amazonaws.com" or "http://localhost:9000"
*/
// "s3Endpoint": "https://my-bucket.s3.us-east-2.amazonaws.com",
/**
* (Required) The Amazon S3 region of the bucket to use for build cache.
* Example: "us-east-1"
*/
// "s3Region": "us-east-1",
/**
* An optional prefix ("folder") for cache items. It should not start with "/".
*/
// "s3Prefix": "my-prefix",
/**
* If set to true, allow writing to the cache. Defaults to false.
*/
// "isCacheWriteAllowed": true
}
}
Чтобы включить запись кеша на локальный диск, необходимо добавить в конфигурацию build-cache.json только два параметра: buildCacheEnabled
со значением true
и cacheProvider
со значением local-only
. При таком варианте tar-архивы для каждого проекта после запуска команды rush build
помещаются в папку common/temp/build-cache.
На момент написания статьи, помимо сохранения кеша в локальном хранилище, доступны ещё два варианта хранения в облачных контейнерах: Microsoft Azure blob storage container и Amazon S3 bucket. Для них необходимо передать в параметр cacheProvider
значения azure-blob-storage
или amazon-s3
соответственно, а также настройки для конфигурации облачного контейнера azureBlobStorageConfiguration
либо amazonS3Configuration
(смотрите конфигурацию выше).
Настройка параметров кеширования отдельно для каждого проекта
Если на этом этапе запустить команду rush build --verbose
(при добавлении флага --verbose
в консоли отображаются логи, которые обычно скрыты во время сборки), то появится предупреждение:
Project does not have a rush-project.json configuration file, or one provided by a rig,
so it does not support caching.
Чтобы избавиться от этого предупреждения, дополнительно помимо конфигурации build-cache.json
нужно внутри каждого проекта настроить, какие папки и при вызове каких команд нужно закешировать. Для этого в папку configвнутри каждого проекта (важный момент, не в папке common, а в каждом проекте)добавляется файлrush-project.json.
{
"incrementalBuildIgnoredGlobs": ["temp/**"],
"disableBuildCacheForProject": false, // при необходимости можно выключить кеширование для отдельных проектов
"operationSettings": [
{
"operationName": "build", // operationName — это название команда или фаза, которая вызывается в проекте
// Название папок верхнего уровня, которые необходимо закешировать. Например: lib, dist
"outputFolderNames": ["output-folder-1", "output-folder-2"]
},
{
"operationName": "_phase:build", //фазы - это еще одна интересная эксперементальная функция, см. Enabling phased builds
"outputFolderNames": ["output-folder-a", "output-folder-b"]
},
{
"operationName": "test",
"disableBuildCacheForOperation": true
}
]
}
Также этот файл будет полезен при настройке ещё одной интересной экспериментальной функции — phased builds. Она позволяет определить некоторые отдельные операции как фазы, которые можно выполнять параллельно в один момент времени. Например одновременный запуск rush build
и rush test
. Скажем, если сборка проекта A завершилась, а он находится в зависимостях проектов B и С, то сначала соберётся проект A, затем одновременно со сборкой проекта C и B начнётся запуск unit-тестов в A. Подробнее про это можно прочитать здесь. И кстати, включение кеширования — одно из обязательных условий для этой фичи.
После добавления файлов rush-project.json в проекты при запуске команды rush build --verbose
результат сборки запишется в хранилище, а в консоли появится запись:
This project was not found in the build cache.
Caching build output folders: dist
Successfully set cache entry.
Время сборки 31 проекта при настроенном локальном хранилище: 6 минут 10,7 секунды.
При повторном запуске rush build
проверяется наличие проекта в кеше. Если запись кеша есть, то существующие выходные папки удаляются, tar-архив из предыдущей сборки извлекается в папку проекта, а сборка пропускается. В результате в терминале появится запись:
Build cache hit.
Clearing cached folders: dist
Successfully restored output from the build cache.
Время повторной сборки: 1,44 секунды.
Заключение
Если сравнивать инкрементную сборку по умолчанию при локальном запуске с кешированием сборок, то длительность повторных сборок примерно одинаковая. Но инкрементная сборка не учитывает, были ли изменения в output-папках, и в таком случае (либо после переключения на новую ветку) может потребоваться полный rebuild.
Главный плюс кеширования, на мой взгляд, — это возможность встроить этот механизм в процесс CI (в случае использования облачного хранилища) и значительно его ускорить. Также в качестве плюса можно выделить получение возможности на основе кеширования настроить фазы сборок и тем самым усилить параллелизм, благодаря чему выиграть время при комбинировании запуска некоторых команд.