Rubyのテストは、素のままですと直列実行なので、テストケース数が増えると実行時間も増えていきます。
CIの実行環境は、マルチプロセッサを選択できることがあるため、テストも並列実行できると、時間短縮を狙うことができます。
Rubyでは parallel_tests というgem越しに、めぼしいテストフレームワークを並列実行することができます。
ところで bin/parallel_test -n 4
のように並列数を指定すると4並列で動かしてくれますが、指定しなかった場合は、どうやら実行環境のプロセッサの数を、どこかから取ってきて動いているようです。README.md を眺めてもよくわからなかったので、ソースを読んでみました。
手元に持ってくる
この記事を書いてる時点の最新版 v3.1.0 を読んでみます。
- https://github.com/grosser/parallel_tests/releases/tag/v3.1.0
- https://github.com/grosser/parallel_tests/tree/v3.1.0
ghq get git@github.com:grosser/parallel_tests.git
cd parallel_tests
git checkout -b tag3.1.0 refs/tags/v3.1.0
rake 越しの実行の場合
このように実行することで、テスト用に create database
と、RSpecを実行してくれます。そのときの並列数は、実行環境のプロセッサの数らしいです。4コアなら4並列です。
bundle exec rake parallel:setup
bundle exec rake parallel:spec
[]
に数字を入れると、実行環境のプロセッサ数のことは忘れて、指定した並列数で実行してくれます。
bundle exec rake parallel:setup[6]
bundle exec rake parallel:spec[6]
これが parallel_tests
のソースコードでは、 :count
というパラメータを取るRakeタスクとして定義されています。
- https://github.com/grosser/parallel_tests/blob/v3.1.0/lib/parallel_tests/tasks.rb#L99-L102
- https://github.com/grosser/parallel_tests/blob/v3.1.0/lib/parallel_tests/tasks.rb#L180-L208
parallel:setup
はさらに run_in_parallel
に渡して、 bin/parallel_test
を呼ぶコマンドライン文字列を組み立てています。
このとき :count
を渡していれば、 -n
を付けるようにしています。しかし -n
を付けなかった場合に、どのように決まるかは不明です。
parallel:spec
も同様で、なんなら :count
を渡していようがいまいが、 -n
を付けています。 :count
を渡さなかった場合は -n ""
となりそうですし、 -n
を付けなかった場合に、どのように決まるかは不明です。
Rakeタスクは、パラメータとして並列数を受けとっていればその数を bin/parallel_test
に渡し、受けとっていなければ bin/parallel_test
に判断させているようです。
bin/parallel_test
CLI に丸投げしていますね
実体はこれで、並列数に関心を集中して読むと、
コマンドライン引数が最も強く、環境変数 $PARALLEL_TEST_PROCESSORS
、Parallel.processor_count
の順に、並列数の取得元としているようです。
-n ""
と渡ってきても、以下でイイかんじに対処できてそうに読めます。
Parallel.processor_count
Parallel.processor_count
は、 parallel_tests
を漁っても存在しておらず、 parallel
に定義されているようです。
gem parallel を漁ると、環境変数 $PARALLEL_TEST_PROCESSORS
、 Etc.nprocessors
の順に、並列数の取得元としているようです。
Etc.nprocessors
Etc.nprocessors
は etc
というライブラリに定義されてそう。
Etc.nprocessors
でググると、Rubyの標準ライブラリのひとつとして実装されていることがわかります。
- https://www.google.com/search?q=Etc.nprocessors
- https://docs.ruby-lang.org/ja/latest/method/Etc/m/nprocessors.html
- https://www.rubydoc.info/stdlib/etc/Etc.nprocessors
2.7.0 で読んでみます。
ghq get git@github.com:ruby/ruby.git
cd ruby
git checkout -b tag2_7_0 refs/tags/v2_7_0
どこに実装があるのか何もわからねえな...とりあえず ripgrep で漁ってみます。
rg nprocessors
doc/ChangeLog-2.2.0
2564: * ext/etc/etc.c (etc_nprocessors_affin): maximum "n" should be 16384.
2568: * ext/etc/etc.c (etc_nprocessors_affin): minor spell fix.
2572: * ext/etc/etc.c (etc_nprocessors_affin): optimize memory usage a
2924: * ext/etc/etc.c (etc_nprocessors_affin): Test CPU_ALLOC availability.
2929: * ext/etc/etc.c (etc_nprocessors_affinity): use sched_getaffinity
2932: * ext/etc/etc.c (etc_nprocessors): use etc_nprocessors_affinity if
2935: [Feature #10267] etc-nprocessors-kosaki2.patch
3539: * ext/etc/etc.c (etc_nprocessors): Windows support.
3544: * ext/etc/etc.c (etc_nprocessors): New method.
test/etc/test_etc.rb
167: def test_nprocessors
168: n = Etc.nprocessors
ext/etc/etc.c
927:etc_nprocessors_affin(void)
992: * p Etc.nprocessors #=> 4
1000: * linux$ taskset 0x3 ./ruby -retc -e "p Etc.nprocessors" #=> 2
1004:etc_nprocessors(VALUE obj)
1013: ncpus = etc_nprocessors_affin();
1033:#define etc_nprocessors rb_f_notimplement
1092: rb_define_module_function(mEtc, "nprocessors", etc_nprocessors, 0);
doc/NEWS-2.2.0
162: * Etc.nprocessors
lib/bundler/installer.rb
223: Etc.nprocessors
spec/ruby/library/etc/nprocessors_spec.rb
4:describe "Etc.nprocessors" do
6: Etc.nprocessors.should be_kind_of(Integer)
7: Etc.nprocessors.should >= 1
spec/mspec/lib/mspec/utils/script.rb
256: [Etc.nprocessors, max].min
https://github.com/ruby/ruby/tree/v2_7_0/ext/etc に転がっているものを眺めてみます。
-
https://github.com/ruby/ruby/blob/v2_7_0/ext/etc/etc.c#L1092 で、Rubyコード内で
Etc.nprocessors
としてetc_nprocessors
を利用できるよう定義されている。 -
https://github.com/ruby/ruby/blob/v2_7_0/ext/etc/etc.c#L979-L1034 が
etc_nprocessors
の実体- Winwos なら
GetSystemInfo()
https://github.com/ruby/ruby/blob/v2_7_0/ext/etc/etc.c#L1027-L1028 から取得している - Linuxなら
sched_getaffinity()
https://github.com/ruby/ruby/blob/v2_7_0/ext/etc/etc.c#L961 から取得している - Linuxで
sched_getaffinity()
が使えなければsysconf(_SC_NPROCESSORS_ONLN)
https://github.com/ruby/ruby/blob/v2_7_0/ext/etc/etc.c#L1021 から取得している
- Winwos なら
parallel_test
は Linux な CodeBuild で動かしているので、 Linux に関心を絞ります。
sched_getaffinity
これは Linux のシステムコールで、C関数として利用できるよう定義されています。テキトーなLinuxディストリビューションで man sched_getaffinity
するとカーネルのマニュアルが出てきます。
- https://man7.org/linux/man-pages/man2/sched_getaffinity.2.html
- http://manpages.ubuntu.com/manpages/bionic/ja/man2/sched_setaffinity.2.html
たいへんありがたいことに解説記事があります。
- https://qiita.com/kubo39/items/dec96d7c93a50a310d7e#ruby-1
- https://qiita.com/masami256/items/47163fefed7c1e337dec
まとめ
-
parallel_test
は、単独では実行環境のCPU数を取得していない。Parallel.processor_count
から取得している -
Parallel.processor_count
は Ruby 標準ライブラリ etc のEtc.nprocessors
から取得している -
Etc.nprocessors
は、Linux ならシステムコールsched_getaffinity()
が使えなければsysconf(_SC_NPROCESSORS_ONLN)
から取得している