概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: A Better Time with Rails url_helper
- 原文公開日: 2017/12/27
- 著者: Travis Hunter
- サイト: https://travisofthenorth.com/
Railsのurl_helperの速度低下を防ぐコツ(翻訳)
ある少年がナイフを一本もらいました。「これはとてもよく切れるナイフだから」という触れ込みでしたが、いざ少年が肉などを切ろうとしてみるとどうもうまく切れないので、少年は「このナイフ、言うほど大したことないな」と思ってしまいました。しかしナイフをくれた人が「ちょい待ち、ナイフの刃をもう少し傾けて。それからナイフの持ち方がよくないからこう持ってごらん」とアドバイスしたところ、今度は見事ナイフで肉を切ることができました。
オープンソースソフトウェアや人生はちょうどこのナイフのように、正しい使い方を学ばないとうまくいかないことがあります。しかも、ナイフそのものに避けがたい問題があることもあります。さらに、ネットで見つけたナイフの使い方の情報がひどいしろもので、始末の悪いことに特定の状況ではその情報が適切だったりすることもあります。人生が面倒なのは今に始まったことではありませんが、ともあれ本記事ではRails.application.routes.url_helpers
というナイフについて書いてみたいと思います。
Railsコントローラのコンテキストの外でURLを生成する状況は非常に多いので、シリアライザやジョブなど、その機能が自動的には使えないような場所でこのモジュールの機能が必要になることも非常によくあります。ネットの情報では、このモジュールに直接アクセスすることを何年もの間気軽に勧めていて、しばらくの間これで何の問題も生じませんでした。
しかし運の悪いことに、最近のバージョンのRailsではこの方法で問題が生じるようになりました(問題を指摘しているGithub issueと修正のPRを参照)。これによる問題を明らかにするため、簡単なテスト用Railsアプリをセットアップしてみました。関連するコードを以下に示します。
# routes.rb
Rails.application.routes.draw do
resources :things do
collection do
get :faster
end
end
end
# app/whatever/url_helper.rb
class UrlHelper
include Singleton
include Rails.application.routes.url_helpers
end
# app/controllers/things_controller.rb
class ThingsController < ApplicationController
def index
things_json = (1..100).map do |i|
{
id: i,
url: Rails.application.routes.url_helpers.thing_url(i, host: 'localhost')
}
end
render json: things_json
end
def faster
things_json = (1..100).map do |i|
{
id: i,
url: UrlHelper.instance.thing_url(i, host: 'localhost')
}
end
render json: things_json
end
end
このindex
アクションでは、StackOverflowのアドバイスどおりにコードでモジュールを直接呼び出しています。faster
アクションの方では、このモジュールをinclude
するヘルパークラスを使っています。Railsではこのモジュールをinclude
して使うことが推奨されているようです。
2つのアプローチの実行結果を並べて見てみましょう。ab
と単一のRailsインスタンスで小さなテストを実行してみました(不要な出力が大量にあったので省略してあります)。
➜ ab -n 1000 http://127.0.0.1:3000/things
Concurrency Level: 1
Time taken for tests: 18.155 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 4815000 bytes
HTML transferred: 4485000 bytes
Requests per second: 55.08 [#/sec] (mean)
Time per request: 18.155 [ms] (mean)
Time per request: 18.155 [ms] (mean, across all concurrent requests)
Transfer rate: 259.00 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 1
Processing: 15 18 2.2 17 42
Waiting: 15 18 2.2 17 42
Total: 15 18 2.2 18 42
➜ ab -n 1000 http://127.0.0.1:3000/things/faster
Concurrency Level: 1
Time taken for tests: 8.540 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 4815000 bytes
HTML transferred: 4485000 bytes
Requests per second: 117.09 [#/sec] (mean)
Time per request: 8.540 [ms] (mean)
Time per request: 8.540 [ms] (mean, across all concurrent requests)
Transfer rate: 550.58 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 1
Processing: 7 8 1.3 8 23
Waiting: 7 8 1.3 8 23
Total: 7 8 1.3 8 23
最初の例(index
メソッド)ではモジュールを直接呼び出していますが、平均して18msを要し、スループットは55リクエスト/秒です。本番で調子の良いときにはリクエストに5秒かかるのであれば「そんなに悪くないんじゃ?」とお思いかもしれません。しかしモジュールを直接呼び出すのではなく、単にinclude
する方はどうでしょうか?こちら(faster
メソッドの方)は平均して8msでスループットは117リクエスト/秒と、最初のアプローチのほぼ倍速になっています。私は名シェフではありませんが、このナイフを正しく持てば適切に肉を切れるのです。
結論: Rails.application.routes.url_helpers
を直接呼ばず、このモジュールをクラスにinclude
することで、コードは高速化します。