追記(2018/08/20)
本記事で紹介したワークアラウンドを適用済みのElm platformおよびelm-test CLIをグローバルにインストール済みのdocker imageを作成しました。
このイメージを使えば、CircleCIなどのdocker-based CIでなら、checkout後即elm-test
でテストできます。
本記事の前提とは異なりグローバルインストールなので、package.json
を用意していろいろ書く必要もなくなります。
Elm packageを開発・CIする場合にはかなり便利だと自負しておりますので使ってみてください。Alpine Linuxベースでイメージサイズも小さめです。
- Elm 0.18
- node-test-runner 0.18.12 (elm-testを実際に実行するためのnpm package)
- CircleCI 2.0
- 本記事の内容はElmの薄い本にもチョロっと書いてあったが、Travisでも有効で、高速化に意味があることを確認した。
- Issueの方はまだcloseされておらず、根本対策もまだ決まってない様子
前提
Elmはローカルインストールするものとする。つまりnode_modules/以下にnpm
でelm platformを落としてくる。1yarn
を使っていても同様。ここではElm packageを開発するようなケースを考え、具体的なwebアプリをビルドするためのwebpack等の存在は前提としないが、存在していてもテスト部分に関しては基本的に同じのはず。
package.jsonはこんな感じになる。
{
...
"devDependencies": {
"elm": "^0.18.0",
"elm-test": "^0.18.10"
},
"scripts": {
"test": "elm-test",
},
...
}
Elm特有のコンパイル生成物がたくさんあるので、それらはCI環境ではキャッシュしたい。elm-stuff/がメインだが、elm-testではtests/以下にもう一つのサブElmプロジェクトが作られるので、そちらのelm-stuff/もキャッシュする。elm-stuff/build-artifacts/に絞ってもいいのだが、この辺は若干調査不足なのでどうするのがベストなのかは未確認。
これらを考えるとCIの設定は以下のような感じ(Travisの例):
sudo: false # dockerコンテナ内で実行される。立ち上がりが速い
os:
- linux # TravisのOSXイメージはたいていおっそいので省いている
language: node_js
node_js:
- "8" # LTSをターゲットにしてるだけで、深い意味はない。6と8の2ジョブでもいい
cache:
directories:
- "node_modules"
- "elm-stuff"
- "tests/elm-stuff"
が、この設定ではElmソースのコンパイルが妙に遅いことが知られている。具体的にはelm-make部分(elm-compiler)がやたらと遅い。
Elm compilation is incredibly slow on CI platforms · Issue #1473 · elm-lang/elm-compiler
問題はこのIssueで報告されている。色々確認を挟んだあと、
obmarg commented on 3 Sep 2016
It could be the case that travis & circle are reporting way more cores than are actually usable. I just checked /proc/cpuinfo in both environments, and they list 32 cores. The travis documentation specifically says you'll have 2 cores for your builds. I can't find any documentation for circle, but I'm pretty sure I don't have exclusive access to all 32 of those cores.
/proc/cpuinfo
と同等の情報をソースにしてcpuコア数を判定していると、CI環境上では実際に利用可能な量より大幅に過大評価されるので、それが原因になるのではないか、という指摘がなされた。
例えばTravisCIでは、
どの環境でも同時に使用可能なコア数は2までに制限されている(CircleCIでは同等の情報が見当たらなかった)
結局この指摘は以下の確認によりビンゴだった模様。Evanもconfirmしている。
I've been doing a bit of work to try and confirm that this false number of CPUs is actually causing this problem. I had a look into how the getNumProcessors works, and discovered [libsysconfcpus][sysconfcpus], which lets you override the number of CPUs reported by sysconf (which getNumProcessors uses under the hood).
I then built & ran that on my CI environment:
$ rm -R elm-stuff/build-artifacts/*
$ time sysconfcpus -n 1 elm-make
Success! Compiled 47 modules.
real 0m2.215s
user 0m2.195s
sys 0m0.024s
$ rm -R elm-stuff/build-artifacts/*
$ time elm-make
Success! Compiled 47 modules.
real 9m21.660s
user 15m38.880s
sys 2m47.578s
> So it does look like the CPU count detection is the problem. Seems like a command line option (or similar) might be a reasonable idea?
この問題に関するelm−compilerの修正(コア数を手動設定するオプションなど)はまだ導入されてはいない模様。[そもそも、使用可能なコアが実際にたくさんある場合(過大報告されているというわけではない場合)もパフォーマンスが出ていない][fund]という報告もあるので、根は深い可能性がある。
[fund]: https://github.com/elm-lang/elm-compiler/issues/1473#issuecomment-337823146
現状では、上記の確認でも使われている**[sysconfcpus][sysconfcpus]を利用して、elm-compilerが使用するコア数が少なくなる(2コア以下になる)ようだまくらかしてやる**というワークアラウンドが広く使われているので、Elm開発者は少なくとも0.18の間はこの対策をしておくと**クラウドCI環境でのビルド/テストが数倍以上高速化する**。
[sysconfcpus]: http://www.kev.pulo.com.au/libsysconfcpus/
# 高速化設定のサンプル(Travis & Circle CI)
以下のGistに、本記事の前提で上記Issueで紹介されているワークアラウンドを適用するスクリプトをまとめた。(筆者のElmレポジトリで実際に使用中)
https://gist.github.com/ymtszw/bfef21f2297ec08041a45953538f417f
本質的な部分はreplace_elm_make.shで、**elm-makeをsysconfcpusを噛ませて呼ぶようにする**ことで、elm-makeを経由するコンパイル全てにワークアラウンドを適用する。
解説がてら現在のバージョンをダンプする。
## ensure_libsysconfcpus.sh
```sh:ensure_libsysconfcpus.sh
#!/usr/bin/env bash
set -eu
cwd=$(pwd)
if [ ! -d sysconfcpus/bin ]; then
git clone https://github.com/obmarg/libsysconfcpus.git
pushd libsysconfcpus
./configure --prefix="${cwd}/sysconfcpus"
make && make install
popd
fi
-
./configure
の--prefix
は絶対パスを渡す必要があり、TravisでもCircleでも確実にCWDを絶対パスで手に入れる苦肉の策としてpwd
を使った。もっといい案があったらこっそり教えて下さい。- はじめ
$TRAVIS_BUILD_DIR
および$CIRCLE_BUILD_DIRECTORY
を引数経由で与えるやり方でやろうとしていたが、CircleCIでworking_directory
が~/repo
のようになっていると、configure
がtildeを展開できず、エラーになる。どっかで確実に展開されるよう工夫すればいけるかも。
- はじめ
replace_elm_make.sh
#!/usr/bin/env bash
# replace normal elm-make with sysconfcpus-prefixed elm-make
# epic build time improvement - see https://github.com/elm-lang/elm-compiler/issues/1473#issuecomment-245704142
set -euo pipefail
ncore=${1:-1}
if ! grep "sysconfcpus -n ${ncore}" "$(npm bin)/elm-make"; then
if [ ! -f "$(npm bin)/elm-make-old" ]; then
mv "$(npm bin)/elm-make" "$(npm bin)/elm-make-old"
fi
cat << EOF > "$(npm bin)/elm-make"
#!/usr/bin/env bash
set -eu
echo "Running elm-make with sysconfcpus -n ${ncore}"
$(pwd)/sysconfcpus/bin/sysconfcpus -n ${ncore} "$(npm bin)/elm-make-old" "\$@"
EOF
chmod +x "$(npm bin)/elm-make"
fi
-
npm bin
はnode_modules/.bin/
への絶対パスを返す。 - node_modules/はキャッシュされるので、このスクリプトはべき等である必要がある。
grep
で現在のnode_modules/.bin/elm-makeの中身を調べたり、elm-make-oldの存在をチェックしたりしているのはそのため。
.travis.yml.sample
sudo: false
os:
- linux
language: node_js
node_js:
- "6"
- "8"
cache:
directories:
- "sysconfcpus"
- "node_modules"
- "elm-stuff"
- "tests/elm-stuff"
before_install:
- ./scripts/ci/ensure_libsysconfcpus.sh
before_script:
- ./scripts/ci/replace_elm_make.sh 2
-
install
とscript
を明示的にnpm install
とnpm test
のように指定しておいてもいいが端折っている。 - nodeのLTSに対してビルドしているのは筆者の主義。
- Travisでは2コア使えるはず。
.circleci/config.yml.sample
# .circleci/config.yml for CircleCI 2.0
version: 2
jobs:
node6:
docker:
- image: circleci/node:6
working_directory: ~/repo
steps:
- checkout
- restore_cache:
keys:
- v6-dependencies-{{ checksum "package.json" }}-{{ checksum "elm-package.json" }}
- v6-dependencies-
- run: ./scripts/ci/ensure_libsysconfcpus.sh
- run: npm install
- run: ./scripts/ci/replace_elm_make.sh 1
- run: npm test
- save_cache:
paths:
- sysconfcpus
- node_modules
- elm-stuff
- tests/elm-stuff
key: v6-dependencies-{{ checksum "package.json" }}-{{ checksum "elm-package.json" }}
node8:
docker:
- image: circleci/node:8
working_directory: ~/repo
steps:
- checkout
- restore_cache:
keys:
- v8-dependencies-{{ checksum "package.json" }}-{{ checksum "elm-package.json" }}
- v8-dependencies-
- run: ./scripts/ci/ensure_libsysconfcpus.sh
- run: npm install
- run: ./scripts/ci/replace_elm_make.sh 1
- run: npm test
- save_cache:
paths:
- sysconfcpus
- node_modules
- elm-stuff
- tests/elm-stuff
key: v8-dependencies-{{ checksum "package.json" }}-{{ checksum "elm-package.json" }}
workflows:
version: 2
build:
jobs:
- node6
- node8
-
npm test
の中でtests/ディレクトリ以下のコンパイルも走るので、save_cache
はsteps
の最後に持っていった。 - CircleCIでは何コア使えるのかがわからないので、とりあえず1。
- cache keyにchecksumを使える便利な機能があるので、"package.json"と"elm-package.json"双方をソースに使用した。
- この2つに含まれる情報を前提に"tests/elm-package.json"が生成されることを考えると、"tests/elm-package.json"のchecksumは不要。
結果
最近書いたymtszw/elm-xml-decodeのビルドで試した。doctestのコード生成もしているので、elm-testのみの場合と比べると平均して遅いが、かなり効果は出ている。
TravisCI
適用前(6分前後):
適用後(2分前後):
3倍くらい速くなった。が、Travisはそもそもコンテナの立ち上がりが結構遅い。立ち上がってからはまあ速い。結構分散が大きい。
CircleCI
適用前(2分前後):
適用後(40秒前後):
2〜3倍速くなった。CircleCI 2.0のほうがコンテナの立ち上がりは圧倒的に速い。立ち上がってからのパフォーマンスはちょっと読めないところがあるが、平均してTravisよりだいぶ速い。
いずれにせよこのワークアラウンドによってElmのビルド・テストは相当速くなるので、0.18の間はやっておきましょう。
-
これ自体はrequirementってわけではなく、CI環境のユーザ用にElmをglobalインストールしても同じようなことはできる。スクリプトは適宜書き換えが必要。 ↩