CI/CDパフォーマンス改善の勘所

@mizchi at Offers Deep Dive

About

Q: なんでCI/CD高速化するんだっけ?

CI 待ち時間はあらゆる作業に対しての純粋なオーバーヘッドだから

  • 人間がレビューを受ける回数増やすため
    • レビュワーはCI通らないと見ない/見る必要ない
  • デプロイ回数を増やすため(Four Keys)
  • 直近のタスクを覚えておけず、フロー効率が下がるため
  • 自分の本音: 15分超えたら仕事にならない

CI: 目標設定

  • ~5分
    • タスクに連続性が感じられる範囲
  • ~15分
    • 休憩を挟みつつタスクを忘れずにおける範囲
  • それ以上
    • 待ち時間に別作業をやり始める(ような圧を感じる)
    • スイッチングコストでフロー効率が悪化

アプリケーション/モジュールの設計パターン

  • 単一モジュール
    • 利用される最小の利用単位
  • 単一アプリケーション
    • デプロイを伴うサーバー/クライアントアプリケーション
  • モノレポ
    • packages/*, apps/*
    • 複数のモジュール/アプリケーションで構成。内部依存がある
  • 同一リポジトリ内に複数のモノレポ
    • 複雑。コンウェイ的な事情。大抵 shared/** な共有ライブラリもある

分割統治: コスパがいい単一ライブラリ+ユニットテストを最小単位で考える状態を作る

CIチューニング方針

  • 目安: 単体テスト: 5分以内 / E2E 込み 15分以内
  • トップダウンに並列化 / ボトムアップにマイクロチューニング
  • 過度に複雑にならない範囲で ワークフローを並列化する
    • 前提が強いと再設計が大変で、腐りやすい。多少愚直なぐらいでいい
    • モジュール単位のテスト自体に柔軟性がほしい。特にテストランナー設定
  • 共通セットアップ(テンプレート)部分を強めにチューニング

CIログ読む

  • 時間かかってる順に取り組む
  • 分割されている場合、最後に終了するワークフローから逆算

alt text

共通セットアップ

  • git checkout / setup-xxx
    • clone
  • 外部ライブラリのインストール(npm install)
  • Docker?
    • どれだけキャッシュしようがイメージの転送量速度ボトルネック
    • CI 内は docker 消すのが一番手っ取り早いが...
  • 全体 5m 達成するなら、目安 90s 以内に抑えたい

turborepo から学ぶこと

  • https://turbo.build/
  • 依存関係からトポロジカルソート
  • モジュール単位の (git) diff で変更範囲を判定
    • 変更があったら下位モジュールのタスク実行/結果をキャッシュ
    • diff がなければキャッシュを再利用
  • 差分がなければ何も実行しないのが理想

モジュール境界の設計

  • モジュール抽出は担当責務の宣言
    • 責務外の外部レイヤ(DB/サーバー/ブラウザ)に可能な限り干渉しない
    • テスト駆動/DDD/クリーンアーキテクチャの発想が必要
    • 逆に、テストが書きやすいコードを考えると、モジュール単位が決まる
  • 結合テストは関連モジュールのセットアップだけでビルドできるようにしたい
  • 単一アプリケーション内のutils/* が膨らんでいる場合、それはモジュール

E2E やるか問題

E2E(playwright/puppeteer) に手を染めたら考えること

  • 導入/管理は高コストな反面、テストケースの追加は低コスト
    • スモークテストは内部設計の理解なく書けてしまう
    • 定期的にテストを棚卸しすること
    • 「それユニットテストで良くない?」
  • 安易なウェイト処理を行わない
    • 適切なイベント駆動ならE2Eでも高速に動く waitFor(...)
  • 最終的に金で並列化する(しかない)

最後に: 結局素振りが一番大事

  • 小さいリポジトリで素振りする
    • 初期設計者以外は知見溜まりづらい
    • 自分で同じ構成のものを素組しよう
  • 過度に今の状態に最適化しない
    • コード量や複雑さに応じてボトルネックは移動する
    • 共通設定に強い前提があると、プロジェクト全体の健全性を損ねる
  • E2E との付き合い方を決めること
    • コツとしては、とにかくユニットテストに寄せる。E2E を増やさない

失敗集

  • git の履歴が膨らんで checkout だけで 2分超えた
  • workspaces 未対応の npm@6 で npm link でハックしてたら再現できなくなった
  • xargs で rollup をスレッド数だけ並列化したらM1を2台破壊した
  • yarn v1 にしかない --modules-folder を使ったら他に再現できる環境がなくなった
  • 型だけ定義する types モジュールを切り出したら循環参照が解決できなくなった
  • nx の生成するボイラープレートが古くて負債を再生産し続けていた
  • 共通セットアップの docker network 上のエンドポイントに全モジュールが依存して剥がせない
  • モジュール分割せずに新旧E2Eテストランナーを複数入れたら、 dependencies がデッドロック