mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
use the strategy pattern to match request verbs
Rather than building a regexp for every route, lets use the strategy pattern to select among objects that can match HTTP verbs. This commit introduces strategy objects for each verb that has a predicate method on the request object like `get?`, `post?`, etc. When we build the route object, look up the strategy for the verbs the user specified. If we can't find it, fall back on string matching. Using a strategy / null object pattern (the `All` VerbMatcher is our "null" object in this case) we can: 1) Remove conditionals 2) Drop boot time allocations 2) Drop run time allocations 3) Improve runtime performance Here is our boot time allocation benchmark: ```ruby require 'action_pack' require 'action_dispatch' route_set = ActionDispatch::Routing::RouteSet.new routes = ActionDispatch::Routing::Mapper.new route_set result = ObjectSpace::AllocationTracer.trace do 500.times do routes.resources :foo end end sorted = ObjectSpace::AllocationTracer.allocated_count_table.sort_by(&:last) sorted.each do |k,v| next if v == 0 p k => v end __END__ Before: $ be ruby -rallocation_tracer route_test.rb {:T_SYMBOL=>11} {:T_REGEXP=>4017} {:T_STRUCT=>6500} {:T_MATCH=>12004} {:T_DATA=>84092} {:T_OBJECT=>99009} {:T_HASH=>122015} {:T_STRING=>216652} {:T_IMEMO=>355137} {:T_ARRAY=>441057} After: $ be ruby -rallocation_tracer route_test.rb {:T_SYMBOL=>11} {:T_REGEXP=>17} {:T_STRUCT=>6500} {:T_MATCH=>12004} {:T_DATA=>84092} {:T_OBJECT=>99009} {:T_HASH=>122015} {:T_STRING=>172647} {:T_IMEMO=>355136} {:T_ARRAY=>433056} ``` This benchmark adds 500 resources. Each resource has 8 routes, so it adds 4000 routes. You can see from the results that this patch eliminates 4000 Regexp allocations, ~44000 String allocations, and ~8000 Array allocations. With that, we can figure out that the previous code would allocate 1 regexp, 11 strings, and 2 arrays per route *more* than this patch in order to handle verb matching. Next lets look at runtime allocations: ```ruby require 'action_pack' require 'action_dispatch' require 'benchmark/ips' route_set = ActionDispatch::Routing::RouteSet.new routes = ActionDispatch::Routing::Mapper.new route_set routes.resources :foo route = route_set.routes.first request = ActionDispatch::Request.new("REQUEST_METHOD" => "GET") result = ObjectSpace::AllocationTracer.trace do 500.times do route.matches? request end end sorted = ObjectSpace::AllocationTracer.allocated_count_table.sort_by(&:last) sorted.each do |k,v| next if v == 0 p k => v end __END__ Before: $ be ruby -rallocation_tracer route_test.rb {:T_MATCH=>500} {:T_STRING=>501} {:T_IMEMO=>1501} After: $ be ruby -rallocation_tracer route_test.rb {:T_IMEMO=>1001} ``` This benchmark runs 500 calls against the `matches?` method on the route object. We check this method in the case that there are two methods that match the same path, but they are differentiated by the verb (or other conditionals). For example `POST /users` vs `GET /users`, same path, different action. Previously, we were using regexps to match against the verb. You can see that doing the regexp match would allocate 1 match object and 1 string object each time it was called. This patch eliminates those allocations. Next lets look at runtime performance. ```ruby require 'action_pack' require 'action_dispatch' require 'benchmark/ips' route_set = ActionDispatch::Routing::RouteSet.new routes = ActionDispatch::Routing::Mapper.new route_set routes.resources :foo route = route_set.routes.first match = ActionDispatch::Request.new("REQUEST_METHOD" => "GET") no_match = ActionDispatch::Request.new("REQUEST_METHOD" => "POST") Benchmark.ips do |x| x.report("match") do route.matches? match end x.report("no match") do route.matches? no_match end end __END__ Before: $ be ruby -rallocation_tracer runtime.rb Calculating ------------------------------------- match 17.145k i/100ms no match 24.244k i/100ms ------------------------------------------------- match 259.708k (± 4.3%) i/s - 1.303M no match 453.376k (± 5.9%) i/s - 2.279M After: $ be ruby -rallocation_tracer runtime.rb Calculating ------------------------------------- match 23.958k i/100ms no match 29.402k i/100ms ------------------------------------------------- match 465.063k (± 3.8%) i/s - 2.324M no match 691.956k (± 4.5%) i/s - 3.469M ``` This tests tries to see how many times it can match a request per second. Switching to method calls and string comparison makes the successful match case about 79% faster, and the unsuccessful case about 52% faster. That was fun!
This commit is contained in:
parent
c989e2c56f
commit
0b476de445
2 changed files with 49 additions and 16 deletions
|
@ -8,9 +8,49 @@ module ActionDispatch
|
|||
|
||||
attr_accessor :precedence
|
||||
|
||||
ANY = //
|
||||
module VerbMatchers
|
||||
VERBS = %w{ DELETE GET HEAD OPTIONS LINK PATCH POST PUT TRACE UNLINK }
|
||||
VERBS.each do |v|
|
||||
class_eval <<-eoc
|
||||
class #{v}
|
||||
def self.verb; name.split("::").last; end
|
||||
def self.call(req); req.#{v.downcase}?; end
|
||||
end
|
||||
eoc
|
||||
end
|
||||
|
||||
class Unknown
|
||||
attr_reader :verb
|
||||
|
||||
def initialize(verb)
|
||||
@verb = verb
|
||||
end
|
||||
|
||||
def call(request); @verb === request.request_method; end
|
||||
end
|
||||
|
||||
class All
|
||||
def self.call(_); true; end
|
||||
def self.verb; ''; end
|
||||
end
|
||||
|
||||
VERB_TO_CLASS = VERBS.each_with_object({ :all => All }) do |verb, hash|
|
||||
klass = const_get verb
|
||||
hash[verb] = klass
|
||||
hash[verb.downcase] = klass
|
||||
hash[verb.downcase.to_sym] = klass
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def self.verb_matcher(verb)
|
||||
VerbMatchers::VERB_TO_CLASS.fetch(verb) do
|
||||
VerbMatchers::Unknown.new verb.to_s.dasherize.upcase
|
||||
end
|
||||
end
|
||||
|
||||
def self.build(name, app, path, constraints, required_defaults, defaults)
|
||||
request_method_match = constraints.delete(:request_method) || ANY
|
||||
request_method_match = verb_matcher(constraints.delete(:request_method)) || []
|
||||
new name, app, path, constraints, required_defaults, defaults, request_method_match
|
||||
end
|
||||
|
||||
|
@ -121,18 +161,20 @@ module ActionDispatch
|
|||
end
|
||||
|
||||
def requires_matching_verb?
|
||||
@request_method_match != ANY
|
||||
!@request_method_match.all? { |x| x == VerbMatchers::All }
|
||||
end
|
||||
|
||||
def verb
|
||||
@request_method_match
|
||||
%r[^#{verbs.join('|')}$]
|
||||
end
|
||||
|
||||
private
|
||||
def verbs
|
||||
@request_method_match.map(&:verb)
|
||||
end
|
||||
|
||||
def match_verb(request)
|
||||
return true unless requires_matching_verb?
|
||||
verb === request.request_method
|
||||
@request_method_match.any? { |m| m.call request }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -178,16 +178,7 @@ module ActionDispatch
|
|||
private :build_conditions
|
||||
|
||||
def request_method
|
||||
# Rack-Mount requires that :request_method be a regular expression.
|
||||
# :request_method represents the HTTP verb that matches this route.
|
||||
#
|
||||
# Here we munge values before they get sent on to rack-mount.
|
||||
if @via == [:all]
|
||||
//
|
||||
else
|
||||
verbs = @via.map { |m| m.to_s.dasherize.upcase }
|
||||
%r[^#{verbs.join('|')}$]
|
||||
end
|
||||
@via.map { |x| Journey::Route.verb_matcher(x) }
|
||||
end
|
||||
private :request_method
|
||||
|
||||
|
|
Loading…
Reference in a new issue