railsアプリでstackprofを使ってボトルネックを探す + JSON::Schema(2.2.1)の高速化
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 # 試行回数は1回 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 # 試行回数は1回 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