Permalink
Browse files

Optimise named route generation when using positional arguments. Closes

#9450 [Koz]

  This change delivers significant performance benefits for the most
  common usage scenarios for modern rails applications by avoiding the
  costly trip through url_for.  Initial benchmarks indicate this is
  between 6 and 20 times as fast.


git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@7421 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
1 parent 7ace0a6 commit 80ff0b9f1c07eae7668680fd12335ffa218e53ac @NZKoz NZKoz committed Sep 9, 2007
View
@@ -1,5 +1,12 @@
*SVN*
+* Optimise named route generation when using positional arguments. [Koz]
+
+ This change delivers significant performance benefits for the most
+ common usage scenarios for modern rails applications by avoiding the
+ costly trip through url_for. Initial benchmarks indicate this is
+ between 6 and 20 times as fast.
+
* Explicitly require active_record/query_cache before using it. [Jeremy Kemper]
* Fix layout overriding response status. #9476 [lotswholetime]
@@ -74,6 +74,10 @@ def reset!
unless defined? @named_routes_configured
# install the named routes in this session instance.
+ # But we have to disable the optimisation code so that we can
+ # generate routes without @request being initialized
+ Routing.optimise_named_routes=false
+ Routing::Routes.reload!
klass = class<<self; self; end
Routing::Routes.install_helpers(klass)
@@ -1,6 +1,7 @@
require 'cgi'
require 'uri'
require 'action_controller/polymorphic_routes'
+require 'action_controller/routing_optimisation'
class Object
def to_param
@@ -255,6 +256,11 @@ module Routing
mattr_accessor :controller_paths
self.controller_paths = []
+ # Indicates whether or not optimise the generated named
+ # route helper methods
+ mattr_accessor :optimise_named_routes
+ self.optimise_named_routes = true
+
# A helper module to hold URL related helpers.
module Helpers
include PolymorphicRoutes
@@ -325,13 +331,25 @@ def controller_relative_to(controller, previous)
end
class Route #:nodoc:
- attr_accessor :segments, :requirements, :conditions
+ attr_accessor :segments, :requirements, :conditions, :optimise
def initialize
@segments = []
@requirements = {}
@conditions = {}
end
+
+ # Indicates whether the routes should be optimised with the string interpolation
+ # version of the named routes methods.
+ def optimise?
+ @optimise
+ end
+
+ def segment_keys
+ segments.collect do |segment|
+ segment.key if segment.respond_to? :key
+ end.compact
+ end
# Write and compile a +generate+ method for this Route.
def write_generation
@@ -381,6 +399,7 @@ def generation_requirements
end
requirement_conditions * ' && ' unless requirement_conditions.empty?
end
+
def generation_structure
segments.last.string_structure segments[0..-2]
end
@@ -977,9 +996,15 @@ def build(path, options)
requirements = assign_route_options(segments, defaults, requirements)
route = Route.new
+
route.segments = segments
route.requirements = requirements
route.conditions = conditions
+
+ # Routes cannot use the current string interpolation method
+ # if there are user-supplied :requirements as the interpolation
+ # code won't raise RoutingErrors when generating
+ route.optimise = !options.key?(:requirements) && ActionController::Routing.optimise_named_routes
if !route.significant_keys.include?(:action) && !route.requirements[:action]
route.requirements[:action] = "index"
@@ -1051,7 +1076,7 @@ def method_missing(route_name, *args, &proc)
# named routes.
class NamedRouteCollection #:nodoc:
include Enumerable
-
+ include ActionController::Routing::Optimisation
attr_reader :routes, :helpers
def initialize
@@ -1128,15 +1153,15 @@ def #{selector}(options = nil)
def define_url_helper(route, name, kind, options)
selector = url_helper_name(name, kind)
-
# The segment keys used for positional paramters
- segment_keys = route.segments.collect do |segment|
- segment.key if segment.respond_to? :key
- end.compact
+
hash_access_method = hash_access_name(name, kind)
@module.send :module_eval, <<-end_eval # We use module_eval to avoid leaks
def #{selector}(*args)
+
+ #{generate_optimisation_block(route, kind)}
+
opts = if args.empty? || Hash === args.first
args.first || {}
else
@@ -1154,7 +1179,7 @@ def #{selector}(*args)
# foo_url(bar, baz, bang, :sort_by => 'baz')
#
options = args.last.is_a?(Hash) ? args.pop : {}
- args = args.zip(#{segment_keys.inspect}).inject({}) do |h, (v, k)|
+ args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)|
h[k] = v
h
end
@@ -1167,7 +1192,6 @@ def #{selector}(*args)
@module.send(:protected, selector)
helpers << selector
end
-
end
attr_accessor :routes, :named_routes
@@ -0,0 +1,99 @@
+module ActionController
+ module Routing
+ # Much of the slow performance from routes comes from the
+ # complexity of expiry, :requirements matching, defaults providing
+ # and figuring out which url pattern to use. With named routes
+ # we can avoid the expense of finding the right route. So if
+ # they've provided the right number of arguments, and have no
+ # :requirements, we can just build up a string and return it.
+ #
+ # To support building optimisations for other common cases, the
+ # generation code is seperated into several classes
+ module Optimisation
+ def generate_optimisation_block(route, kind)
+ return "" unless route.optimise?
+ OPTIMISERS.inject("") do |memo, klazz|
+ optimiser = klazz.new(route, kind)
+ memo << "return #{optimiser.generation_code} if #{optimiser.guard_condition}\n"
+ end
+ end
+
+ class Optimiser
+ attr_reader :route, :kind
+ def initialize(route, kind)
+ @route = route
+ @kind = kind
+ end
+
+ def guard_condition
+ 'false'
+ end
+
+ def generation_code
+ 'nil'
+ end
+ end
+
+ # Given a route:
+ # map.person '/people/:id'
+ #
+ # If the user calls person_url(@person), we can simply
+ # return a string like "/people/#{@person.to_param}"
+ # rather than triggering the expensive logic in url_for
+ class PositionalArguments < Optimiser
+ def guard_condition
+ number_of_arguments = route.segment_keys.size
+ # if they're using foo_url(:id=>2) it's one
+ # argument, but we don't want to generate /foos/id2
+ if number_of_arguments == 1
+ "args.size == 1 && !args.first.is_a?(Hash)"
+ else
+ "args.size == #{number_of_arguments}"
+ end
+ end
+
+ def generation_code
+ elements = []
+ idx = 0
+
+
+ if kind == :url
+ elements << '#{request.protocol}'
+ elements << '#{request.host_with_port}'
+ end
+
+ # The last entry in route.segments appears to
+ # *always* be a 'divider segment' for '/'
+ # but we have assertions to ensure that we don't
+ # include the trailing slashes, so skip them
+ route.segments[0..-2].each do |segment|
+ if segment.is_a?(DynamicSegment)
+ elements << "\#{URI.escape(args[#{idx}].to_param, ActionController::Routing::Segment::UNSAFE_PCHAR)}"
+ idx += 1
+ else
+ elements << segment.to_s
+ end
+ end
+ %("#{elements * ''}")
+ end
+ end
+
+ # This case is mostly the same as the positional arguments case
+ # above, but it supports additional query parameters as the last
+ # argument
+ class PositionalArgumentsWithAdditionalParams < PositionalArguments
+ def guard_condition
+ "args.size == #{route.segment_keys.size + 1}"
+ end
+
+ # This case uses almost the Use the same code as positional arguments,
+ # but add an args.last.to_query on the end
+ def generation_code
+ super.insert(-2, '?#{args.last.to_query}')
+ end
+ end
+
+ OPTIMISERS = [PositionalArguments, PositionalArgumentsWithAdditionalParams]
+ end
+ end
+end
@@ -26,6 +26,18 @@ class ProductsController < ResourcesController; end
end
class ResourcesTest < Test::Unit::TestCase
+
+
+ # The assertions in these tests are incompatible with the hash method
+ # optimisation. This could indicate user level problems
+ def setup
+ ActionController::Routing.optimise_named_routes = false
+ end
+
+ def tear_down
+ ActionController::Routing.optimise_named_routes = true
+ end
+
def test_should_arrange_actions
resource = ActionController::Resources::Resource.new(:messages,
:collection => { :rss => :get, :reorder => :post, :csv => :post },
Oops, something went wrong.

0 comments on commit 80ff0b9

Please sign in to comment.