Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance degradation (~6x) when using given to model a dependent relation #2100

Closed
senhalil opened this issue Sep 9, 2020 · 12 comments · Fixed by #2127
Closed

Performance degradation (~6x) when using given to model a dependent relation #2100

senhalil opened this issue Sep 9, 2020 · 12 comments · Fixed by #2127
Labels

Comments

@senhalil
Copy link

senhalil commented Sep 9, 2020

When change one of the parameters of out API as dependent to another one, the post duration increased from ~30 seconds to ~600 seconds. Is this normal?

Our model is deeply nested but both the dependee and depender are on the first level.

params do
  optional :matrix
  given matrix: ->(val) { val.nil? || val.empty? } do
    requires :vehicle, type: Hash do
      requires :router_mode, type: String
    end
  end
  given matrix: ->(val) { val.any? } do
    requires :vehicle, type: Hash do
      optional :matrix_id, type: String
      optional :router_mode, type: String
      exactly_one_of :matrix_id, :router_mode
    end
  end
end
@dblock
Copy link
Member

dblock commented Sep 9, 2020

I bet it's fixed by #2096. Try HEAD?

@dblock dblock added the bug? label Sep 9, 2020
@senhalil
Copy link
Author

senhalil commented Sep 9, 2020

Unfortunately, the performance degradation still exists. Actually, I work together with @braktar and we tested on his computer with the fix.

@dblock
Copy link
Member

dblock commented Sep 9, 2020

Put this into a benchmark and let's fix it.

@dnesteryuk
Copy link
Member

@senhalil are you going to work on the benchmark?

@senhalil
Copy link
Author

@dnesteryuk this issue fell in the cracks apparently, thanks for the ping.

I don't think I will be able to work on this issue since we gave up using given. The performance issue is still there and can be observed by making one of the parameters dependent in the existing large_model benchmark https://github.com/ruby-grape/grape/blob/master/benchmark/large_model.rb

@dblock dblock mentioned this issue Oct 19, 2020
@dnesteryuk
Copy link
Member

I wrote this simple benchmark to compare params without dependencies and with them:

require 'grape'
require 'benchmark/ips'

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

  params do
    optional :matrix
    given matrix: ->(val) { val.nil? || val.empty? } do
      requires :vehicle, type: Hash do
        requires :router_mode, type: String
      end
    end
    given matrix: ->(val) { val.any? } do
      requires :vehicle, type: Hash do
        optional :matrix_id, type: String
        optional :router_mode, type: String
        exactly_one_of :matrix_id, :router_mode
      end
    end
  end
  post '/' do
    'hello'
  end
end

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

  params do
    optional :matrix

    requires :vehicle, type: Hash do
      optional :matrix_id, type: String
      optional :router_mode, type: String
      exactly_one_of :matrix_id, :router_mode
    end
  end
  post '/' do
    'hello'
  end
end

options = {
  method: 'POST',
  params: {
    matrix: {
      something: 'test'
    },
    vehicle: {
      router_mode: 'hello'
    }
  }
}

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

# warm up
GivenAPI.call

Benchmark.ips do |ips|
  ips.report('Given') do
    GivenAPI.call env
  end

  ips.report('Simple') do
    SimpleAPI.call env
  end

  ips.compare!
end

The result:

Warming up --------------------------------------
               Given   388.000  i/100ms
              Simple   583.000  i/100ms
Calculating -------------------------------------
               Given      3.938k (± 2.6%) i/s -     19.788k in   5.028092s
              Simple      5.920k (± 2.1%) i/s -     29.733k in   5.024627s

Comparison:
              Simple:     5920.1 i/s
               Given:     3938.2 i/s - 1.50x  slower

I would say it isn't that bad.

@senhalil
Copy link
Author

I happened to have sometime this weekend, I thought I can chip-in.

I suspect this issue is related to data size and model parameter type, that's why it doesn't show up on a simple model with a small instance. I am still surprised that it takes 1.50x slower even with this model and instance.

I should have shared the definition of the model parameters properly in my original post. The matrix parameter actually has parameters of type Array[Array[Float]] inside (which are not taking part in the conditional statements). Even though neither the definition of these matrix elements depends on the conditional statement nor they appear in the condition itself, somehow they decrease the performance.

I have found an error in my original code that produced 20x difference -- one of the parameters were not defined in the SimpleAPI 🤦 -- sorry for the hype!

I created the following smallish model from scratch by cropping the irrelevant parts of ./benchmark/large_model.rb. The perf diff is around 6x with vrp_w_matrix_example.json (which is the relevant parts of a not so small but definitely not big VRP problem of 432 points).

I suspect the bigger the data gets, the worse the performance difference gets -- even if the part of the data have nothing to do with the conditional part of the model.

Warming up --------------------------------------
               Given     1.000  i/100ms
              Simple     1.000  i/100ms
Calculating -------------------------------------
               Given      0.109  (± 0.0%) i/s -      7.000  in  65.676987s
              Simple      0.720  (± 0.0%) i/s -     43.000  in  60.040736s

Comparison:
              Simple:        0.7 i/s
               Given:        0.1 i/s - 6.58x  (± 0.00) slower
require 'grape'
require 'benchmark/ips'

class SimpleAPI < Grape::API
  prefix :api
  version 'v1', using: :path
  content_type :json, 'application/json; charset=UTF-8'
  default_format :json

  def self.vrp_request_point(this)
    this.requires(:id, type: String, allow_blank: false)
    this.optional(:matrix_index, type: Integer, allow_blank: false, desc: 'Index within the matrices')
    this.optional(:location, type: Hash, allow_blank: false) do
      requires(:lat, type: Float, allow_blank: false)
      requires(:lon, type: Float, allow_blank: false)
    end
    this.exactly_one_of :matrix_index, :location
  end

  params do
    optional(:vrp, type: Hash, documentation: { param_type: 'body' }) do
      optional(:name, type: String)

      optional(:matrices, type: Array, documentation: { desc: 'Define the distances' }) do
        requires(:id, type: String, allow_blank: false)
        optional(:time, type: Array[Array[Float]], allow_blank: false, desc: 'Matrix of time')
        optional(:distance, type: Array[Array[Float]], allow_blank: false, desc: 'Matrix of distance')
      end

      optional(:points, type: Array) do
        SimpleAPI.vrp_request_point(self)
      end
    end
  end

  post '/' do
    'hello'
  end
end


class GivenAPI < Grape::API
  prefix :api
  version 'v1', using: :path
  content_type :json, 'application/json; charset=UTF-8'
  default_format :json

  def self.vrp_request_point(this)
    this.requires(:id, type: String, allow_blank: false)
    this.optional(:location, type: Hash, allow_blank: false) do
      requires(:lat, type: Float, allow_blank: false)
      requires(:lon, type: Float, allow_blank: false)
    end
  end

  params do
    optional(:vrp, type: Hash, documentation: { param_type: 'body' }) do
      optional(:name, type: String)

      optional(:matrices, type: Array, documentation: { desc: 'Define the distances' }) do
        requires(:id, type: String, allow_blank: false)
        optional(:time, type: Array[Array[Float]], allow_blank: false, desc: 'Matrix of time')
        optional(:distance, type: Array[Array[Float]], allow_blank: false, desc: 'Matrix of distance')
      end

      given matrices: ->(val) { val.nil? || val.empty? } do
        optional(:points, type: Array, documentation: { desc: 'Particular place on the map' }) do
          GivenAPI.vrp_request_point(self)
          exactly_one_of :location
        end
      end
      given matrices: ->(val) { val && !val.empty? } do
        optional(:points, type: Array, documentation: { desc: 'Particular place in the map' }) do
          GivenAPI.vrp_request_point(self)
          optional(:matrix_index, type: Integer, allow_blank: false, desc: 'Index within the matrices')
          exactly_one_of :matrix_index, :location
        end
      end
    end
  end
  post '/' do
    'hello'
  end
end

options = {
  method: 'POST',
  params: JSON.parse(File.read('vrp_w_matrix_example.json')) # https://gist.github.com/senhalil/263188ebdbe04ef545fc4278838cd8d8#file-vrp_w_matrix_example-json
}

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

Benchmark.ips do |ips|
  ips.config(time: 60, warmup: 1)

  ips.report('Given') do
    GivenAPI.call env
  end

  ips.report('Simple') do
    SimpleAPI.call env
  end

  ips.compare!
end

@senhalil senhalil changed the title Performance degradation (20x) when using given to model a dependent relation Performance degradation (~6x) when using given to model a dependent relation Oct 24, 2020
dnesteryuk added a commit that referenced this issue Oct 27, 2020
Fixes #2100

The reason was in `ActiveSupport::HashWithIndifferentAccess`, it
is super expensive.

When users use `Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder`
or `Grape::Extensions::Hashie::Mash::ParamBuilder` as a parameter builder
there is no change. However, users who use `Grape::Extensions::Hash::ParamBuilder`
must make sure an attribute to be dependent on must be a symbol.

    given :matrix do
      # block here
    end
dnesteryuk added a commit that referenced this issue Oct 27, 2020
Fixes #2100

The reason was in `ActiveSupport::HashWithIndifferentAccess`, it
is super expensive.

When users use a `Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder`
or `Grape::Extensions::Hashie::Mash::ParamBuilder` parameter builder
there is no change. However, users who use `Grape::Extensions::Hash::ParamBuilder`
must make sure a parameter to be dependent on must be a symbol.

    given :matrix do
      # block here
    end

Benchmark after this fix:

Warming up --------------------------------------
               Given     1.000  i/100ms
              Simple     1.000  i/100ms
Calculating -------------------------------------
               Given      0.804  (± 0.0%) i/s -     49.000  in  61.186831s
              Simple      0.855  (± 0.0%) i/s -     52.000  in  60.926097s

Comparison:
              Simple:        0.9 i/s
               Given:        0.8 i/s - 1.06x  slower
dnesteryuk added a commit that referenced this issue Oct 27, 2020
Fixes #2100

The reason was in `ActiveSupport::HashWithIndifferentAccess`, it
is super expensive.

When users use a `Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder`
or `Grape::Extensions::Hashie::Mash::ParamBuilder` parameter builder
there is no change. However, users who use `Grape::Extensions::Hash::ParamBuilder`
must make sure a parameter to be dependent on must be a symbol.

    given :matrix do
      # block here
    end

Benchmark after this fix:

Warming up --------------------------------------
               Given     1.000  i/100ms
              Simple     1.000  i/100ms
Calculating -------------------------------------
               Given      0.804  (± 0.0%) i/s -     49.000  in  61.186831s
              Simple      0.855  (± 0.0%) i/s -     52.000  in  60.926097s

Comparison:
              Simple:        0.9 i/s
               Given:        0.8 i/s - 1.06x  slower
dnesteryuk added a commit that referenced this issue Oct 28, 2020
Fixes #2100

The reason was in `ActiveSupport::HashWithIndifferentAccess`, it
is super expensive.

When users use a `Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder`
or `Grape::Extensions::Hashie::Mash::ParamBuilder` parameter builder
there is no change. However, users who use `Grape::Extensions::Hash::ParamBuilder`
must make sure a parameter to be dependent on must be a symbol.

    given :matrix do
      # block here
    end

Benchmark after this fix:

Warming up --------------------------------------
               Given     1.000  i/100ms
              Simple     1.000  i/100ms
Calculating -------------------------------------
               Given      0.804  (± 0.0%) i/s -     49.000  in  61.186831s
              Simple      0.855  (± 0.0%) i/s -     52.000  in  60.926097s

Comparison:
              Simple:        0.9 i/s
               Given:        0.8 i/s - 1.06x  slower
dnesteryuk added a commit that referenced this issue Oct 28, 2020
Fixes #2100

The reason was in `ActiveSupport::HashWithIndifferentAccess`, it
is super expensive.

When users use a `Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder`
or `Grape::Extensions::Hashie::Mash::ParamBuilder` parameter builder
there is no change. However, users who use `Grape::Extensions::Hash::ParamBuilder`
must make sure a parameter to be dependent on must be a symbol.

    given :matrix do
      # block here
    end

Benchmark after this fix:

Warming up --------------------------------------
               Given     1.000  i/100ms
              Simple     1.000  i/100ms
Calculating -------------------------------------
               Given      0.804  (± 0.0%) i/s -     49.000  in  61.186831s
              Simple      0.855  (± 0.0%) i/s -     52.000  in  60.926097s

Comparison:
              Simple:        0.9 i/s
               Given:        0.8 i/s - 1.06x  slower
dnesteryuk added a commit that referenced this issue Oct 28, 2020
Fixes #2100

The reason was in `ActiveSupport::HashWithIndifferentAccess`, it
is super expensive.

When users use a `Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder`
or `Grape::Extensions::Hashie::Mash::ParamBuilder` parameter builder
there is no change. However, users who use `Grape::Extensions::Hash::ParamBuilder`
must make sure a parameter to be dependent on must be a symbol.

    given :matrix do
      # block here
    end

Benchmark after this fix:

Warming up --------------------------------------
               Given     1.000  i/100ms
              Simple     1.000  i/100ms
Calculating -------------------------------------
               Given      0.804  (± 0.0%) i/s -     49.000  in  61.186831s
              Simple      0.855  (± 0.0%) i/s -     52.000  in  60.926097s

Comparison:
              Simple:        0.9 i/s
               Given:        0.8 i/s - 1.06x  slower
@senhalil
Copy link
Author

Amazing! Thanks for following this through @dnesteryuk

@dnesteryuk
Copy link
Member

@senhalil did you try the master branch on your project?

@senhalil
Copy link
Author

senhalil commented Oct 29, 2020

@dnesteryuk tried with the same benchmark and it looks great 👇 Cheers!

((HEAD detached at origin/master))$ bundle exec ruby benchmark/large_model_with_given.rb
Warming up --------------------------------------
               Given     1.000  i/100ms
              Simple     1.000  i/100ms
Calculating -------------------------------------
               Given      1.452  (± 0.0%) i/s -     86.000  in  60.535269s
              Simple      1.517  (± 0.0%) i/s -     91.000  in  60.484706s

Comparison:
              Simple:        1.5 i/s
               Given:        1.5 i/s - 1.05x  (± 0.00) slower

@senhalil
Copy link
Author

senhalil commented Oct 29, 2020

@dnesteryuk @dblock I don't know how it can be done in a proper way which can be used in an automated way but if you would like to turn this benchmark into a proper benchmark or a test, let me know!

The lowest I could drop the duration of the benchmark is around a minute in total which is kind of too much but I am afraid anything shorter than this would not give reliable results (at least that's the case on my machine).

((HEAD detached at origin/master))$ time bundle exec ruby benchmark/large_model_with_given.rb
Warming up --------------------------------------
               Given     1.000  i/100ms
              Simple     1.000  i/100ms
Calculating -------------------------------------
               Given      0.847  (± 0.0%) i/s -     26.000  in  30.945069s
              Simple      0.883  (± 0.0%) i/s -     26.000  in  30.037589s

Comparison:
              Simple:        0.9 i/s
               Given:        0.8 i/s - 1.04x  (± 0.00) slower


real    1m11,613s
user    1m6,482s
sys     0m0,433s

@dblock
Copy link
Member

dblock commented Oct 30, 2020

We check-in these benchmarks into https://github.com/ruby-grape/grape/tree/master/benchmark, I wonder if there's a way to run them as part of CI and have them fail if the threshold is breached?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants