ChromeとHeadless Chromeの速さを比べてみた

この記事は[Selenium/Appium Advent Calendar 2017](https://qiita.com/advent-calendar/2017/selenium)の16日目の記事です。

業務で自動システムテスト(E2Eテスト)の運用・開発を行っています。
数年ほど運用を続けていて、ブラウザはfirefox、IE、Chrome、OSもLinux、Windows、macOSと色々と試してきました。
どのブラウザ、どのOSでテストするかは必要となるテスト内容によって変わってきますよね。

本記事では[Headless Chromium](https://chromium.googlesource.com/chromium/src/+/master/headless/README.md)の導入調査をした際の内容をまとめようと思います。
Headlessに求めるものはやはりスピードですよね。
導入調査ではHead有り・無しのブラウザ操作時の速度を計測してみました。

# はじめに結論 :point_up:
## 計測結果からわかったこと
- 計測したすべての操作でheadless chromeのほうが速い
- 特にブラウザの起動は1/3近く短縮できる

## 調査してみて思ったこと
- テストコードを実装するときはheadありのほうがデバッグしやすい
  - ただし、プロダクトの開発に合わせてテスト実行する時はheadless、テストコードのメンテや新規追加する時はheadありにしたりすると結果に違いが出るかもしれないので注意が必要
- headlessでできないseleniumによる操作がありそうなのでその点は更に調査が必要

-----------------------------------------------------
-- 以下調査の詳細 -------------------------------------

# 準備 :pick:
以下の3つを準備
1. 最低限のCRUD機能を持つサンプルWebアプリケーション(テスト対象がWebアプリのため)
2. Webアプリのデータを登録するDB
3. Selenium実行環境(SeleniumServerとChromeを起動する環境)

構築・破棄、コードでの変更管理が楽なので、dockerコンテナで構築しました。(本題ではないので、手順は最後に記載します)
計測用のrubyスクリプトを実行し、サンプルウェブアプリケーションに対してseleniumでユーザ操作を再現します。

![measure_headless_chrome (1).png](https://qiita-image-store.s3.amazonaws.com/0/10800/f91f1c9d-9e7b-1882-2171-ccaac9c38da9.png)


# 計測 :stopwatch:

速くなる点が以下だと推測。その点に着目して計測しました。

1. ページの表示
2. テキスト入力
3. フォームの送信
4. ブラウザ起動

## 計測用のスクリプト作成

１) selenium-webdriverライブラリをインストール
 
```bash:bundlerをインストール
gem install bundler
```
```bash:gemライブラリの準備&install
mkdir headless_benchmark
cd headless_benchmark
bundle init
cat << EOS >> Gemfile
gem 'selenium-webdriver'
EOS
bundle install --path ./bundle
```
２) 計測用のrbファイルを作成

```ruby:headless_benchmark.rb
#!/usr/bin/env ruby

require 'benchmark'
require 'selenium-webdriver'

SELENIUM_URL = 'http://localhost:4445/wd/hub'.freeze

def create_headless_driver
  caps = Selenium::WebDriver::Remote::Capabilities.chrome("chromeOptions" => {args: ["--headless", "--disable-gpu", "window-size=1280x800"]})
  Selenium::WebDriver.for :remote, url: SELENIUM_URL, desired_capabilities: caps
end

def create_driver
  Selenium::WebDriver.for :remote, url: SELENIUM_URL, desired_capabilities: :chrome
end

APP_TOP = 'http://web:3000/users'.freeze

Benchmark.bmbm 10 do |r|

  # ページの表示
  r.report 'headless: load page:' do
    driver = create_headless_driver
    30.times do |num| 
      driver.navigate.to APP_TOP
    end
    driver.quit
  end

  r.report 'not headless: load page:' do
    driver = create_driver
    30.times do |num|
      driver.navigate.to APP_TOP
    end
    driver.quit
  end

  # テキスト入力
  r.report 'headless: input text:' do
    driver = create_headless_driver
    driver.navigate.to "#{APP_TOP}/new"
    30.times do |num|
      elem_name = driver.find_element(:css, '#user_name')
      elem_name.send_keys 'name name'
      elem_email = driver.find_element(:css, '#user_email')
      elem_email.send_keys 'user@email.com'
      elem_name.clear
      elem_email.clear
    end
    driver.quit
  end

  r.report 'not headless: input text:' do
    driver = create_driver
    driver.navigate.to "#{APP_TOP}/new"
    30.times do |num|
      elem_name = driver.find_element(:css, '#user_name')
      elem_name.send_keys "name name"
      elem_email = driver.find_element(:css, '#user_email')
      elem_email.send_keys 'user@email.com'
      elem_name.clear
      elem_email.clear
    end
    driver.quit
  end

  # フォームの送信
  r.report 'headless: submit:' do
    driver = create_headless_driver
    30.times do |num|
      driver.navigate.to "#{APP_TOP}/new"
      elem_name = driver.find_element(:css, '#user_name')
      elem_name.send_keys "name name #{num}"
      elem_email = driver.find_element(:css, '#user_email')
      elem_email.send_keys "user#{num}@email.com"
      elem_email.submit
    end
    driver.quit
  end

  r.report 'not headless: submit:' do
    driver = create_driver
    30.times do |num|
      driver.navigate.to "#{APP_TOP}/new"
      elem_name = driver.find_element(:css, '#user_name')
      elem_name.send_keys "name name #{num}"
      elem_email = driver.find_element(:css, '#user_email')
      elem_email.send_keys "user#{num}@email.com"
      elem_email.submit
    end
    driver.quit
  end

  # ブラウザの起動
  r.report 'headless: launch browser' do
    30.times do |num|
      driver = create_headless_driver
      driver.quit
    end
  end

  r.report 'not headless: launch browser' do
    30.times do |num|
      driver = create_driver
      driver.quit
    end
  end
end
```

# 計測結果 :bar_chart:

※ real = 実経過時間です

```sh
bundle exec ruby headless_benchmark.rb
Rehearsal ----------------------------------------------------------------
省略
-------

                                   user     system      total        real
# ページの表示
headless: load page:           0.030000   0.010000   0.040000 (  9.278591)
not headless: load page:       0.030000   0.010000   0.040000 ( 11.709836)
# テキスト入力
headless: input text:          0.130000   0.050000   0.180000 (  8.656509)
not headless: input text:      0.140000   0.050000   0.190000 ( 11.224485)
# フォームの送信
headless: submit:              0.140000   0.050000   0.190000 ( 20.451673)
not headless: submit:          0.140000   0.050000   0.190000 ( 29.014519)
# ブラウザの起動
headless: launch browser       0.120000   0.030000   0.150000 ( 10.355756)
not headless: launch browser   0.090000   0.020000   0.110000 ( 28.746675)
```

# 準備（詳細）:construction_worker_tone2:
## サンプルWebアプリケーションとSelenium実行環境の構築

以下のコマンドを実行して構築しました。
一部設定ファイルの編集作業がありますので、残念ながらコピペのみでは完結しないので、ご注意下さい。

```bash:サンプルWEBアプリケーションとSelenium実行環境構築
mkdir sample_app
cd sample_app
docker pull ruby
docker pull mysql

# railsコンテナ用のDockerfile作成
cat << EOS > Dockerfile
FROM ruby
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
RUN mkdir -p /sample_app
WORKDIR /sample_app
RUN gem install bundler
ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock
RUN bundle install
ADD . /sample_app
EOS

# Gemfile/Gemfile.lock作成
cat << EOS > Gemfile
source 'https://rubygems.org'
gem 'rails'
EOS
touch Gemfile.lock

# rails,mysql,selenium standaloneのコンテナ起動するdocker-composeファイル
cat << EOS > docker-compose.yml
version: '3'
services:
  db:
    image: mysql
    environment:
      - MYSQL_ROOT_PASSWORD=password
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/sample_app
    ports:
      - "3000:3000"
    depends_on:
      - db
  sss-chrome-debug:
    image: selenium/standalone-chrome-debug
    ports:
      - "4445:4444"
      - "5901:5900"
    depends_on:
      - web
EOS

# railsアプリケーションファイルの作成
docker-compose run web rails new . --force --database=mysql --skip-bundle

# database設定の修正
vi config/database.yml
# ------
  default: &default
    adapter: mysql2
    encoding: utf8
    pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
    username: root
    password: password # 修正箇所1
    host: db # 修正箇所2
# ------

# ビルド
docker-compose build
# コンテナ起動
docker-compose up -d
# テーブルの作成
docker-compose run web rake db:create
# ユーザ登録テーブル、コントローラービュー作成
docker-compose run web rails generate scaffold User name:string email:string
# テーブル追加
docker-compose run web rake db:migrate
# コンテナ確認
docker ps
```

VNCビューワーからsampleapp_sss-chrome-debug_1コンテナに接続し、コンテナ内のchromeから http://web:3000/users (ローカルでみるときはhttp://localhost:3000/users) が開ければ準備完了。
sss-chrome-debugコンテナへはVNCビューワーからlocalhost:5901、パスワードはsecretで接続できます(デフォルトパスワード)
![start_rails_app_for_selenium.png](https://qiita-image-store.s3.amazonaws.com/0/10800/de1066c8-247e-3051-88de-29ed2cb125fc.png)

また、http://localhost:4445/ を開くとselenium serverのページが開けます。

# 参考 :bow: 
- [SeleniumからHeadless Chrome使ってみた](http://qiita.com/orangain/items/db4594113c04e8801aad)
- [Ruby でベンチマークを取る方法](https://qiita.com/scivola/items/c5b2aeaf7d67a9ef310a)
- [【超爆速】DockerでRailsの環境を作る【５分クッキング】](https://qiita.com/k-waragai/items/06d8f798a27f263098bc)
- [Ruby on Rails チュートリアル 実例を使ってRailsを学ぼう](https://railstutorial.jp/chapters/toy_app?version=5.1#sec-toy_app_what_we_learned_in_this_chapte)
- [selenium/standalone-chrome-debug](https://hub.docker.com/r/selenium/standalone-chrome-debug/)
- [Benchmarkモジュール](https://docs.ruby-lang.org/ja/latest/class/Benchmark.html)