parallel_tests が並列数を決定する仕組み

Rubyのテストは、素のままですと直列実行なので、テストケース数が増えると実行時間も増えていきます。

CIの実行環境は、マルチプロセッサを選択できることがあるため、テストも並列実行できると、時間短縮を狙うことができます。

Rubyでは parallel_tests というgem越しに、めぼしいテストフレームワークを並列実行することができます。

* https://github.com/grosser/parallel_tests

ところで `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並列です。

```bash
bundle exec rake parallel:setup
bundle exec rake parallel:spec 
```

`[]` に数字を入れると、実行環境のプロセッサ数のことは忘れて、指定した並列数で実行してくれます。

```bash
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` を呼ぶコマンドライン文字列を組み立てています。

* https://github.com/grosser/parallel_tests/blob/v3.1.0/lib/parallel_tests/tasks.rb#L32

このとき `:count` を渡していれば、 `-n` を付けるようにしています。しかし `-n` を付けなかった場合に、どのように決まるかは不明です。

* https://github.com/grosser/parallel_tests/blob/v3.1.0/lib/parallel_tests/tasks.rb#L32

`parallel:spec` も同様で、なんなら `:count` を渡していようがいまいが、 `-n` を付けています。 `:count` を渡さなかった場合は `-n ""` となりそうですし、 `-n` を付けなかった場合に、どのように決まるかは不明です。

* https://github.com/grosser/parallel_tests/blob/v3.1.0/lib/parallel_tests/tasks.rb#L202

Rakeタスクは、パラメータとして並列数を受けとっていればその数を `bin/parallel_test` に渡し、受けとっていなければ `bin/parallel_test` に判断させているようです。

# bin/parallel_test

CLI に丸投げしていますね

* https://github.com/grosser/parallel_tests/blob/v3.1.0/bin/parallel_test#L9

実体はこれで、並列数に関心を集中して読むと、

* https://github.com/grosser/parallel_tests/blob/v3.1.0/lib/parallel_tests/cli.rb#L16-L17

コマンドライン引数が最も強く、環境変数 `$PARALLEL_TEST_PROCESSORS` 、`Parallel.processor_count` の順に、並列数の取得元としているようです。

* https://github.com/grosser/parallel_tests/blob/v3.1.0/lib/parallel_tests.rb#L15-L21

`-n ""` と渡ってきても、以下でイイかんじに対処できてそうに読めます。

* https://github.com/grosser/parallel_tests/blob/v3.1.0/lib/parallel_tests.rb#L20

# Parallel.processor_count

`Parallel.processor_count` は、 `parallel_tests` を漁っても存在しておらず、 `parallel` に定義されているようです。

* https://github.com/grosser/parallel_tests/blob/v3.1.0/Gemfile.lock#L57
    * https://github.com/grosser/parallel/blob/v1.19.2

gem parallel を漁ると、環境変数 `$PARALLEL_TEST_PROCESSORS` 、 `Etc.nprocessors` の順に、並列数の取得元としているようです。

* https://github.com/grosser/parallel/blob/v1.19.2/lib/parallel/processor_count.rb#L5-L9

# Etc.nprocessors

`Etc.nprocessors` は `etc` というライブラリに定義されてそう。

* https://github.com/grosser/parallel/blob/v1.19.2/lib/parallel/processor_count.rb#L1

`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 で読んでみます。

* https://github.com/ruby/ruby/tree/v2_7_0

```bash
ghq get git@github.com:ruby/ruby.git
cd ruby
git checkout -b tag2_7_0 refs/tags/v2_7_0
```

どこに実装があるのか何もわからねえな...とりあえず ripgrep で漁ってみます。

```bash
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 から取得している

`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)` から取得している