1) Задержка обратной связи прерывает выполнение других задач
Наши конвейеры непрерывной интеграции обеспечивают важную обратную связь по нашему коду — выполнение статического анализа, автоматизация тестирования и покрытие. В идеале мы получаем эту обратную связь до того, как слишком углубимся в другую задачу. Обратная связь, которая приходит после того, как мы уже переключились на другую задачу, означает выбор между смещением контекста из этой задачи или задержкой исправления выделенных проблем.

2) Одновременное слияние нескольких пользователей будет заблокировано
Если вы используете правила защиты ветки и обязательные статусы GitHub, ваш конвейер CI должен быть завершен, прежде чем вы сможете слиться с основной веткой. Чем дольше выполняется ваш CI, тем выше вероятность того, что кто-то опередит вас в слиянии, заставив вас получить последние изменения и перезапустить слияние. Эта ситуация еще более распространена для команд, использующих монорепозитории, когда несколько команд фиксируют данные в одном репозитории, или групп, использующих транковую разработку, где каждый член команды может объединяться с основной веткой несколько раз в день.

GitHub работает над очередью слияния, которая устранит эти проблемы, но тем временем нам нужно следить за нашими средами выполнения CI.

Кроме того, если вы еще не используете его, я рекомендую вам включить автоматическое слияние в вашем репозитории GitHub. Достаточно сложно дождаться CI, не забыв при этом вернуться к слиянию после завершения CI и самостоятельно нажать кнопку слияния.

3) Медленная CI имеет тенденцию становиться еще медленнее
Когда тесты и пайплайны уже работают медленно, люди меньше заботятся об их производительности. Со временем пренебрежение накапливается, и конвейеры становятся все медленнее и медленнее. Медленные конвейеры маскируют серьезные проблемы с производительностью. В одном проекте выяснилось, что 80% продолжительности нашего теста было вызвано таймерами сна, которые не были смоделированы. Поскольку все так привыкли к медленным тестам, никто не удосужился провести расследование.

Медленные тесты увеличиваются по мере роста вашего приложения; если ваше трехмесячное приложение содержит двести тестов, выполнение которых занимает четыре минуты, то вскоре у него будет четыре сотни тестов, выполнение которых занимает восемь минут.

4) Наложение библиотек утомительно
Это особенно неприятно, когда вы пытаетесь внести изменения в общую библиотеку, и вам нужно ждать медленной непрерывной интеграции в двух разных репозиториях. Вам нужно дождаться CI для вашего запроса на включение в библиотеку, затем еще раз для слияния, прежде чем вы сможете даже увеличить версию библиотеки для потребляющего приложения. Для часто обновляемых библиотек ожидание в конвейерах непрерывной интеграции отнимает много времени.

5) Медленная CI конфликтует с некоторыми другими популярными методологиями

  • Монорепозитории. Поскольку изменение общей библиотеки в монорепозитории может повлиять на несколько приложений в монорепозитории, вашему CI потребуется выполнить все тесты в монорепозитории. Выполнение такого большого количества тестов не только требует еще больше времени, но и означает, что запущенные, медленные и ненадежные тесты одной команды влияют на всех остальных, использующих один и тот же репозиторий.
  • (Немасштабируемая) разработка на основе магистрали. Если вы делаете коммит непосредственно в основную ветку, а затем запускаете CI, другие могут получить ваши изменения еще до того, как CI подтвердит, что они работают. Поскольку неработающая сборка в основной ветке является высшим приоритетом, необходимость переключения задач из-за сбоя CI еще более разрушительна.
  • Синхронные проверки кода — при попытке выполнить проверку кода перед запуском другой задачи вам необходимо дождаться завершения конвейеров CI, прежде чем запрашивать проверку. Медленные конвейеры добавляют утомительное ожидание намеренно синхронному процессу.

С помощью нескольких незначительных изменений вы можете значительно сократить время работы ваших конвейеров CI. GitHub Actions имеет несколько важных ограничений, о которых вам нужно знать.

Улучшение производительности Jest

Ограничение 1: каждая виртуальная машина запускается заново

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

Если вы хотите увидеть разницу на своем локальном компьютере для разработки, попробуйте запустить `jest — clearCache` перед выполнением тестов.

Поскольку виртуальные машины, используемые GitHub Actions, запускаются заново при каждом запуске, каждый раз, когда вы запускаете Action, конвейеру необходимо транспилировать весь ваш TypeScript, пересматривать вашу папку и фиктивную структуру и догадываться, как распараллелить ваши тесты. Чтобы обойти недостаток сохраняемости, вам нужно использовать встроенный кеш GitHub Action, чтобы добавить сохраняемость между запусками CI.

- name: Cache Jest cache
  uses: actions/cache@v3
  with:
    path: /tmp/jest_rt
    key: ${{ hashFiles('**/package-lock.json') }}

- run: run test

По моему опыту, документация Jest о кешах оказалась точной: каждый раз, когда я добавлял этот оператор в существующий конвейер, я наблюдал соответствующее сокращение времени выполнения вдвое.

В отличие от кэширования ваших установленных node_modules, здесь нет четкого ключа кэширования. Выберите что-то, что меняется слишком часто, и у вас никогда не будет действительного кеша для извлечения; выберите значение, которое меняется слишком редко, и большая часть кеша будет устаревшей. Использование файла блокировки проекта может обеспечить хороший баланс, если его содержимое меняется каждую неделю или две. Цель состоит в том, чтобы кеш Jest покрывал подавляющее большинство файлов, которые не менялись в последнее время.

Ограничение 2: Агенты сборки имеют только два ядра

Jest активно использует распараллеливание. При локальном запуске Jest пытается использовать половину доступных ядер ЦП, чтобы сбалансировать производительность тестов, не блокируя ваш компьютер.

Однако, в отличие от ваших локальных компьютеров, виртуальные машины, используемые GitHub Actions, имеют только два ядра, что снижает способность Jest выполнять ваши тесты параллельно. GitHub работает над предложением больших бегунов с большим количеством ядер, но пока нет точных сроков доступности.

Во-первых, вы должны убедиться, что Jest использует оба доступных ядра. Значения по умолчанию, которые не позволяют вам заблокировать компьютер разработки во время выполнения тестов, не требуются в конвейере. Вы не удвоите свою тестовую производительность, поскольку Jest использовала часть одного ядра для планирования, но вы все равно получите приличный прирост.

- run: npm run test -- --maxWorkers=2

Во-вторых, вы можете использовать дополнительные агенты сборки, чтобы компенсировать нехватку ядер. Используя «матричную» стратегию, вы можете разделить один рабочий процесс на несколько виртуальных машин. Использование дополнительных виртуальных машин в качестве резервных для многопоточности может показаться излишним, но это очень эффективно.

Если вы используете Jest 28 или более позднюю версию, вы можете использовать команду --shard CLI, чтобы равномерно распределить тесты по каждому агенту сборки.

--shard

jobs:
  test:
    strategy:
      matrix:
        shard: [1/3, 2/3, 3/3]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - run: npm install

      - uses: actions/cache@v3
        with:
          path: /tmp/jest_rt
          key: ${{ hashFiles('**/package-lock.json') }}-${{ matrix.shard }}

      - run: npm run test -- --maxWorkers=2 --shard=${{ matrix.shard }}

В качестве альтернативы, если вы все еще застряли на более старой версии Jest, вы можете вручную разделить свои тесты с помощью CLI. Для достижения правильного баланса может потребоваться экспериментирование, но не так уж сложно сбалансировать вещи так, чтобы группы выполняли задание в течение минуты или двух друг от друга.

# package.json
"test:a-n": "jest --testPathPattern=\"/src/components/[A-Na-n].*\"",
"test:o-z": "jest --testPathPattern=\"/src/components/[O-Zo-z].*\"",
jobs:
  test:
    strategy:
      matrix:
        testGroup: ['a-n', 'o-z']
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - run: npm install

      - uses: actions/cache@v3
        with:
          path: /tmp/jest_rt
          key: ${{ hashFiles('**/package-lock.json') }}-${{ matrix.testGroup }}

      - run: npm run test:${{ matrix.testGroup }} -- --maxWorkers=2

Если вы сможете лучше использовать преимущества параллелизма, вы сможете значительно повысить производительность конвейера. Тестовый прогон, который занимает 20 минут, можно сократить до пяти минут, разделив его на четыре параллельных прогона.

Наконец, обратная связь CI не обязательно должна быть все или ничего; Рассмотрим задачи в конвейере CI, которые можно разделить на три категории:

  • Задачи обратной связи: сбои здесь требуют участия разработчика и должны предупреждать разработчика как можно скорее. Линты, сборки и тесты попадают в эту категорию. Стремитесь выполнить эти задачи в течение пяти минут после запуска конвейера.
  • Блокирующие задачи: это важные шаги, которые должны блокировать слияния, но вряд ли потерпят неудачу и редко требуют вмешательства разработчика. Примеры включают отправку контейнеров Docker в ваш репозиторий образов или публикацию библиотек во внутреннем репозитории npm. Они должны завершиться примерно через семь-десять минут после начала пробежки.
  • Неблокирующие задачи: эти задачи не должны блокировать слияния; у них либо нулевой процент отказов, либо отказы здесь не являются приоритетом. Этот шаг важен для проверки кода, но менее важен для слияния. Или вы можете передавать данные о конвейере, тестах или сборке в сторонний инструмент.

Разделение рабочего процесса на отдельные задания гарантирует, что разработчики получат уведомление о конвейере, как только что-то выйдет из строя. Между тем, задачи, которым не нужно блокировать слияния, не нужно устанавливать в качестве обязательных статусов, и они могут позволить слияниям продолжаться до их завершения.

jobs:
  test:
    uses: ./.github/workflows/test.yaml

  publish:
    needs: [test]
    uses: ./.github/workflows/publish.yaml
    
  upload-coverage:
    needs: [ test ]
    uses: ./.github/workflows/upload-coverage.yaml

Всего за час или два работы применение этих настроек может завершить ваши тесты менее чем за пять минут. Более быстрые тесты означают более быструю обратную связь, и уверенность в ваших изменениях приходит быстрее. Вы потратите меньше времени на ожидание завершения CI и будете меньше переключаться между задачами.

Больше контента на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord.

Хотите повысить узнаваемость и принятие вашего технологического стартапа? Посмотрите Цирк.