ActiveResourceが遅い→JSONならパースが速いよ

きっかけ

ネットワーク越しだし、速度が出ないのはまぁいいんだけど、それにしたって遅い。
具体的にはXMLのパースが遅い、遅すぎる。
なんとかならぬか。

どうやらXMLSimpleがボトルネックらしい。
JSON使った方がましかなぁ。

パーサの速度比較

同じデータをto_jsonとto_xmlでそれぞれシリアライズしたファイルを用意。(20数個のフィールドを持つレコード20件のもの。)
Hash.from_xml(XmlSimple)、ActiveSupport::JSON.decode、JSON.parse(JSON implementation for Ruby)それぞれでパースに必要な時間を測定してみた。

Benchmark.bm do |x|
  x.report { 10.times{ Hash.from_xml(xml) } }
  x.report { 10.times{ ActiveSupport::JSON.decode(json) } }
  x.report { 10.times{ JSON.parse(json) } }
end
      user     system      total        real
  0.870000   0.530000   1.400000 (  1.394237)
  0.160000   0.010000   0.170000 (  0.168284)
  0.000000   0.000000   0.000000 (  0.006735)

JSON.parse、圧倒的じゃないか・・・

   / ̄ ̄\ 
 /   _ノ  \ 
 |    ( ●)(●) 
. |     (__人__)   XmlSimple遅すぎだろ
  |     ` ⌒´ノ    常識的に考えて…
.  |         } 
.  ヽ        } 
   ヽ     ノ        \ 
   /    く  \        \ 
   |     \   \         \ 
    |    |ヽ、二⌒)、          \ 

経過

コントローラ
def index
  #  省略
  respond_to do |format|
    format.xml { render :xml => @shops }
    format.xml { render :xml => @shops }
  end
end

def create
  #  省略
  respond_to do |format|
    if @shop.save
      format.xml { render :xml => @shop, :status => :created, :location => @shop }
      format.json { render :json => @shop, :status => :created, :location => @shop }
    else
      # ActiveRecord::Errors#to_xmlは専用のものでオーバーライドされているが、
      #to_jsonはそうでは無く、TypeError: wrong argument type Hash (expected Data)が発生してしまうのに注意。
      format.xml { render :xml => @shop.errors, :status => :unprocessable_entity }
      format.json { render :json => { :errors => @shop.errors.full_messages }, :status => :unprocessable_entity }
    end
  end
  
  # show/update/destroyも同様に。
end
ActiveResource

ActiveResource::Base.formatを変更する。

class Shop << ActiveResource::Base
  self.site = 'http://taslam-example.jp/'
  self.format = :json  # ActiveResource::Formats:JsonFormatを使う
end

しかし、、

Shop.find(:all).first.name # => "\\u30a2\\u30c9\\u30c7\\u30b6\\u30a4\\u30f3"

日本語がうまくデコードされないみたいだ。

ActiveResource::Formats:JsonFormatを見てみると、

def decode(json)
  ActiveSupport::JSON.decode(json)
end

どうやら、ActiveSupport::JSON.decodeがだめみたい。
試してみると、JSON.parseだとうまくデコードされる模様。
そこで、このActiveSupport::JSONJSONに置き換えたActiveResource::Formats:ExJsonFormatを作って、こちらを使うようにする。こっちのが速いしね。

# application.rbの末尾などに
require 'json'
module ActiveResource
  module Formats
    module ExJsonFormat
      include ActiveResource::Formats::JsonFormat  

      def decode(json)
        JSON.parse(json)
      end
      
      extend self
    end
  end
end
class Shop << ActiveResource::Base
  self.site = 'http://taslam-example.jp/'
  self.format = :ex_json
end

これで大丈夫かと思ったんだけど、テストしてみるとリソースの作成、更新に失敗しているみたい。ActiveRecord::Baseを見てみる。

def to_xml(options={})
  attributes.to_xml({:root => self.class.element_name}.merge(options))
end

def create
  returning connection.post(collection_path, to_xml, self.class.headers) do |response|
    self.id = id_from_response(response)
    load_attributes_from_response(response)
  end
end

データをXMLで送ってるのかな。ActiveResource::Connection#postを追ってみる。

def post(path, body = '', headers = {})
  request(:post, path, body.to_s, build_request_headers(headers))
end

def default_header
  @default_header ||= { 'Content-Type' => format.mime_type }
end

def build_request_headers(headers)
  authorization_header.update(default_header).update(headers)
end

Content-type: application/json で、 /shops.json に、データをXMLで送ってたみたいだ。受け取ってる側のログをみると、

Processing ShopsController#create (for 192.168.1.9 at 2008-05-29 15:01:15) [POST]
  Parameters: {"format"=>"json", "action"=>"create", "controller"=>"shops"}
Completed in 0.38161 (2 reqs/sec) | Rendering: 0.00029 (0%) | DB: 0.00000 (0%) | 422 Unprocessable Entity [http://taslam-example.jp/shops.json]

やっぱり、データが受け取れてないや。

ひとまず、検索時だけJSONで高速化できれば当初の目的は達成できる(し、JSONに変更したことでもし不具合がでてもデータの更新ができないなどということにならない)ので、findを使う際のみformatを切り替えることにした。

class Shop << ActiveResource::Base
  self.site = 'http://taslam-example.jp/'
  
  def self.find(*args)
    self.format = self.connection.format = ActiveResource::Formats[:ex_json]
    super(*args)
  ensure
    self.format = self.connection.format = ActiveResource::Formats[:xml]
  end

end