Grape: Include is still better for code splitting
Posted: Apr 12, 2020
Grape offers a mount method for combining parts (endpoints) of API.
class AddressAPI < Grape::API
get '/addresses' do
# logic here
end
params do
# params declaration
end
post '/address' do
# logic here
end
params do
# params declaration
end
patch '/address' do
# logic here
end
end
class API < Grape::API
version 'v1', using: :path
mount AddressAPI
end
Naturally, we would like to avoid one long file (class), so some developers could use this way for code splitting.
However, there is another one way which every Ruby developer knows.
module AddressAPI
extend ActiveSupport::Concern
included do
get '/addresses' do
# logic here
end
params do
# params declaration
end
post '/address' do
# logic here
end
params do
# params declaration
end
patch '/address' do
# logic here
end
end
end
class API < Grape::API
version 'v1', using: :path
include AddressAPI
end
Note: I could implement it without ActiveSupport, but Grape depends on it, so I prefer to benefit from dependencies I have in the stack.
A project I work on uses the last approach, I don't know the reason, most likely, the mount wasn't supported back then. So, I was curios which approach is better in terms of memory.
I added params to the post and path endpoints:
requires :address, type: Hash do
requires :street, type: String
requires :postal_code, type: Integer
optional :city, type: String
optional :tags, type: Array[String]
end
then installed memory_profiler to Ruby 2.7.1 and measured the API declaration.
Mount:
Total allocated: 549351 bytes (4881 objects)
Total retained: 146918 bytes (1020 objects)
Include:
Total allocated: 419311 bytes (3938 objects)
Total retained: 79702 bytes (583 objects)
The difference is essential, the mount needs extract 126.99 Kb of the memory, 65.6 Kb of 126.99 Kb wasn't removed by garbage collector, very likely, it will stay.
A reason for this difference is inheritance, AddressAPI
inherits settings of API
. The inheritance logic is quite complex, it even involves copies in time. Settings contain lots of things:
- declared params
- validations
- helpers
- callbacks (
before
,after
etc) - rescue handlers
Ok, let's make it even more fun and extract the post endpoint to a separate class and mount into AddressAPI
:
class PostAddressAPI < Grape::API
params do
requires :address, type: Hash do
requires :street, type: String
requires :postal_code, type: Integer
optional :city, type: String
optional :tags, type: Array[String]
end
end
post '/address' do
# nothing here
end
end
Outcome:
Total allocated: 656751 bytes (5654 objects)
Total retained: 203502 bytes (1398 objects)
To be fair, I checked the same approach with the include:
Total allocated: 420367 bytes (3942 objects)
Total retained: 80758 bytes (587 objects)
The difference is 230.84 Kb! If you use the mount for code splitting, probably, you need to consider the include.