Rust で Profile-Guided Optimization やってみた
TLDR
Rust で実装した Wasm インタープリタで PGO 試したら 0〜10% ぐらい速くなった
Profile-Guided Optimization (PGO) とは
PGO はコンパイラの最適化の手法のうちの1つですが,プログラムを実行した後に行うという点で少し他と異なります.
- まずは普段どおりの最適化オプションでプログラムをビルドする
- プログラムを実環境で動かし,profile data を取る
- 再度プログラムをビルドしなおす.ただし,ここの最適化では 2. で収集した profile data に基づいてインライン化やレジスタ割り当てなどを行う
- より最適化されたプログラムが出来上がる
このように,実世界でどう実行されるかをフィードバックした最適化をかけて再ビルドすることで,プログラムのビルド時で完結していた従来の最適化よりも進んだ最適化をかけられるようにするというものです.
最近だと,Facebook が BOLT という大規模アプリ向けの最適化フレームワークを研究開発していたり,Google が llvm-propeller というこれも大規模アプリ向けのフレームワークを研究開発していたりで,最適化の分野では熱いトピックだと思ってます.あと Android のネイティブコードでも PGO が使えるみたいです.
この PGO の仕組みは LLVM に既に実装されており,Clang などで利用可能で,Clang のユーザマニュアルもあります.上記の 2. において profile data を収集するために Linux の perf などを使う方法も考えられますが,LLVM を使っていれば,より正確なデータを効率的に収集し,それを元にした最適化をかけるところまで全部やってくれます.Rust の PGO サポートは LLVM のこの仕組みに完全に乗っかる形になっています.
Rust で PGO する
最近 Rust で PGO が既にサポートされているのを rustc のガイドで見かけて,僕の観測範囲ではまだ誰も試してなさそうだったので試してみました.
基本的には下記の短いドキュメントに非常にミニマルな main.rs
単一ファイルでのやり方が書いてあります.
Profile Guided Optimization - The rustc book
ただ,小さすぎるプログラムだと効果が出なさそうなのと,自分の開発してるプログラムでどれぐらい効果があるものなのか興味があったので,今回は wain プロジェクトを使ってやることにしました.
wain は僕が趣味で開発している WebAssembly インタープリタで,no dependency, Safe Rust で約1.5万行ほどの実装規模です.小さいですが,小規模というほどでもない感じですね. 外部依存は無いですが,複数 crate に分割して workspace で管理しています.
準備
PGO は Stable Rust でサポートされているようですが,一応最新の LLVM でやりたかったので nightly を使っています. rustup を使って LLVM 関連のツールをインストールします.
rustup component add llvm-tools-preview
llvm-tools-preview コンポーネントをインストールしてもパスは通されないので,~/.rustup/toolchains/{toolchain}-{triple}/lib/rustlib/{triple}/bin
にパスを自前で通しておきます.
# nightly なら export PATH=$HOME/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/bin:$PATH # stable なら export PATH=$HOME/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/bin:$PATH
まずは profile を有効にしてビルドする
profile data を収集するために,まずは profile data を自動生成してくれるオプションを有効にした実行ファイルをビルドします.
CARGO_BUILD_RUSTFLAGS="-Cprofile-generate=$(pwd)/pgo-data" cargo build --release
$CARGO_BUILD_RUSTFLAGS
を使うことで,Cargo が rustc を使ってコンパイルする際に追加でコンパイルオプションを指定できます.
-Cprofile-generate=$(pwd)/pgo-data
というコンパイルオプションにより,カレントディレクトリの pgo-data
というディレクトリに profile data が収集されていきます.
ここで注意なのですが,cargo rustc
コマンドは使えません.今回は依存 crate を含むバイナリ全体を最適化したいので,依存 crate のコードでも profile data を収集する必要があります.そのため,依存 crate をビルドする際にも -Cprofile-generate
オプションを指定するのですが,cargo rustc
は依存 crate のビルド時には rustc に指定したオプションを渡しません.
上記 cargo build
により,普段どおり ./target/release/
に最適化された実行ファイルが出来上がります(今回は ./target/release/wain
)
profile data を収集する
今回はお試しなので,単に examples ディレクトリにある Wasm ファイルを順番に実行することにします
./target/release/wain ./examples/brainfxxk.wasm ./target/release/wain ./examples/mandelbrot.wasm ./target/release/wain ./examples/mt19937.wasm ./target/release/wain ./examples/n_queens.wasm ./target/release/wain ./examples/pi.wasm ...
これによって,./pgo-data
内に .profraw
という拡張子のバイナリファイルが生成されます..profraw
ファイルは実行ファイルと動的リンクライブラリの分だけ生成されるので,例えば2つライブラリを動的リンクしている実行バイナリであれば,3つの .profraw
ファイルが生成されます.
wain は動的リンクライブラリを使っていないので,1つだけ .profraw
が生成されます.
> ls ./pgo-data default_13294579539908499303_0.profraw
./target/release/wain
に異なる引数を与えて新しい Wasm コードを実行するたびに,収集した profile data が自動でこのファイルに追加されていきます.各 Wasm ファイルを ./target/release/wain
で実行しながら default_13294579539908499303_0.profraw
を見ると,徐々にファイルサイズが大きくなっていくのが分かります.
profile data を1つのファイルにマージする
前節で説明したように,profile data を収集した .profraw
ファイルは複数になることがあります.また,今回は手元の PC でしか profile data を収集していませんが,例えば複数の異なる本番環境で実行した profile data を1箇所に持ってきたりすると必然的に profile data は複数ファイルになります.
これらを llvm-profdata
コマンドで1つのファイルにマージします.llvm-profdata
コマンドは準備の節で説明した llvm-tools-preview
コンポーネントに含まれています.
llvm-profdata merge -o merged.profdata ./pgo-data
このコマンドを走らせるだけで,マージした profile data ファイル merged.profdata
を生成してくれます.
profile data を元にした最適化を有効にしてビルド
収集した merged.profdata
をコンパイラに食わせて,PGO を行います.
CARGO_BUILD_RUSTFLAGS="-Cprofile-use=$(pwd)/merged.profdata" cargo build --release
このように -Cprofile-use
オプションで指定してやるだけです.後は rustc と LLVM がすべてやってくれます.-Cprofile-generate
の場合と同様に,cargo rustc
は使えないので注意してください.依存 crate を含むプログラム全体で PGO を有効にします.
これで ./target/release/wain
に PGO を有効にしてビルドされた実行ファイルが生成されます.通常のリリースビルドでサイズが 716KB,PGO を有効にすると 783KB でわずかにサイズが増えていました.
どれぐらい速くなったのか測る
PGO を有効にした wain と通常のリリースモードでビルドした wain を用意します.
mv ./target/release/wain wain-pgo cargo build --release mv ./target/release/wain wain-orig
profile data の収集に使った examples ディレクトリにある Wasm ファイルを hyperfine を使って比較します.hyperfine は比較したいコマンドを与えると良い感じに実行・計測して比較してくれるすごいやつです.
hyperfine './wain-pgo ./examples/brainfxxk.wasm' './wain-orig ./examples/brainfxxk.wasm' hyperfine './wain-pgo ./examples/mandelbrot.wasm' './wain-orig ./examples/mandelbrot.wasm' hyperfine './wain-pgo ./examples/mt19937.wasm' './wain-orig ./examples/mt19937.wasm' hyperfine './wain-pgo ./examples/n_queens.wasm' './wain-orig ./examples/n_queens.wasm' hyperfine './wain-pgo ./examples/pi.wasm' './wain-orig ./examples/pi.wasm' ...
計測結果
examples/brainfxxk.wasm
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/brainfxxk.wasm |
25.0 ± 1.9 | 23.6 | 40.1 | 1.06 ± 0.10 |
./wain-pgo examples/brainfxxk.wasm |
23.5 ± 1.2 | 22.0 | 28.3 | 1.00 |
examples/mandelbrot.wasm
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/mandelbrot.wasm |
234.1 ± 5.5 | 227.3 | 248.3 | 1.09 ± 0.03 |
./wain-pgo examples/mandelbrot.wasm |
214.6 ± 3.0 | 211.1 | 220.4 | 1.00 |
examples/mt19937.wasm
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/mt19937.wasm |
74.4 ± 1.7 | 72.6 | 79.9 | 1.07 ± 0.03 |
./wain-pgo examples/mt19937.wasm |
69.7 ± 1.6 | 67.8 | 75.2 | 1.00 |
examples/n_queens.wasm
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/n_queens.wasm |
9.2 ± 0.6 | 8.5 | 12.9 | 1.06 ± 0.11 |
./wain-pgo examples/n_queens.wasm |
8.7 ± 0.6 | 8.0 | 12.1 | 1.00 |
examples/nbodies.wasm
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/nbodies.wasm |
131.1 ± 2.7 | 128.2 | 139.1 | 1.12 ± 0.03 |
./wain-pgo examples/nbodies.wasm |
117.3 ± 2.3 | 114.9 | 125.0 | 1.00 |
examples/pi.wasm
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/pi.wasm |
115.7 ± 2.8 | 113.2 | 124.6 | 1.04 ± 0.04 |
./wain-pgo examples/pi.wasm |
111.6 ± 2.7 | 109.2 | 120.3 | 1.00 |
examples/primes.wasm
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/primes.wasm |
2.6 ± 0.4 | 2.2 | 5.1 | 1.01 ± 0.21 |
./wain-pgo examples/primes.wasm |
2.6 ± 0.4 | 2.3 | 6.0 | 1.00 |
examples/quicksort.wasm
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/quicksort.wasm |
2.5 ± 0.4 | 2.0 | 5.4 | 1.00 |
./wain-pgo examples/quicksort.wasm |
2.5 ± 0.5 | 2.0 | 6.3 | 1.02 ± 0.25 |
examples/sqrt.wasm
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/sqrt.wasm |
2.6 ± 0.4 | 2.1 | 5.2 | 1.00 ± 0.20 |
./wain-pgo examples/sqrt.wasm |
2.6 ± 0.4 | 2.1 | 5.2 | 1.00 |
examples/brainfxxk.wat
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/brainfxxk.wat |
25.3 ± 1.0 | 24.0 | 29.2 | 1.08 ± 0.07 |
./wain-pgo examples/brainfxxk.wat |
23.4 ± 1.1 | 22.3 | 30.9 | 1.00 |
examples/mandelbrot.wat
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/mandelbrot.wat |
234.5 ± 5.0 | 229.0 | 244.1 | 1.08 ± 0.03 |
./wain-pgo examples/mandelbrot.wat |
217.2 ± 3.5 | 212.6 | 223.2 | 1.00 |
examples/mt19937.wat
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/mt19937.wat |
75.2 ± 1.5 | 73.6 | 81.1 | 1.06 ± 0.04 |
./wain-pgo examples/mt19937.wat |
70.6 ± 2.2 | 68.4 | 77.9 | 1.00 |
examples/n_queens.wat
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/n_queens.wat |
9.8 ± 0.7 | 9.0 | 12.3 | 1.05 ± 0.10 |
./wain-pgo examples/n_queens.wat |
9.3 ± 0.6 | 8.6 | 11.9 | 1.00 |
examples/nbodies.wat
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/nbodies.wat |
132.2 ± 3.2 | 128.0 | 139.8 | 1.12 ± 0.03 |
./wain-pgo examples/nbodies.wat |
117.9 ± 2.0 | 115.7 | 122.6 | 1.00 |
examples/pi.wat
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/pi.wat |
115.8 ± 2.8 | 112.6 | 122.0 | 1.04 ± 0.03 |
./wain-pgo examples/pi.wat |
111.3 ± 2.3 | 109.2 | 118.4 | 1.00 |
examples/primes.wat
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/primes.wat |
2.9 ± 0.4 | 2.5 | 6.1 | 1.01 ± 0.22 |
./wain-pgo examples/primes.wat |
2.9 ± 0.4 | 2.4 | 5.6 | 1.00 |
examples/quicksort.wat
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/quicksort.wat |
2.8 ± 0.4 | 2.4 | 6.2 | 1.00 |
./wain-pgo examples/quicksort.wat |
2.8 ± 0.4 | 2.3 | 6.6 | 1.00 ± 0.19 |
examples/sqrt.wat
Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
---|---|---|---|---|
./wain-orig examples/sqrt.wat |
2.9 ± 0.4 | 2.4 | 5.3 | 1.01 ± 0.19 |
./wain-pgo examples/sqrt.wat |
2.8 ± 0.4 | 2.4 | 5.3 | 1.00 |
ほぼ変わらないものもありますが,0〜10% ほど速くなっていることが分かります.実行対象がほぼ決まっているプログラムであれば,プログラムに直接手を入れること無く最大 10% の高速化が得られると考えるとかなりお得に見えます.
ただし,もちろん profile data 収集時に使わなかった入力では逆に遅くなる可能性もあるので,実運用時には profile data をどう上手く収集するかがカギの1つになりそうです.
ちなみに毎回手でコマンドを打つのが面倒だったので数十行のシェルスクリプトを書いてやってました.
https://gist.github.com/rhysd/1324f6726f8905f4e71c0f549cb35d97
実際に実行したコマンドを知りたい場合は参照してみてください
GitHub Action で Vim や Neovim を簡単にインストールできる action-setup-vim をつくった
今週ちまちまと git-messenger.vim や clever-f.vim の CI を GitHub Actions に移行していました.毎回 Vim プラグインの CI のために Vim や Neovim のセットアップを書くのが面倒なのと,Windows 上で Vim や Neovim を入れるのが(Powershell に不慣れなこともあり)大変だったので,GitHub Action として切り出すことにしました.
1ステップで Vim や Neovim を簡単にインストールできます.
使い方
下記のようにステップを書けば Vim または Neovim をインストールしてくれます.
macOS または Linux で安定版 Vim をインストール:
- uses: rhysd/action-setup-vim@v1
Windows では github-token
を input として与える必要があります.これは,vim-win32-installer から最新のリリースを持ってくる必要があり,そのために GitHub API を叩いているからです.
- uses: rhysd/action-setup-vim@v1 with: github-token: ${{ secrets.GITHUB_TOKEN }}
最新の Vim をインストールするには version: nightly
を input に指定します.
- uses: rhysd/action-setup-vim@v1 with: version: nightly github-token: ${{ secrets.GITHUB_TOKEN }}
Neovim をインストールするには neovim: true
を input に指定します.最新の stable の Neovim がすべての OS でインストールできます.Vim とは異なり,Windows 上でも github-token
input は不要です.
- uses: rhysd/action-setup-vim@v1 with: neovim: true
昨晩ビルドされたばかりの nightly の Neovim をインストールするには version: nightly
を指定すれば OK です.
- uses: rhysd/action-setup-vim@v1 with: neovim: true version: nightly
これらのステップを実行後,Vim をインストールした場合は vim
コマンドが,Neovim をインストールした場合は nvim
コマンドがそれぞれ利用可能になっているはずです.
また,action の executable
output としてインストールした Vim または Neovim の実行ファイルへのフルパスをセットしていて,それを使うこともできます.
例えば checkout@v2
で themis.vim をインストールし,action-setup-vim で Vim をインストールして単体テストを走らせる例は
# テストしたいプラグインを checkout - uses: actions/checkout@v2 # themis.vim を checkout - uses: actions/checkout@v2 with: repository: thinca/vim-themis path: vim-themis # Vim をインストール - uses: rhysd/action-setup-vim@v1 id: vim # プラグインの単体テストを themis.vim で実行 - name: Run unit tests with themis.vim env: THEMIS_VIM: ${{ steps.vim.outputs.executable }} run: | ./vim-themis/bin/themis ./test
実際に clever-f.vim のワークフロー で利用しています.
インストールされる Vim および Neovim の詳細
インストール元は OS と version
input によって下記のようになっています.優先度は
- システムのパッケージマネージャ
- 公式リリース
- ソースからビルド
となっています.ユーザ数や実行の速さを考慮してこうなっています.
Vim
OS | Version | Installation |
---|---|---|
Linux | stable |
gvim-gnome パッケージを apt でインストール |
Linux | nightly |
vim/vim リポジトリの HEAD をビルド |
macOS | stable |
brew install macvim で Homebrew からインストール |
macOS | nightly |
vim リポジトリの HEAD をビルド |
Widnows | stable |
Windows での公式安定版は無いので,Nightly と同じ |
Windows | nightly |
公式インストーラ repository のリリースからインストール |
Neovim
OS | Version | Installation |
---|---|---|
Linux | stable |
Neovim 公式の stable release からインストール |
Linux | nightly |
Neovim 公式の nightly release からインストール |
macOS | stable |
Homebrew を使って brew install neovim でインストール |
macOS | nightly |
Neovim 公式の nightly release からインストール |
Windows | stable |
Neovim 公式の stable release からインストール |
Windows | nightly |
Neovim 公式の nightly release からインストール |
制限
今のところ,特定のバージョンを指定してインストールはできません.技術的制約があるわけではないので,もし需要があればやるかもしれません. v1.1.0 で指定可能になりました.
# Vim v8.2.0126 を全ての OS でインストール.Windows でも github-token input は必要ありません - uses: rhysd/action-setup-vim@v1 with: version: v8.2.0126 # Neovim v0.4.3 を全ての OS でインストール - uses: rhysd/action-setup-vim@v1 with: neovim: true version: v0.4.3
また,GUI バージョンについては現状ではサポートしていませんが,リリース物に含まれる場合はインストールされます.具体的には下記の場合です:
まとめ
Vim または Neovim を簡単にインストールできる action action-setup-vim を作成しました. GitHub Action の Vim プラグイン CI への導入の敷居が個人的にぐっと下がったので,他のプラグインについても順次移行していきたいところ.
GitHub Actions の JavaScript Action を TypeScript で書いた
GitHub Action を TypeScript で作成したので,覚え書きがてらどうやって作ったかについて書きます. github-action-benchmark という Action をつくりました.
紹介記事:継続的にベンチマークを取るための GitHub Action をつくった
Action とは
今年9月に GitHub Action v2 がリリースされました.GitHub Action は GitHub が提供する CI/CD サービスです. 既存のサービスと大きく違う点は,処理を汎用的に Action として切り出して再利用できることです. 例えば,GitHub からのリポジトリのクローン actions/fetch や Node.js のセットアップ actions/setup-node などの基本的な実行ステップも Action として実装されています.
Action の種類
GitHub Action には JavaScript Action と Docker Action があります.下記のページに公式の解説があります.
https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-actions
- Docker Action: Docker コンテナを書くと,そのコンテナが GitHub Action のステップで実行されます
- Docker コンテナとして実行されるので,実行環境は Linux オンリーです
- ちょっとしたシェルコマンドをまとめた Action ならシェルスクリプトで書いて,簡単な Dockerfile を書いてエントリポイントから実行するだけで実現できます
- Docker コンテナのイメージに任意のツールや言語のランタイムを入れておけば,好きなツールや言語で Action がつくれます
- Action の入力は環境変数,出力は標準出力で行います
- 公式ヘルプ: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-a-docker-container-action
- JavaScript Action: Node.js のパッケージを書くと,そのパッケージが GitHub Action のステップとして実行されます
- Node.js で実行されるので,好きな npm パッケージがそのまま使えます
- 公式の npm パッケージ群があって,Action の入出力はライブラリ
@actions/*
で行えます.TypeScript に公式で対応しています - コンテナで実行されないので,Docker Action に比べて高速らしいです
- 公式ヘルプ: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-a-javascript-action
この記事では後者のみに絞ります.
Hello world
まず JavaScript で雛形を書きます.
Creating a JavaScript action を見て順番に試していけば良いので,ここで解説することは特にありません.
action.yml
を置き,npm init
して @actions/*
パッケージを入れ,index.js
を書きます.
次に npm install --save-dev typescript
して TypeScript コンパイラを入れ,index.js
を index.ts
に書き換え,tsconfig.json
を用意します.
{ "compilerOptions": { "module": "commonjs", "moduleResolution": "node", "preserveConstEnums": true, "noImplicitAny": true, "noImplicitReturns": true, "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, "noEmitOnError": true, "strictNullChecks": true, "target": "es2019", "sourceMap": true, "esModuleInterop": true, "resolveJsonModule": true }, "files": [ "index.ts" ] }
もしくは公式のテンプレートを使っても良いです.
公式の @actions/*
パッケージは TypeScript で書かれていて型定義を持っているのでこのままでほぼ問題ありませんが,残念ながら一発ではコンパイルが通りません.
@actions/github
が使っている @octokit/graphql
は v4 から TypeScript に対応しているのですが,@actions/github
が使っているのは v2 なので型定義がありません.
少し調べてみると,@actions/github
は @octokit/graphql
の型定義を自前で抱えていることが分かったので,v4 に対応するまではそこのものをコピーしてくるか,下記のように回避するための型定義ファイルを書いて自分のプロジェクトに含めておく必要がありました.
// Workaround for @actions/github until @octokit/graphql v4 is used declare module '@octokit/graphql' { type GraphQlQueryResponse = unknown; type Variables = unknown; }
最後にテスト用の workflow ファイルを更新します.TypeScript のコンパイルが必要になるので,リポジトリを checkout してきて node をセットアップし,npm install
して tsc
でコンパイルします.
実行する Action を ./
で始まる相対パスにしておくとローカルのアクションを実行してくれます.
ちなみに .
ではダメで ./
にする必要があって,少しハマりました.
runs-on: ubuntu-latest name: A job to say hello steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + - run: npm install + - run: ./node_modules/.bin/tsc -p . + - name: Hello world action step id: hello - uses: actions/hello-world-javascript-action@master + uses: ./ with: who-to-greet: 'Mona the Octocat' # Use the output from the `hello` step
後は自分の好きなように入力を定義し,処理を書いて結果を出力するだけなので,基本的に下記のドキュメントを見て進めれば OK です.
action.yml
の書き方: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/metadata-syntax-for-github-actions.workflow
ファイルの書き方: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions- actions/toolkit リポジトリの README
この記事では今回 github-action-benchmark を作成する際に引っかかったポイントや考えたポイントだけ書きます.
Action の入力
Action への入力は workflow ファイルでは with:
に key: value
で複数していします.JavaScript Action では @actions/core
パッケージの getInput(name: string): string
で値を取得できます.
少し注意する必要があるのは,入力は文字列型オンリーな点です.この入力には array や map が使えません.なので,例えば下記のようなリスト形式やキーバリュー形式の入力を定義することはできません.
uses: ./ with: awesome-array-option: - one - two - three awesome-map-option: key: value key2: value2
これは入力が Docker Action で環境変数 $INPUT_*
にマッピングされていることに由来していそうですが,理由は不明です.
リストを使いたい場合はどうすれば良いのかなといくつか公式 Action を調べてみると,
with: awesome-array-option: | one two three
のように改行区切りの文字列で指定させて Action 側で処理しているものがありました.
Action のデバッグ
GitHub Action の実行環境と同じ環境変数(例えば入力 foo-bar
は $INPUT_FOO_BAR
)を与えて node index.js
で実行するか,Action をプッシュしてテスト用の workflow の実行結果を見てデバッグすることになります.簡単なものであれば前者だけで事足りますが,最終的に GitHub Action で実行して結果を確認することになると思います.
@actions/core
の core.debug()
でデバッグ情報を埋め込んでおくのですが,これは(デバッグ情報なので当然ですが)デフォルトでは表示されません.
デバッグ情報は特定の secret に true
をセットすることで初めて表示されます.secret は本来リポジトリ固有の秘密の情報(API トークンとか)を保存する場所ですが,デバッグ情報の設定にも利用されています.リポジトリページ → Settings → Secrets タブ で設定できます.
ACTIONS_RUNNER_DEBUG
: workflow を実行する Runner の実行ログACTIONS_STEP_DEBUG
: 各ステップの実行ログ
Action のデバッグ情報を見るには後者が必要です.
Action のテスト
単体テスト
普通の npm パッケージと同じようにテストすれば OK です. GitHub Action と同じ環境変数を与えてテストを実行すれば OK です.
github-action-benchmark では単体テスト中に git
コマンドを実行されると困るのと,@actions/core
の実装のロジックに依存するのが嫌だったので @actions/core
を mock-require で mock してしまってます.
真面目にやるならテスト用の Git リポジトリを作ってその中でテストケースを走らせるべきですが,その辺はサボってます.
E2Eテスト
実際に Action を走らせて期待した動作になっているかは GitHub Action で走らせて結果を見るのが一番良いと思います. github-action-benchmark では CI 用の workflow の中で action をビルドして実行し,実行後に期待した状態になっているかをスクリプトでチェックしています.
これだと「正しく fail するか」をテストすることができない(action が fail するとその時点で workflow が止まってしまうので)のですが,どうやれば良いかはまだよく分かっていないです…(誰かご存知なら教えてもらえると嬉しいです)
Action の公開
Action は GitHub の公開リポジトリに置いておくと即使えるようになります.例えば user/foo-action
というリポジトリをつくると
name: user/foo-action@ref
のように書くだけで使えるようになります.ここで ref
は Git リポジトリの ref で,ブランチ名やタグ名,コミットハッシュなどが使えます.
なので,この ref
で Action のバージョンを制御するのですが,セマンティックバージョニングをサポートするような仕組みは無いので,自前でルールを定める必要があります.
github-action-benchmark ではセマンティックバージョニングに従ってリリースすることを決めました.
rhysd/github-action-benchmark@v1
は v1.x.y の最新を表すrhysd/github-action-benchmark@v1.1.0
は v1.0.0 に固定することを表す- マイナーバージョンまでの固定や範囲指定はサポートしない(必要性を感じなかったので)
としました.
また,JavaScript Action は Action のリポジトリを checkout した時点で即 node .
で実行できるようになっていないといけないので,TypeScript や babel を使っている場合は事前にコンパイルした JavaScript コードをリポジトリに置いておく必要があります.
また,Action 実行時に npm install
も行わないので node_modules
も丸ごと置いておく必要があります.native extension を利用しているパッケージが node_modules
に含まれている時にどうすれば良いのかはよく分かりません…
master
ブランチにビルドした生成物と依存パッケージを直置きしたくなかったので,下記のように運用することにしました
- 各メジャーバージョンごとに orphan ブランチを切る.例えば v1.x.y 向けは
v1
ブランチ - 新しいバージョンのリリースは対応するメジャーバージョンのブランチで行う.例えば現在の
master
ブランチをv1.0.2
にリリースする場合は,まずはmaster
ブランチで TypeScript コードを JavaScript にコンパイルし,v1
ブランチに切り替えてそれらを add して commit し,v1.0.2
タグをつけて push します - 次のメジャーバージョン(例えば
v2
)をリリースする時は前バージョンの開発用ブランチ(例えばdev/v1
)を作っておいて v1.x.y 向けの修正はこちらからv1
ブランチに入れる
ちなみに公式の推奨では release/v1
ブランチをつくって v1
タグを毎回最新に張り替えることを推奨しています.これはリリース前に release/v1
上でテストを行う前提なんだと思いますが,毎回張り替えないといけないのと,master
でテストしてから v1
ブランチに成果物を置く前提だったのでブランチ名を v1
としています.
ちなみに master
→ v1
へは簡単なスクリプトを使って成果物を置いてます.
https://github.com/rhysd/github-action-benchmark/blob/master/scripts/prepare-release.sh
Action で発行される GitHub API トークンの注意
GitHub Action では実行ごとに GitHub API のトークンが自動生成され secrets.GITHUB_TOKEN
に格納されます.基本的には普通の API トークンのように使えるのですが,どうやらパーソナルアクセストークンとは異なりイベントを起こす権限を持っていないようです.
例えば GitHub Pages のブランチに push することはできますが,GitHub Pages のデプロイをすることはできません.
この制限のため,github-action-benchmark ではパーソナルアクセストークンが必要になってしまっています.
ちなみにこの制限はなぜか public repo のみで,private repo では問題なく GitHub Pages のデプロイができました.