By default Grape uses ActiveSupport::HashWithIndifferentAccess 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.
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, the total represents a number of bytes. Let's try another builder which only supports symbols.
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:
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 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
Top comments (1)
The bit of memory overhead is good to know about, and certainly not good, but it would be interesting to see how the memory overhead performs in the real world.
If the extra bytes get garbage collected efficiently after the request is over then the overhead per request might not make a difference when your application is running in production.
On the other hand, Rails memory bloat happens pretty easily so it's also possible that this extra memory per request does cause problems. It would be interesting to see.