Grape: The cost of ActiveSupport::HashWithIndifferentAccess
Tags: ruby, grape, performance
Published: 2019-12-08 15:43:26

By [default Grape](https://github.com/ruby-grape/grape#params-class) uses [ActiveSupport::HashWithIndifferentAccess](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/hash_with_indifferent_access.rb) as a parameter builder. The most important thing about `ActiveSupport::HashWithIndifferentAccess` is that symbols are mapped to strings when used as keys.

So, if your routers refer to parameters by using symbols, keys get converted to strings. Let's see how it affects memory.

```ruby
require 'ruby-prof'
require 'grape'

class API < Grape::API
  prefix :api
  version 'v1', using: :path

  params do
    requires :address, type: Hash do
      requires :street, type: String
      requires :postal_code, type: Integer
      optional :city, type: String
    end
  end

  post '/' do
    declared = declared(params)
    declared[:address][:street]
  end
end

options = {
  method: 'POST',
  params: {
    address: {
      street: 'Test Street',
      postal_code: '90698',
      city: 'Imagine'
    }
  }
}

env = Rack::MockRequest.env_for('/api/v1', options)

# warm up
API.call env

RubyProf.measure_mode = RubyProf::MEMORY

result = RubyProf.profile do
  API.call env
end

printer = RubyProf::FlatPrinter.new(result)
printer.print(STDOUT, min_percent: 1)
```

For profiling Grape 1.2.5 and Ruby 2.6.3 were used. Results:

```
Measure Mode: memory
Thread ID: 47373520927160
Fiber ID: 47373526155200
Total: 41112.000000
Sort by: self_time

 %self      total      self      wait     child     calls  name
 13.66   9304.000  5616.000     0.000  3688.000       18  *Hash#each_pair
 10.70  12648.000  4400.000     0.000  8248.000       29  *Class#new
  8.68   5088.000  3568.000     0.000  1520.000        8  *Array#map
  5.64   2320.000  2320.000     0.000     0.000       58   Symbol#to_s
  5.08   2088.000  2088.000     0.000     0.000        9   Hash#merge
```

According to the [Ruby-prof doc](https://ruby-prof.github.io/#reports), the total represents a number of bytes. Let's try another builder which only supports symbols.

```ruby
Grape.configure do |config|
  config.param_builder = Grape::Extensions::Hash::ParamBuilder
end
```

Results:

```
Measure Mode: memory
Thread ID: 47077831236000
Fiber ID: 47077836608980
Total: 33328.000000
Sort by: self_time

 %self      total      self      wait     child     calls  name
 11.28   5736.000  3760.000     0.000  1976.000        7   Hash#each_pair
 10.71   5088.000  3568.000     0.000  1520.000        8  *Array#map
  6.94   9664.000  2312.000     0.000  7352.000       25  *Class#new
  6.27   2088.000  2088.000     0.000     0.000        9   Hash#merge
  4.08   1440.000  1360.000     0.000    80.000        5   Grape::Endpoint#run_filters
```

Switching to the simpler builder saves 7,60 Kbytes per request. Yeah, it isn't crazy number, but the change is one LOC. So, if you can agree with your team on using symbols everywhere, you can save a chunk of memory. By the way, the parameter builder can be configured per route, thus, the migration might be done per route.

However, if strings are used as keys for parameters:

```ruby
  params do
    requires 'address', type: Hash do
      requires 'street', type: String
      requires 'postal_code', type: Integer
      optional 'city', type: String
    end
  end

  post '/' do
    declared = declared(params)
    declared['address']['street']
  end
```

the picture is a bit better for `Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder`.

```
Measure Mode: memory
Thread ID: 47413799950760
Fiber ID: 47413805177840
Total: 39832.000000
Sort by: self_time

 %self      total      self      wait     child     calls  name
 14.10   9064.000  5616.000     0.000  3448.000       18  *Hash#each_pair
 11.05  12448.000  4400.000     0.000  8048.000       29  *Class#new
  8.96   5088.000  3568.000     0.000  1520.000        8  *Array#map
  5.24   2088.000  2088.000     0.000     0.000        9   Hash#merge
  4.66   2016.000  1856.000     0.000   160.000        8   ActiveSupport::HashWithIndifferentAccess#[]=
```

Anyway `Grape::Extensions::Hash::ParamBuilder` is a winner.

### Upcoming Grape 1.3.0

It has [an improvement](https://github.com/ruby-grape/grape/pull/1940) in `Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder`, so it will be slightly better.

```
Measure Mode: memory
Thread ID: 46918412761520
Fiber ID: 46918421185000
Total: 36448.000000
Sort by: self_time

 %self      total      self      wait     child     calls  name
 12.86   8336.000  4688.000     0.000  3648.000       11  *Hash#each_pair
  9.79   5248.000  3568.000     0.000  1680.000        8  *Array#map
  8.89  11880.000  3240.000     0.000  8640.000       27  *Class#new
  5.93   2160.000  2160.000     0.000     0.000       54   Symbol#to_s
  5.73   2088.000  2088.000     0.000     0.000        9   Hash#merge
```