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.

c49e6f37878d24aff2e6f52ece66d25a.png

Эти кружки представляют локальные проекты монорепозитория, а не внешние npm-пакеты. Проект D зависит от C и G, и это означает, что С и G нужно собрать до сборки D.

8eae08a81ead482beb72ce5d90cd7a95.png

Если мы внесём изменения в проект B, то на момент вызова последующего build произойдёт rebuild сначала проекта B, потом C, и в последнюю очередь D, так как они зависят друг от друга. Все остальные проекты останутся без изменений.

В основе кеширования сборок лежит похожий механизм определения проектов, которые необходимо пересобрать. Разница в том, что результат успешной сборки помещается в tar-архив. Имя файла с архивом содержит специальный хеш. Созданный архив кешируется и помещается в хранилище.

Перед сборкой проекта вычисляется его хеш, который далее запрашивается в кеше. Если запись кеша есть в хранилище, то все существующие выходные папки удаляются, tar-архив из предыдущей сборки извлекается в папку проекта, а сборка пропускается. Кешируются не все папки проекта, а только те, которые указаны в конфигурации, например, папка dist.

Есть два варианта хранения закешированных архивов:

  • В папке кеша на локальном диске. Расположение по умолчанию — common/temp/build-cache.

  • В контейнере облачного хранилища. По умолчанию система CI будет настроена на запись в облачное хранилище, а отдельным пользователям будет предоставлен доступ только для чтения. Например, каждый раз, когда PR объединяется с основной ветвью, система CI получает архивы сборок и загружает их в облачное хранилище. И поэтому даже самая первая сборка после команды git clone будет очень быстрой.

Хеш формируется следующим образом. Создаётся хэш SHA1, и в него в определённом порядке добавляются данные:

  • список названий output-папок в JSON-формате, которые должны быть закешированы;

  • название последней введённой команды;

  • хеш каждой зависимости проекта.

Сформированный хеш помещается в название файла с tar-архивом, который будет сохранён в хранилище. Исходный код получения хеша:

Функция _getCacheIdФункция _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.

3003a9189e34240fa5052457bac88284.png

На момент написания статьи, помимо сохранения кеша в локальном хранилище, доступны ещё два варианта хранения в облачных контейнерах: 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.

/config/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 (в случае использования облачного хранилища) и значительно его ускорить. Также в качестве плюса можно выделить получение возможности на основе кеширования настроить фазы сборок и тем самым усилить параллелизм, благодаря чему выиграть время при комбинировании запуска некоторых команд.

© Habrahabr.ru