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

Retained Grape::Http::Headers in memory instead of new ones created. #1942

Merged
merged 1 commit into from
Dec 14, 2019

Conversation

ericproulx
Copy link
Contributor

I ran a memory profiling (memory_profiler gem) on my app by hitting our Grape api first. I found it odd that it retains a bunch of http verbs when they are defined as const in Grape::Http::Headers :

       354  "HEAD"
       353  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/router/route.rb:67
         1  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/router.rb:30

       352  "GET"
       351  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/router/route.rb:67
         1  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/router.rb:30

       168  "POST"
       107  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/router/route.rb:67
        60  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/api/instance.rb:230
         1  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/router.rb:30

       113  "PATCH"
        82  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/router/route.rb:67
        30  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/api/instance.rb:230
         1  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/router.rb:30

        50  "DELETE"
        46  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/router/route.rb:67
         3  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/api/instance.rb:230
         1  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/router.rb:30

        43  "PUT"
        34  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/router/route.rb:67
         8  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/api/instance.rb:230
         1  /Users/eproulx/.rbenv/versions/2.4.9/lib/ruby/gems/2.4.0/gems/grape-1.2.6/lib/grape/router.rb:30

I looked at the code and made a fix by detecting supported_methods instead of retaining the route method received.

Actual memory footprint on first call

Total allocated: 50532358 bytes (526479 objects)
Total retained:  6386850 bytes (17587 objects)

And with the improvement

Total allocated: 50453158 bytes (524521 objects)
Total retained:  6346610 bytes (16603 objects)

1958 less objects allocated (79 200 bytes)
984 less objects retained (40 240 bytes)

@grape-bot
Copy link

grape-bot commented Dec 9, 2019

1 Warning
⚠️ There’re library changes, but not tests. That’s OK as long as you’re refactoring existing code.

Generated by 🚫 danger

@dblock
Copy link
Member

dblock commented Dec 10, 2019

So for your example it's 80K. How many endpoints? How much slower is endpoint mounting?

@ericproulx
Copy link
Contributor Author

A lot of endpoints .. Is there an easyway to count them ? Like Grape::Api.endpoints.count ?
For the endpoint mounting, i'll benchmark my app boot time.

@@ -46,7 +46,7 @@ def compile!
end

def append(route)
map[route.request_method.to_s.upcase] << route
map[Grape::Http::Headers.find_supported_method(route.request_method.to_s)] << route
Copy link
Member

Choose a reason for hiding this comment

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

The request_method method comes from this place, so upcase isn't needed here 🙂

@@ -26,6 +26,10 @@ module Headers
HTTP_ACCEPT = 'HTTP_ACCEPT'

FORMAT = 'format'

def self.find_supported_method(route_method)
Copy link
Member

Choose a reason for hiding this comment

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

The name of the method is find supported method, and you are returning unsupported methods from it. I think this method should either .detect or return nil and the caller should || ....

Next, I think we should turn Grape::Http::Headers::SUPPORTED_METHODS into a case-insensitive hash and lookup with Grape::Http::Headers::SUPPORTED_METHODS[method], which will be a lot more efficient than detect.

@ericproulx
Copy link
Contributor Author

ericproulx commented Dec 10, 2019

@dblock true

# frozen_string_literal: true

require 'grape'
require 'benchmark/ips'

h = {
  Grape::Http::Headers::GET => Grape::Http::Headers::GET,
  Grape::Http::Headers::POST => Grape::Http::Headers::POST,
  Grape::Http::Headers::PUT => Grape::Http::Headers::PUT,
  Grape::Http::Headers::DELETE => Grape::Http::Headers::DELETE,
  Grape::Http::Headers::PATCH => Grape::Http::Headers::PATCH,
  Grape::Http::Headers::HEAD => Grape::Http::Headers::HEAD,
  Grape::Http::Headers::OPTIONS => Grape::Http::Headers::OPTIONS,
}


Benchmark.ips do |ips|
  ips.report('Hash with case insensitive') do
    h[Grape::Http::Headers::SUPPORTED_METHODS.sample]
  end
  ips.report('Detect') do
    Grape::Http::Headers.find_supported_method Grape::Http::Headers::SUPPORTED_METHODS.sample
  end
  ips.compare!
end
Warming up --------------------------------------
Hash with case insensitive
                       305.719k i/100ms
              Detect   108.135k i/100ms
Calculating -------------------------------------
Hash with case insensitive
                          7.792M (± 3.5%) i/s -     39.132M in   5.029709s
              Detect      1.374M (± 7.6%) i/s -      6.921M in   5.072976s

Comparison:
Hash with case insensitive:  7792031.6 i/s
  Detect:  1373815.6 i/s - 5.67x  slower

@dblock
Copy link
Member

dblock commented Dec 11, 2019

A lot of endpoints .. Is there an easyway to count them ? Like Grape::Api.endpoints.count ?

I am not sure, but I think .routes will get you started :)

For the endpoint mounting, i'll benchmark my app boot time.

Let us know when you're ready/happy with this PR! Looking forward to merge.

@ericproulx
Copy link
Contributor Author

ericproulx commented Dec 11, 2019

I dont know why but now I'm just 2x slower instead of almost 6

Warming up --------------------------------------
Hash with case insensitive
                       309.328k i/100ms
              detect   173.918k i/100ms
Calculating -------------------------------------
Hash with case insensitive
                          7.785M (± 4.9%) i/s -     38.975M in   5.021621s
              detect      2.699M (± 1.0%) i/s -     13.566M in   5.026560s

Comparison:
Hash with case insensitive:  7785176.2 i/s
              detect:  2699078.6 i/s - 2.88x  slower

Anyway, the reality is having a case-insensitive hash is really hard (no easy way). I mean we could upcase/downcase everything like before but we wont have a substantial gain (less memory usage) anymore. Also, since its a static array, worst case scenario is O(7). So, I'm not sure the 2x slower is a big issue here. One thing for sure, is right now having more endpoints, mean more memory usage which could be stop we this code.

@dblock
Copy link
Member

dblock commented Dec 11, 2019

I don't think it's a big issue, I was just trying to make this code simpler. Your call.

@@ -64,7 +64,8 @@ def route_path
end

def initialize(method, pattern, **options)
upcased_method = Grape::Http::Headers.find_supported_method(method.to_s)
str_method = method.to_s
Copy link
Member

Choose a reason for hiding this comment

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

Minor, maybe method_s and method_upcase is a better name/model for these variables. Don't care enough.

Changelog

Keep the original route.request_method since its already sanitized

Moved || outside of the function.

Renamed local variables
@dblock dblock merged commit 1d35559 into ruby-grape:master Dec 14, 2019
@dblock
Copy link
Member

dblock commented Dec 14, 2019

I merged this, thanks.

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.

None yet

4 participants