diff options
author | Michael Koziarski <michael@koziarski.com> | 2007-09-09 00:18:55 +0000 |
---|---|---|
committer | Michael Koziarski <michael@koziarski.com> | 2007-09-09 00:18:55 +0000 |
commit | 80ff0b9f1c07eae7668680fd12335ffa218e53ac (patch) | |
tree | 42307295c7d16ee3a778ec74b6f3becb0b5fb2ab /actionpack/lib | |
parent | 7ace0a654c46cf9ca57f42981c826eace21fa42c (diff) | |
download | rails-80ff0b9f1c07eae7668680fd12335ffa218e53ac.tar.gz rails-80ff0b9f1c07eae7668680fd12335ffa218e53ac.tar.bz2 rails-80ff0b9f1c07eae7668680fd12335ffa218e53ac.zip |
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
Diffstat (limited to 'actionpack/lib')
-rw-r--r-- | actionpack/lib/action_controller/integration.rb | 4 | ||||
-rw-r--r-- | actionpack/lib/action_controller/routing.rb | 40 | ||||
-rw-r--r-- | actionpack/lib/action_controller/routing_optimisation.rb | 99 |
3 files changed, 135 insertions, 8 deletions
diff --git a/actionpack/lib/action_controller/integration.rb b/actionpack/lib/action_controller/integration.rb index 2bff3a7ae0..80602489b9 100644 --- a/actionpack/lib/action_controller/integration.rb +++ b/actionpack/lib/action_controller/integration.rb @@ -74,6 +74,10 @@ module ActionController 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) diff --git a/actionpack/lib/action_controller/routing.rb b/actionpack/lib/action_controller/routing.rb index 4cf9b010ba..d20c17c30e 100644 --- a/actionpack/lib/action_controller/routing.rb +++ b/actionpack/lib/action_controller/routing.rb @@ -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 ActionController 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 @@ module ActionController 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 @@ module ActionController end requirement_conditions * ' && ' unless requirement_conditions.empty? end + def generation_structure segments.last.string_structure segments[0..-2] end @@ -977,9 +996,15 @@ module ActionController 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 @@ module ActionController # named routes. class NamedRouteCollection #:nodoc: include Enumerable - + include ActionController::Routing::Optimisation attr_reader :routes, :helpers def initialize @@ -1128,15 +1153,15 @@ module ActionController 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 @@ module ActionController # 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 @@ module ActionController @module.send(:protected, selector) helpers << selector end - end attr_accessor :routes, :named_routes diff --git a/actionpack/lib/action_controller/routing_optimisation.rb b/actionpack/lib/action_controller/routing_optimisation.rb new file mode 100644 index 0000000000..535f1bed63 --- /dev/null +++ b/actionpack/lib/action_controller/routing_optimisation.rb @@ -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
\ No newline at end of file |