背景
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
{
"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 などのソース管理システムに追跡させないべきです。
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 ワークフローを実行します。
- ソースチェックアウト
actions/checkout@v4
- Node.js セットアップ
actions/setup-node@v4
- パッケージインストール
npm ci
このうち、2 と 3、またキャッシュを使用する場合はキャッシュリストアにかかる時間も含めた部分を計測対象とします。
ワークフローを 5回 re-run (合計 6回実行) し、初回以外の 5回で実行時間を確認します。
キャッシュを用いる場合、初回はキャッシュヒットしないために時間がかかるため、初回の実行時間は無視します。
実験
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 で 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 |
'actions/cache@v4' について軽く説明
actions/cache
の設定について、key はファイル名、path はそのファイルの中身 (保存する内容)、というイメージで OK。
ワークフローの actions/cache
に差し掛かったところで、ファイルの復元を試みます。ヒットする key がなければ、復元されず (当該ファイルやディレクトリは存在しないまま) 次に進みます。
当該ファイルが無いままだと正しく進められないので、キャッシュヒットしなかった場合はファイルを準備するスクリプトを動かす、という動きのために、上のワークフローファイルにあるように if: steps.cache-restore.outputs.cache-hit != 'true'
が活用できます。
他のステップが終わった後に、Post ステップが入ってきます。key がヒットしなかった場合は、ここでキャッシュ保存をします。
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/cache
で node_modules
を丸々復元する形だと、cache 無しで十数秒のところが数秒になり、いい感じにスピードアップができました。(なお適切なやり方かは不明)
この機会で、GitHub Actions の cache についてもちょっとは詳しくなれました。