読者です 読者をやめる 読者になる 読者になる

CubicLouve

Spring_MTの技術ブログです。https://github.com/SpringMT (http://spring-mt.tumblr.com/ からの移転)

railsアプリでstackprofを使ってボトルネックを探す + JSON::Schema(2.2.1)の高速化

ruby

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