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:

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.