railsアプリが遅いって言われたので、久しぶりにrubyでisuconしてみました。
railsアプリでstackprofを使ったプロファイリング
まず、自分がいつもやってる方法なのですが、config.ru
にstackprofの設定を仕込みます。
stackprofはrackミドルウェアとして差し込めるようになっています。
下記設定はrailsだけでなく、sinatraでももちろん動きます。(これをいつも仕込んでおいてあります。)
Gemfileにgem 'stackprof'
を書いてconfig.ruに下記のように仕込んでいます。
is_stackprof = ENV['ENABLE_STACKPROF'].to_i.nonzero?
stackprof_mode = (ENV['STACKPROF_MODE'] || :cpu).to_sym
stackprof_interval = (ENV['STACKPROF_INTERVAL'] || 1000).to_i
stackprof_save_every = (ENV['STACKPROF_SAVE_EVERY'] || 100 ).to_i
stackprof_path = ENV['STACKPROF_PATH'] || 'tmp'
use StackProf::Middleware, enabled: is_stackprof,
mode: stackprof_mode,
raw: true,
interval: stackprof_interval,
save_every: stackprof_save_every,
path: stackprof_path
run Rails.application
環境変数でstackprofの設定を渡しています。
stackprofを仕込んでrailsを起動するときはこんな感じです。
ENABLE_STACKPROF=1 bundle exec rails s
で、今回とったプロファイリング結果がこちら。
(まあ、色々削った結果を載せています)
==================================
Mode: cpu(1000)
Samples: 7758 (7.75% miss rate)
GC: 1720 (22.17%)
==================================
TOTAL (pct) SAMPLES (pct) FRAME
5799 (74.7%) 2987 (38.5%) JSON::Schema.add_indifferent_access
858 (11.1%) 638 (8.2%) JSON#generate
439 (5.7%) 439 (5.7%) block in JSON::Schema::TypeV4Attribute.data_valid_for_type?
866 (11.2%) 359 (4.6%) ActiveSupport::JSON::Encoding::JSONGemEncoder#jsonify
279 (3.6%) 279 (3.6%) block in JSON::Schema.add_indifferent_access
174 (2.2%) 174 (2.2%) String#to_json_with_active_support_encoder
296 (3.8%) 118 (1.5%) block in Array#as_json
111 (1.4%) 98 (1.3%) block in Hash#as_json
2205 (28.4%) 96 (1.2%) block in JSON::Schema::PropertiesV4Attribute.validate
・
・
7150 (92.2%) 32 (0.4%) JSON::Schema::Validator#validate
・
・
3361 (43.3%) 11 (0.1%) JSON::Schema#initialize
JSON::Schema.add_indifferent_access
が悪さしているぽい。
で、ここからは、最近のコード変更点とJSON::Schemaに関わる部分を洗い出して、悪さしているコードを洗い出して、最小の再現コード書いてみたのがこちら。
require 'json'
require 'json-schema'
require 'benchmark'
class BenchmarkJSONSchmea
def initialize
@json_data = []
70_000.times do |i|
@json_data << {id: i, name: "てすとてすと", point: 12345, description: "テストてすと"}
end
@json_schema = <<-JSON
{
"type": "array",
"items": {
"type": "object", "required": ["id"],
"properties": {
"id": {"type": "integer"},
"name": {"type": "string", "minLength": 2, "maxLength": 15},
"point": {"type": "integer"},
"description": {"type": "string", "minLength": 2, "maxLength": 15}
}
}
}
JSON
end
def run
Benchmark.bm(7, ">total:", ">ave:") do |x|
x.report("raw: ") { JSON::Validator.validate!(JSON.parse(@json_schema), JSON.parse(@json_data.to_json)) }
end
end
end
BenchmarkJSONSchmea.new.run
で、結果がこちら
% bundle exec ruby bench.rb
user system total real
raw: 6.310000 0.110000 6.420000 ( 7.567306)
遅い。。。
stackprofの結果を鑑みて
stackprofではJSON::Schema.add_indifferent_access
が遅いって言われているので、ここを見てみてパッチを当ててみた。
require 'json'
require 'json-schema'
require 'benchmark'
class JSON::Schema
def self.add_indifferent_access(schema)
if schema.is_a?(Hash)
schema.default_proc = proc do |hash,key|
if hash.has_key?(key)
hash[key]
else
hash.has_key?(key) ? hash[key] : nil
end
end
schema.keys.each do |key|
add_indifferent_access(schema[key])
end
end
end
end
class BenchmarkJSONSchmea
def initialize
@json_data = []
10_000.times do |i|
@json_data << {"id" => i, "name" => "てすとてすと", "point" => 12345, "description" => "テストてすと"}
end
@json_schema = <<-JSON
{
"type": "array",
"items": {
"type": "object", "required": ["id"],
"properties": {
"id": {"type": "integer"},
"name": {"type": "string", "minLength": 2, "maxLength": 15},
"point": {"type": "integer"},
"description": {"type": "string", "minLength": 2, "maxLength": 15}
}
}
}
JSON
end
def run
Benchmark.bm(7, ">total:", ">ave:") do |x|
x.report("patch: ") do
JSON::Validator.validate!(JSON.parse(@json_schema), JSON.parse(@json_data.to_json))
end
end
end
end
BenchmarkJSONSchmea.new.run
結果がこちら
% bundle exec ruby patch_bench.rb
user system total real
patch: 0.890000 0.010000 0.900000 ( 0.993027)
これで5倍くらいは速くなってます。
あとはrefimentsでパッチ当てて、こんな感じで局所化したかったけどなぜかうまく動作しない。。。
module JSONSchemaExtension
refine JSON::Schema.singleton_class do
def add_indifferent_access(schema)
if schema.is_a?(Hash)
schema.default_proc = proc do |hash,key|
if hash.has_key?(key)
hash[key]
else
hash.has_key?(key) ? hash[key] : nil
end
end
schema.keys.each do |key|
add_indifferent_access(schema[key])
end
end
end
end
end
class BenchmarkJSONSchmea
using JSONSchemaExtension
end