Merge pull request #5242 from rails/opt_routes

Optimize routes generation in simple cases.

If you pass to the route helper the same amount of arguments
as the required segments, route generation will be optimized
as a string interpolation. After this commit, `post_path(post)`
is about 6.5 times faster, `post_url(post)` is about 5 times.
This commit is contained in:
José Valim 2012-03-02 06:56:40 -08:00
commit 1a046ab9bf
9 changed files with 107 additions and 48 deletions

View File

@ -1,7 +1,7 @@
# Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing # Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing
# the <tt>_routes</tt> method. Otherwise, an exception will be raised. # the <tt>_routes</tt> method. Otherwise, an exception will be raised.
# #
# In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define # In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define
# url options like the +host+. In order to do so, this module requires the host class # url options like the +host+. In order to do so, this module requires the host class
# to implement +env+ and +request+, which need to be a Rack-compatible. # to implement +env+ and +request+, which need to be a Rack-compatible.
# #
@ -18,7 +18,7 @@
# @url = root_path # named route from the application. # @url = root_path # named route from the application.
# end # end
# end # end
# #
module ActionController module ActionController
module UrlFor module UrlFor
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -26,22 +26,20 @@ module ActionController
include AbstractController::UrlFor include AbstractController::UrlFor
def url_options def url_options
@_url_options ||= super.reverse_merge( @_url_options ||= begin
:host => request.host, hash = super.reverse_merge(
:port => request.optional_port, :host => request.host,
:protocol => request.protocol, :port => request.optional_port,
:_path_segments => request.symbolized_path_parameters :protocol => request.protocol,
).freeze :_path_segments => request.symbolized_path_parameters
)
if _routes.equal?(env["action_dispatch.routes"]) if _routes.equal?(env["action_dispatch.routes"])
@_url_options.dup.tap do |options| hash[:script_name] = request.script_name.dup
options[:script_name] = request.script_name.dup
options.freeze
end end
else
@_url_options hash.freeze
end end
end end
end end
end end

View File

@ -40,7 +40,9 @@ module ActionDispatch
rewritten_url << ":#{options.delete(:port)}" if options[:port] rewritten_url << ":#{options.delete(:port)}" if options[:port]
end end
path = options.delete(:path) || '' path = ""
path << options.delete(:script_name).to_s.chomp("/")
path << options.delete(:path).to_s
params = options[:params] || {} params = options[:params] || {}
params.reject! {|k,v| v.to_param.nil? } params.reject! {|k,v| v.to_param.nil? }

View File

@ -446,7 +446,11 @@ module ActionDispatch
_route = @set.named_routes.routes[name.to_sym] _route = @set.named_routes.routes[name.to_sym]
_routes = @set _routes = @set
app.routes.define_mounted_helper(name) app.routes.define_mounted_helper(name)
app.routes.class_eval do app.routes.singleton_class.class_eval do
define_method :mounted? do
true
end
define_method :_generate_prefix do |options| define_method :_generate_prefix do |options|
prefix_options = options.slice(*_route.segment_keys) prefix_options = options.slice(*_route.segment_keys)
# we must actually delete prefix segment keys to avoid passing them to next url_for # we must actually delete prefix segment keys to avoid passing them to next url_for

View File

@ -191,14 +191,50 @@ module ActionDispatch
selector = url_helper_name(name, kind) selector = url_helper_name(name, kind)
hash_access_method = hash_access_name(name, kind) hash_access_method = hash_access_name(name, kind)
@module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 if optimize_helper?(kind, route)
remove_possible_method :#{selector} @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
def #{selector}(*args) remove_possible_method :#{selector}
url_for(#{hash_access_method}(*args)) def #{selector}(*args)
end if args.size == #{route.required_parts.size} && !args.last.is_a?(Hash) && optimize_routes_generation?
END_EVAL options = #{options.inspect}.merge!(url_options)
options[:path] = "#{optimized_helper(route)}"
ActionDispatch::Http::URL.url_for(options)
else
url_for(#{hash_access_method}(*args))
end
end
END_EVAL
else
@module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
remove_possible_method :#{selector}
def #{selector}(*args)
url_for(#{hash_access_method}(*args))
end
END_EVAL
end
helpers << selector helpers << selector
end end
# Clause check about when we need to generate an optimized helper.
def optimize_helper?(kind, route) #:nodoc:
route.ast.grep(Journey::Nodes::Star).empty? && route.requirements.except(:controller, :action).empty?
end
# Generates the interpolation to be used in the optimized helper.
def optimized_helper(route)
string_route = route.ast.to_s
while string_route.gsub!(/\([^\)]*\)/, "")
true
end
route.required_parts.each_with_index do |part, i|
string_route.gsub!(part.inspect, "\#{Journey::Router::Utils.escape_fragment(args[#{i}].to_param)}")
end
string_route
end
end end
attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :formatter, :set, :named_routes, :default_scope, :router
@ -323,7 +359,7 @@ module ActionDispatch
# Rails.application.routes.url_helpers.url_for(args) # Rails.application.routes.url_helpers.url_for(args)
@_routes = routes @_routes = routes
class << self class << self
delegate :url_for, :to => '@_routes' delegate :url_for, :optimize_routes_generation?, :to => '@_routes'
end end
# Make named_routes available in the module singleton # Make named_routes available in the module singleton
@ -557,6 +593,14 @@ module ActionDispatch
RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length, RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
:trailing_slash, :anchor, :params, :only_path, :script_name] :trailing_slash, :anchor, :params, :only_path, :script_name]
def mounted?
false
end
def optimize_routes_generation?
!mounted? && default_url_options.empty?
end
def _generate_prefix(options = {}) def _generate_prefix(options = {})
nil nil
end end
@ -568,19 +612,17 @@ module ActionDispatch
user, password = extract_authentication(options) user, password = extract_authentication(options)
path_segments = options.delete(:_path_segments) path_segments = options.delete(:_path_segments)
script_name = options.delete(:script_name) script_name = options.delete(:script_name).presence || _generate_prefix(options)
path = (script_name.blank? ? _generate_prefix(options) : script_name.chomp('/')).to_s
path_options = options.except(*RESERVED_OPTIONS) path_options = options.except(*RESERVED_OPTIONS)
path_options = yield(path_options) if block_given? path_options = yield(path_options) if block_given?
path_addition, params = generate(path_options, path_segments || {}) path, params = generate(path_options, path_segments || {})
path << path_addition
params.merge!(options[:params] || {}) params.merge!(options[:params] || {})
ActionDispatch::Http::URL.url_for(options.merge!({ ActionDispatch::Http::URL.url_for(options.merge!({
:path => path, :path => path,
:script_name => script_name,
:params => params, :params => params,
:user => user, :user => user,
:password => password :password => password

View File

@ -102,6 +102,9 @@ module ActionDispatch
super super
end end
# Hook overriden in controller to add request information
# with `default_url_options`. Application logic should not
# go into url_options.
def url_options def url_options
default_url_options default_url_options
end end
@ -152,6 +155,11 @@ module ActionDispatch
protected protected
def optimize_routes_generation?
return @_optimized_routes if defined?(@_optimized_routes)
@_optimized_routes = _routes.optimize_routes_generation? && default_url_options.empty?
end
def _with_routes(routes) def _with_routes(routes)
old_routes, @_routes = @_routes, routes old_routes, @_routes = @_routes, routes
yield yield

View File

@ -23,20 +23,25 @@ module ActionView
include ActionDispatch::Routing::UrlFor include ActionDispatch::Routing::UrlFor
include TagHelper include TagHelper
def _routes_context # We need to override url_optoins, _routes_context
controller # and optimize_routes_generation? to consider the controller.
end
# Need to map default url options to controller one. def url_options #:nodoc:
# def default_url_options(*args) #:nodoc:
# controller.send(:default_url_options, *args)
# end
#
def url_options
return super unless controller.respond_to?(:url_options) return super unless controller.respond_to?(:url_options)
controller.url_options controller.url_options
end end
def _routes_context #:nodoc:
controller
end
protected :_routes_context
def optimize_routes_generation? #:nodoc:
controller.respond_to?(:optimize_routes_generation?) ?
controller.optimize_routes_generation? : super
end
protected :optimize_routes_generation?
# Returns the URL for the set of +options+ provided. This takes the # Returns the URL for the set of +options+ provided. This takes the
# same options as +url_for+ in Action Controller (see the # same options as +url_for+ in Action Controller (see the
# documentation for <tt>ActionController::Base#url_for</tt>). Note that by default # documentation for <tt>ActionController::Base#url_for</tt>). Note that by default

View File

@ -56,7 +56,7 @@ class UrlOptionsController < ActionController::Base
end end
def url_options def url_options
super.merge(:host => 'www.override.com', :action => 'new', :locale => 'en') super.merge(:host => 'www.override.com')
end end
end end
@ -183,9 +183,9 @@ class UrlOptionsTest < ActionController::TestCase
get :from_view, :route => "from_view_url" get :from_view, :route => "from_view_url"
assert_equal 'http://www.override.com/from_view?locale=en', @response.body assert_equal 'http://www.override.com/from_view', @response.body
assert_equal 'http://www.override.com/from_view?locale=en', @controller.send(:from_view_url) assert_equal 'http://www.override.com/from_view', @controller.send(:from_view_url)
assert_equal 'http://www.override.com/default_url_options/new?locale=en', @controller.url_for(:controller => 'default_url_options') assert_equal 'http://www.override.com/default_url_options/index', @controller.url_for(:controller => 'default_url_options')
end end
end end

View File

@ -59,11 +59,11 @@ end
class MockController class MockController
def self.build(helpers) def self.build(helpers)
Class.new do Class.new do
def url_for(options) def url_options
options = super
options[:protocol] ||= "http" options[:protocol] ||= "http"
options[:host] ||= "test.host" options[:host] ||= "test.host"
options
super(options)
end end
include helpers include helpers

View File

@ -17,7 +17,7 @@ class SprocketsHelperWithRoutesTest < ActionView::TestCase
def setup def setup
super super
@controller = BasicController.new @controller = BasicController.new
@assets = Sprockets::Environment.new @assets = Sprockets::Environment.new
@assets.append_path(FIXTURES.join("sprockets/app/javascripts")) @assets.append_path(FIXTURES.join("sprockets/app/javascripts"))
@ -34,7 +34,7 @@ class SprocketsHelperWithRoutesTest < ActionView::TestCase
test "namespace conflicts on a named route called asset_path" do test "namespace conflicts on a named route called asset_path" do
# Testing this for sanity - asset_path is now a named route! # Testing this for sanity - asset_path is now a named route!
assert_match asset_path('test_asset'), '/assets/test_asset' assert_equal asset_path('test_asset'), '/assets/test_asset'
assert_match %r{/assets/logo-[0-9a-f]+.png}, assert_match %r{/assets/logo-[0-9a-f]+.png},
path_to_asset("logo.png") path_to_asset("logo.png")