背景
Railsで作ったGUIのアプリをブラウザ自動化テストする際、CI環境のCPU使用率が高いことが気になっていました。複数のテストが並行した場合に、不安定になってどれか失敗してしまうということも起きました(※CPU使用率との因果関係は不明ですが)。
テストは RSpec + Capybara + Selenium + Headless Chrome で作ってあります。chromeの起動オプションでCPU使用率を節約できるものがないか調べたりしたのですが、効果のあるものは見つけられませんでした。
色々探していたら、CapybaraにもCPU使用率が上がってしまう要因があることを知りました。
待っているとPCのファンが唸りを上げてしまう原因と対策
次のように書くと内部では 0.01 のウェイトが入っているため処理回数は1秒間で約100回、全体で約6000回になる
Capybara.has_selector?(..., wait: 60)
値 0.01
はハードコードされていて外からは変更できません。上の記事には真っ当な対策が載っていますが、既に作り込んでいるテストコードをあちこち書き換えるのは大変です。そこで応急処置的にインストールしたCapybaraのソースコードを書き換えることにしました。CI環境であれば基本的に毎回作り直すので、中身を壊してしまっても影響はまずありません。
【11/4追記】Capybaraの該当箇所を設定可能にするPullRequestを取り込んでもらえ、 3.38.0 でリリースされました。詳細については記事末尾をご覧ください。
Capybara.default_retry_interval = 0.25
Capybaraの内容確認
javascriptを活用した動的なwebページだと、テスト時は要素が現れる(or消える)まで待つといった対応がよく必要になります。Capybaraは指定条件が満たされないときに自動的に指定時間リトライしてくれるのですが、その頻度が多いようです。
Capybaraのメソッド Capybara::Node::Base#synchronize
の中に、 sleep(0.01)
という記述があります。これがリトライの間隔です。
ここを例えば sleep(0.25)
に変えれば、チェック回数は 0.01/0.25 = 1/25 に激減します。 0.25 でも1秒間に約4回チェックすることになるので、数秒間しか表示されない要素であっても見逃すことは無いと思います。
書き換え方法
CIで実行するために、シェルでsedコマンドを使ってソースコードを書き換えます。gemをbundlerで管理していれば、インストール先のディレクトリはコマンドで出力できます。
bundle install
# "sleep" を含む行にある "0.dddd" という形の数値を "0.25" に置き換える
sed -i.bk -e '/sleep/s/0\.[0-9]*/0.25/' $(bundle show capybara)/lib/capybara/node/base.rb
# "sleep" 周辺の行を表示する
grep -n -C3 sleep $(bundle show capybara)/lib/capybara/node/base.rb
# 差分を表示する(CIが失敗しないよう終了ステータスは無視)
diff -u $(bundle show capybara)/lib/capybara/node/base.rb{.bk,} || true
結果
今回実際に困っていたテストコードに適用したところ、CI環境のCPU使用率が元の 3/4 に減りました。目に見えるほど効果が出るとは思っていなかったため驚きました(待ちの多いテストの作りだったのかもしれません)。素直に考えると、これまでと同じCPU使用率で1.3倍並行してテストを実行できることになります。
手元のPC上で、もっと単純なただ待つだけのコードで実験すると、 ruby, chromedriver, chrome の各プロセスでCPU使用率が 1/10 程度にまで減りました。
サンプルコード
Ruby: 3.1.2
source 'https://rubygems.org'
gem 'capybara', '~> 3.35.0'
gem 'selenium-webdriver'
gem 'matrix'
require 'capybara/dsl'
require 'selenium-webdriver'
#--- configure ---#
Capybara.register_driver :selenium do |app|
options = Selenium::WebDriver::Chrome::Options.new
options.add_argument('--headless')
Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
end
Capybara.default_driver = :selenium
Capybara.javascript_driver = :selenium
#--- main ---#
include Capybara::DSL
visit 'http://example.com/'
find('#dummy-id', wait: 60)
【追記】設定可能になりました。
コメントに後押しいただき、このリトライ間隔を設定可能にするissue/PRをCapybaraのリポジトリに出したところ、無事に取り込んでもらえました。
Make retry interval configurable #2577 - teamcapybara/capybara
バージョン 3.38.0 からは、sedやモンキーパッチなどで無茶をしなくてもテストコード側で以下のように設定できます。
Capybara.default_retry_interval = 0.25
# または
Capybara.configure do |config|
config.default_retry_interval = 0.25
end