Oromis
Oromis

Reputation: 347

How can I speed up my Rails App JSON rendering?

my rails 4.2.4 app has a JSON API. One action is called very, very frequently, so I want to make it as fast as possible (around 150ms per request is my target).

The controller method looks like this:

def update
  t_update = 0
  t_render = 0
  total = Benchmark.measure do
    success = false
    t_update = Benchmark.measure do
      success = @character.update_attributes(char_params)
    end
    t_render = Benchmark.measure do
        if success
          respond_to do |format|
            format.html { redirect_to @character }
            format.json { render json: char_to_json(@character) }
          end
        else
          respond_to do |format|
            format.html { redirect_to @character }
            format.json { render json: char_to_json(@character), status: 500 }
          end
        end
    end
  end
  logger.warn("##### Update: #{(t_update.real*1000).to_i}ms")
  logger.warn("##### Render: #{(t_render.real*1000).to_i}ms")
  logger.warn("##### Update + Render: #{(total.real*1000).to_i}ms")
end

I've added some benchmark measurements to get an idea where the Time is spent. I get output like this:

App 6144 stdout: ##### Update: 195ms
App 6144 stdout: ##### Render: 265ms
App 6144 stdout: ##### Update + Render: 461ms

This is on the production server, in rails production mode of course. Obviously way too slow. Since rendering takes a higher amount of time, I wanted to optimize that first.

char_to_json looks like this:

def char_to_json(char)
  hash = nil
  json = nil
  t_hash = Benchmark.measure do
    hash = char.as_json
  end
  t_render = Benchmark.measure do
    json = Oj.dump(hash, mode: :object, indent: 0)
  end
  logger.warn("####  Hashing: #{(t_hash.real*1000).to_i}ms")
  logger.warn("####  Rendering: #{(t_render.real*1000).to_i}ms")
  json
end

The benchmarks here show:

App 6144 stdout: ####  Hashing: 263ms
App 6144 stdout: ####  Rendering: 0ms

So all of the time is spent creating the Hash! Admittedly, it's a quite large and complex object graph, but it basically just creates a Hash from my model objects with everything already initialized (ActiveRecord takes around 20ms for the whole request):

def as_json
  grouped_items = items.group_by {|i| i.container }

  json = {
    'id' => id,
    'name' => name,
    'status' => status,
    'creating' => creating?,
    'title' => title,
    'level' => level,
    'level_count' => level_count,
    'rules_compliant' => rules_compliant?,
    'health' => health,
    'max_health' => max_health,
    'stamina' => stamina,
    'mana' => mana,
    'mana_mult_buff' => mana_mult_buff,
    'wounds' => wounds,
    'armor_buff' => decimal_number(armor_buff),
    'damage_incoming' => decimal_number(damage_incoming),

    'rolled_damage_left' => rolled_damage_left,
    'rolled_damage_right' => rolled_damage_right,
    'initiative_roll' => initiative_roll,

    'offensive_buff' => offensive_buff,
    'defensive_buff' => defensive_buff,
    'evasion_buff' => evasion_buff,
    'speed_buff' => speed_buff,

    'notes' => notes,

    'race' => race.as_json,
    'birthsign' => birthsign.as_json,
    'specialization' => specialization.as_json,
    'fav_attribute1' => fav_attribute1.as_json(mode: :fav),
    'fav_attribute2' => fav_attribute2.as_json(mode: :fav),

    'attributes' => character_attributes.map {|attr| attr.as_json },
    'skills' => skills.map {|skill| skill.as_json },
    'resistances' => resistances.map {|resi| resi.as_json },

    'containers' => Item.containers.map do |container|
      {
        'key' => container[0],
        'name' => I18n.t("activerecord.attributes.item.container.#{container[0]}"),
        'weight' => decimal_number(send("#{container[0]}_weight")),
        'max_weight' => respond_to?("max_#{container[0]}_weight") ?
                                             decimal_number(send("max_#{container[0]}_weight")) :
                                             nil,
        'items' => (grouped_items[container[0]] || []).
            sort {|a,b| a.index <=> b.index}.
            map {|item| item.as_json }
      }
    end,

    'slots' => slots.map {|slot| slot.as_json },
    'spells' => spells.map {|spell| spell.as_json self }
  }
  formulas.each do |formula|
    json[formula.property.abbr] = decimal_number(formula.points)
  end
  json
end

How can I investigate further? Where could the time be spent here? Or is creating a large and nested Hash just slow in ruby? I don't want to believe that!

Thank you for any advice!

Upvotes: 2

Views: 2005

Answers (1)

Semjon
Semjon

Reputation: 1023

Eager loading

Check for N+1 database hits. In your as_json few associations were used (like race, skills, resistances), may be not all of those associations selected in the first query when you read @character instance. Additional info http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations

Consider about cache

  • 'race' => race.as_json - I suppose you do not have so many races and they are not updated frequently. You can put races json to Rails.cache like

    'race' => Rails.cache.fetch("json_race_#{race.id}", expires_in: 1.day) do
      race.as_json
    end
    

    Do not forget to invalidate cache if you change races :)

  • 'containers' => Item.containers.map ... - Looks like this part of code is always the same for any character. May be cached as well.

Upvotes: 3

Related Questions