1
0
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

GitHub Action で npm ci 高速化のために cache を使ってみる実験ノート

Posted at

背景

GitHub Actions で、ユニットテストのフレームワークである Vitest を 動かそうとしていました。Vitest を動かすには、Node.js 環境を構築してパッケージインストールをして、という動作が必要になります。

パッケージのインストールは、あまり数が多くないうちは問題にならないにしても、増えてくるとパッケージインストールに時間を食われるようになってきます。

そのとき、インストールしたパッケージを丸々保存しておき、次使うときに復元してやることで、パッケージインストールにかかる時間を減らすことができ、ワークフローの実行時間を減らすことができます。GitHub Actions では、cache という機能があり、それを活用できます。

(今のところ、使っているパッケージは Vitest のみで実行時間は問題になっていないのですが、) 興味を持ったこの機会に cache 機能を使っての効果を確かめてみることにします。

下準備

適当に GitHub のリモートリポジトリを用意し、それをクローンしたローカルリポジトリ上で作業をします。

git clone <URI>
cd <repo_name>

違いを分かりやすくするため、依存解決を重くします。ひたすら知っているパッケージをインストールしましょう。

npm install -D jest vite vitest electron react nextjs nuxt

ls
# node_modules  package.json package-lock.json
package.json
{
  "devDependencies": {
    "electron": "^31.2.0",
    "jest": "^29.7.0",
    "nextjs": "^0.0.3",
    "nuxt": "^3.12.3",
    "react": "^18.3.1",
    "vite": "^5.3.3",
    "vitest": "^2.0.2"
  }
}

node_modulesディレクトリには、インストールしたパッケージの実態が格納されています。これは、同時に生成されたパッケージ情報ファイルの package.json package-lock.json から再構築することが可能なため、Git などのソース管理システムに追跡させないべきです。

.gitignore
node_modules

パッケージの実態 node_modules を再構築するには、以下のコマンドを実行します。

npm ci

この ci は Clean Install の意。Continuous integration の CI ではないので注意。

npm install でも可能ですが、GitHub Actions などの CI (Continuous Integration) 環境上では、npm ci の方が推奨されています。詳細は他記事に丸投げで。

普通、CI 環境上は何もない状態から始まるので、パッケージをインストール (再構築) するところから始まります。パッケージが多くなってくると、その再構築 npm ci に時間をとられるようになってくるわけです。

処理時間測定方法

以下の流れを基本とし、cache を使ってたり使ってなかったりする GitHub Actions ワークフローを実行します。

  1. ソースチェックアウト actions/checkout@v4
  2. Node.js セットアップ actions/setup-node@v4
  3. パッケージインストール npm ci

このうち、2 と 3、またキャッシュを使用する場合はキャッシュリストアにかかる時間も含めた部分を計測対象とします。

actions-steps.png

ワークフローを 5回 re-run (合計 6回実行) し、初回以外の 5回で実行時間を確認します。

actions-re-run-1.png

actions-re-run-2.png

キャッシュを用いる場合、初回はキャッシュヒットしないために時間がかかるため、初回の実行時間は無視します。

実験

cache 無し

まずは cache 機能を用いない、基本的な形で実装します。

name: JS dependency test 1

on: [ push, workflow_dispatch ]

jobs:
  js-depend-test:
    runs-on: ubuntu-latest

    steps:
    - name: Source checkout
      uses: actions/checkout@v4
    - name: Set up Node 20
      uses: actions/setup-node@v4
      with:
        node-version: 20
    - name: Dependency installing
      run: npm ci

所要時間は以下の通り。

回数 setup-node [秒] npm ci [秒] 計 [秒]
1 0 11 11
2 0 10 10
3 0 10 10
4 1 12 13
5 0 10 10
6 2 11 13
2~6 平均 0.6 10.6 11.2

actions/setup-node で cache

actions/node-setup に、キャッシュを設定できる項目があるようです。これも試してみます。

name: JS dependency test 2

on: [ push, workflow_dispatch ]

jobs:
  js-depend-test:
    runs-on: ubuntu-latest

    steps:
    - name: Source checkout
      uses: actions/checkout@v4
    - name: Set up Node 20
      uses: actions/setup-node@v4
      with:
        node-version: 20
+       cache: npm
+       cache-dependency-path: package-lock.json
    - name: Dependency installing
      run: npm ci

所要時間は以下の通り。

回数 setup-node [秒] npm ci [秒] 計 [秒]
1 1 10 11
2 1 7 8
3 9 (?) 8 17
4 3 8 11
5 2 7 9
6 2 7 9
2~6 平均 3.4 7.4 10.8

(?) 一度だけ、なぜか setup-node に異様に時間がかかりました。回線かなんかでも不安定になったのでしょうか・・・?

cache の一覧を見てみると、新しく cache が作られているのが分かります。(下のもう一つの cache は、次の「actions/cache で node_modules を丸々 cache」で生成されたもの。)

actions-cache.png

actions/cache で node_modules を丸々 cache

node_modules さえ復元できれば、必要なパッケージは揃うことになるわけです。

というわけで、cache を使ってnode_modules を丸々保存および復元するようにしてみます。

name: JS dependency test 3

on: [ push, workflow_dispatch ]

jobs:
  js-depend-test:
    runs-on: ubuntu-latest

    steps:
    - name: Source checkout
      uses: actions/checkout@v4
    - name: Set up Node 20
      uses: actions/setup-node@v4
      with:
        node-version: 20
+   - name: Dependency restore
+     id: cache-restore
+     uses: actions/cache@v4
+     with:
+       path: node_modules
+       key: js-depend-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    - name: Dependency installing
+     if: steps.cache-restore.outputs.cache-hit != 'true'
      run: npm ci

cache がヒットした際は、npm ci は不要になるので実行されないように組みました。

所要時間は以下の通り。

回数 setup-node [秒] cache [秒] npm ci [秒] 計 [秒]
1 1 1 10 12
2 0 4 0 (*) 4
3 0 2 0 (*) 2
4 0 2 0 (*) 2
5 0 2 0 (*) 2
6 0 2 0 (*) 2
2~6 平均 0.0 2.4 0.0 2.4

(*) if の条件が満たされないためにスキップされた箇所

step-skipped.png

'actions/cache@v4' について軽く説明

actions/cacheの設定について、key はファイル名、path はそのファイルの中身 (保存する内容)、というイメージで OK。

ワークフローの actions/cache に差し掛かったところで、ファイルの復元を試みます。ヒットする key がなければ、復元されず (当該ファイルやディレクトリは存在しないまま) 次に進みます。

当該ファイルが無いままだと正しく進められないので、キャッシュヒットしなかった場合はファイルを準備するスクリプトを動かす、という動きのために、上のワークフローファイルにあるように if: steps.cache-restore.outputs.cache-hit != 'true' が活用できます。

他のステップが終わった後に、Post ステップが入ってきます。key がヒットしなかった場合は、ここでキャッシュ保存をします。

cache-post.png


actions/setup-node の cache は微妙?

形態 所要時間平均 [秒]
cache 無し 11.2
actions/setup-node でキャッシュ 10.8

(謎の外れ値が入ったのもありますが、) actions/setup-node の効果が微妙に見えます。

ところで、actions/setup-node の README を見てみると、Caching global packages data とあります。

npm ci でパッケージインストールをするとき、global キャッシュにヒットすればキャッシュから復元、ヒットしなければパッケージサーバからダウンロードをする。ダウンロードが無い分、cache 無しよりは早くなったのではないか、と予想します。(ここら辺詳しくないが・・・)

まとめ

形態 所要時間平均 [秒]
cache 無し 11.2
actions/setup-node でキャッシュ 10.8
actions/cache でキャッシュ 2.4

actions/cachenode_modules を丸々復元する形だと、cache 無しで十数秒のところが数秒になり、いい感じにスピードアップができました。(なお適切なやり方かは不明)

この機会で、GitHub Actions の cache についてもちょっとは詳しくなれました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0