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

Replace rack-mount with new router #1276

Merged
merged 1 commit into from
Mar 14, 2016

Conversation

namusyaka
Copy link
Contributor

This is a first step for replcaing rack-mount with new router.
I'm going to improve performance to realize faster new router.

The new router has a few imcompatibility with old one.
The following cases:

  • route_* attribute should be deprecated because the feature is implemented by using method_missing. So it's very slow. (However, reviving the attribute is possible. )
  • Changed rack.routing_args to api.routing_args. The new router is for the Grape. I think the namespace is incorrect.

Thoughts?

@namusyaka namusyaka force-pushed the implement-new-router branch 4 times, most recently from e4d1a14 to 33fdcbd Compare February 14, 2016 14:12
@dblock
Copy link
Member

dblock commented Feb 14, 2016

This is excellent work. I am excited for getting this onto Grape master soon. It's really important to build some benchmarks and see how much improvement this is if at all, I think we owe this to the Grape community for switching something as essential as the router.

@namusyaka
Copy link
Contributor Author

All required tests has been successful now.

@arempe93
Copy link
Contributor

👍 excited to see benchmarks

@namusyaka namusyaka force-pushed the implement-new-router branch 2 times, most recently from 1f0876a to 3a25cca Compare February 21, 2016 14:58
@namusyaka
Copy link
Contributor Author

I've been tring to improve performance of the new router, and then I'd like to report a few points and hear your opinions.

Benchmark

First of all, I want you to know that the performance of rack-mount is excellent.
So I focused reducing memory utilization, and it was successful.

Please look at the following benchmark code and its results.

Sample code

Environment: Mac OS X, Ruby2.3

require 'rack'
require 'grape'
require 'benchmark'
require 'objspace'
require 'memprof2'

def memory_utilization
  Memprof2.start
  yield
  profiler = Memprof2.new
  profiler.configure
  mem = profiler.collect_info.values.inject(&:+)
  Memprof2.stop
  puts "Memory Utilization: #{mem/(1024.0 * 1024.0)}MB"
end

req1 = Rack::MockRequest.env_for('/100')
req2 = Rack::MockRequest.env_for('/users/1234', method: 'PUT')
req3 = Rack::MockRequest.env_for('/sources/hoge/aiueohogefuga', method: 'POST')
req4 = Rack::MockRequest.env_for('/sources/hoge/aiueohogefuga', method: 'PUT')

app = Class.new(Grape::API)
memory_utilization do

  500.times do |n|
    app.get("/#{n}") { n.to_s }
  end

  app.get('/') { 'hello' }
  app.put('/users/:id') { 'hello' }
  app.post('/sources', anchor: false) { 'hello' }

  app.call(req1)
  app.call(req2)
  app.call(req3)
  app.call(req4)
end

Benchmark.bm do |x|
  x.report do
    app.call(req1)
  end

  x.report do
    app.call(req2)
  end

  x.report do
    app.call(req3)
  end

  x.report do
    app.call(req4)
  end
end

rack-mount

Memory Utilization: 32.22776699066162MB
       user     system      total        real
   0.000000   0.000000   0.000000 (  0.000638)
   0.000000   0.000000   0.000000 (  0.000393)
   0.000000   0.000000   0.000000 (  0.000249)
   0.000000   0.000000   0.000000 (  0.000061)

new router

Memory Utilization: 14.685898780822754MB
       user     system      total        real
   0.000000   0.000000   0.000000 (  0.000495)
   0.000000   0.000000   0.000000 (  0.000263)
   0.000000   0.000000   0.000000 (  0.000231)
   0.000000   0.000000   0.000000 (  0.000534)

@dblock @arempe93 What do you think? Any ideas?

@dblock
Copy link
Member

dblock commented Feb 22, 2016

Do I read this correctly that the new router consumes half the memory and performs 30% faster (except for that last row which seems like an outlier for both?).

I think you want to make a lot of those "http" calls, at the very least those routers could have a cold start.

@namusyaka
Copy link
Contributor Author

The new router consumes half the memory, but I think its performance is ambiguous.
Maybe the performance will be on the same level with rack-mount.

Okay, I tried to calculate benchmark for calling a lot of requests.
Please look at this sample code.

require 'rack'
require 'grape'
require 'benchmark'
require 'benchmark/ips'
require 'objspace'
require 'memprof2'

def memory_utilization
  Memprof2.start
  yield
  profiler = Memprof2.new
  profiler.configure
  mem = profiler.collect_info.values.inject(&:+)
  Memprof2.stop
  puts "Memory Utilization: #{mem/(1024.0 * 1024.0)}MB"
end

req1 = Rack::MockRequest.env_for('/100')
req2 = Rack::MockRequest.env_for('/users/1234', method: 'PUT')
req3 = Rack::MockRequest.env_for('/sources/hoge/aiueohogefuga', method: 'POST')
req4 = Rack::MockRequest.env_for('/sources/hoge/aiueohogefuga', method: 'PUT')

app = Class.new(Grape::API)
memory_utilization do

  500.times do |n|
    app.get("/#{n}") { n.to_s }
  end

  app.get('/') { 'hello' }
  app.put('/users/:id') { 'hello' }
  app.post('/sources', anchor: false) { 'hello' }

  app.call(req1)
  app.call(req2)
  app.call(req3)
  app.call(req4)
end

Benchmark.ips do |x|
  [req1, req2, req3, req4].each do |req|
    x.report { app.call(req) }
  end
end

Also, the results are:

rack-mount

Memory Utilization: 32.254828453063965MB
Calculating -------------------------------------
                       531.000  i/100ms
                       505.000  i/100ms
                       478.000  i/100ms
                         2.558k i/100ms
-------------------------------------------------
                          5.870k (±12.8%) i/s -     28.674k
                          5.507k (± 7.9%) i/s -     27.775k
                          5.594k (± 7.1%) i/s -     28.202k
                         46.889k (± 9.6%) i/s -    232.778k

new router

Memory Utilization: 14.55474853515625MB
Calculating -------------------------------------
                       396.000  i/100ms
                       552.000  i/100ms
                       561.000  i/100ms
                       259.000  i/100ms
-------------------------------------------------
                          4.008k (± 7.4%) i/s -     20.196k
                          5.638k (± 4.2%) i/s -     28.704k
                          5.417k (± 4.5%) i/s -     27.489k
                          2.650k (± 4.2%) i/s -     13.468k

@@ -244,7 +244,7 @@ module Grape
allow(path).to receive(:uses_specific_format?) { true }
allow(path).to receive(:settings) { { format: :json } }

expect(path.path_with_suffix).to eql('/the/path(.json)')
expect(path.path_with_suffix).to eql('/the/path(.:format)')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please explain this one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, this is my bad. Fixed.

@dblock
Copy link
Member

dblock commented Feb 22, 2016

I will run this against a production codebase to see if anything fails. Generally I am down with merging this, properly READMEed, CHANGELOGed, squashed, etc., please. We need to replace rack-mount since it's a dead project.

@dblock
Copy link
Member

dblock commented Feb 22, 2016

Haven't debugged, but the first failure in a real world project is with grape-swagger.

/Users/dblock/.rvm/gems/ruby-2.2.2/bundler/gems/grape-swagger-2f9aa49fc2e4/lib/grape-swagger.rb:27:in `block in add_swagger_documentation': undefined method `split' for nil:NilClass (NoMethodError)
    from /Users/dblock/.rvm/gems/ruby-2.2.2/bundler/gems/grape-swagger-2f9aa49fc2e4/lib/grape-swagger.rb:25:in `each'
    from /Users/dblock/.rvm/gems/ruby-2.2.2/bundler/gems/grape-swagger-2f9aa49fc2e4/lib/grape-swagger.rb:25:in `add_swagger_documentation'
    from /Users/dblock/source/artsy/gravity/dblock/app/api/v1/root_endpoint.rb:237:in `<class:RootEndpoint>'
    from /Users/dblock/source/artsy/gravity/dblock/app/api/v1/root_endpoint.rb:3:in `<module:V1>'
    from /Users/dblock/source/artsy/gravity/dblock/app/api/v1/root_endpoint.rb:2:in `<module:Api>'
    from /Users/dblock/source/artsy/gravity/dblock/app/api/v1/root_endpoint.rb:1:in `<top (required)>'

It's missing routes.

@namusyaka
Copy link
Contributor Author

@dblock Sorry for the failing with grape-swagger.
This failure is caused by breaking comatibility of route_* attrs. So I fixed the problems.
Maybe you can use it now.

methods_per_path[route.route_path] ||= []
methods_per_path[route.route_path] << route.route_method
# using the :any shorthand produces [nil] for route methods, substitute all manually
route_settings = (routes_map[route.pattern.to_regexp] ||= {})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not in love with this ||= assignment inside the right hand side. It's just a little difficult to read.

@namusyaka
Copy link
Contributor Author

Done, thanks for pointing out!

@dblock
Copy link
Member

dblock commented Feb 25, 2016

First regression. We need specs on master for this since it wasn't caught in the Grape specs and I got it from my real world project, would appreciate a separate PR just for this story on master.

require 'spec_helper'

describe Grape::API do
  subject do
    Class.new(Grape::API) do
      format :json

      get ':id' do
        params
      end
    end
  end

  def app
    subject
  end

  it 'does not include extension in id' do
    get '/foo.bar'
    expect(last_response.status).to eq 404
  end
end

This succeeds on master, fails on your branch with a 200, and returns a value of foo.bar for id, which is definitely wrong since in this case .bar should be interpreted as an extension causing a format selection.

Note that it works consistently without a format, ie. this passes:

require 'spec_helper'

describe Grape::API do
  subject do
    Class.new(Grape::API) do
      get ':id' do
        params.to_json
      end
    end
  end

  def app
    subject
  end

  it 'does not include extension in id' do
    get '/foo.bar'
    expect(last_response.status).to eq 200
    expect(last_response.body).to eq({ id: 'foo', format: 'bar' }.to_json)
  end
end

@dblock
Copy link
Member

dblock commented Feb 25, 2016

Do you think we can preserve route_xyz methods, possibly with a deprecation warning?


def transaction(env)
input, method, routing_args = *extract_required_args(env)
response = yield(input, method, ROUTING_ARGS_SETTER.curry[env, routing_args])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain what this magic is? :) This is beyond my Ruby-fu.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I removed the tricky code ;)

dblock added a commit to dblock/grape that referenced this pull request Mar 14, 2016
namusyaka added a commit to namusyaka/grape that referenced this pull request Mar 14, 2016
dblock added a commit that referenced this pull request Mar 14, 2016
@namusyaka
Copy link
Contributor Author

@dblock Thanks for your spec, I've just fixed the behavior and squashed.
Could you please check it out?

@dblock
Copy link
Member

dblock commented Mar 14, 2016

Running my battery of tests ... you can write UPGRADING in the meantime maybe. Thanks.

```

Note that `Route#route_xyz` methods have been deprecated since 0.15.0.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in UPGRADING.

@dblock
Copy link
Member

dblock commented Mar 14, 2016

From my tests ... this should be in UPGRADINGn and possibly in code: route.route_method was replaced by route.request_method, not route.method as per the deprecation.

@dblock
Copy link
Member

dblock commented Mar 14, 2016

Another tricky one, namusyaka#2.

path, line = *location.scan(SOURCE_LOCATION_REGEXP).first
path = File.realpath(path) if Pathname.new(path).relative?
warn <<-EOS
#{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{name}.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will say the wrong thing for route_method ;)

@namusyaka
Copy link
Contributor Author

@dblock Thank you for polite review! Fixed.

@namusyaka
Copy link
Contributor Author

If the changes have no problem, I'm going to squashed them.

@dblock
Copy link
Member

dblock commented Mar 14, 2016

Finished in 46 minutes 33 seconds (files took 49.5 seconds to load)
4712 examples, 0 failures, 28 pending

Lets squash it and get it merged!

@namusyaka
Copy link
Contributor Author

Wow, great! I'm so happy! Thank you!

dblock added a commit that referenced this pull request Mar 14, 2016
@dblock dblock merged commit 2166237 into ruby-grape:master Mar 14, 2016
@dblock
Copy link
Member

dblock commented Mar 14, 2016

Merged.

@arempe93
Copy link
Contributor

🎉

@kuraga
Copy link
Contributor

kuraga commented Mar 29, 2016

Is this Pendragon or... ?..

@dblock
Copy link
Member

dblock commented Mar 30, 2016

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

Successfully merging this pull request may close these issues.

4 participants