aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/journey/route.rb
diff options
context:
space:
mode:
authorAaron Patterson <aaron.patterson@gmail.com>2015-08-17 18:17:55 -0700
committerAaron Patterson <aaron.patterson@gmail.com>2015-08-17 18:17:55 -0700
commit0b476de445faf330c58255e2ec3eea0f3a7c1bfc (patch)
treee36e859ffad8552cbd0e217b9f432b0f2d746ddb /actionpack/lib/action_dispatch/journey/route.rb
parentc989e2c56f415e0a4429b1348e76be4fc8e9f35b (diff)
downloadrails-0b476de445faf330c58255e2ec3eea0f3a7c1bfc.tar.gz
rails-0b476de445faf330c58255e2ec3eea0f3a7c1bfc.tar.bz2
rails-0b476de445faf330c58255e2ec3eea0f3a7c1bfc.zip
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!
Diffstat (limited to 'actionpack/lib/action_dispatch/journey/route.rb')
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb54
1 files changed, 48 insertions, 6 deletions
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
index fa69ba776a..5d5fe4698d 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -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