-
Notifications
You must be signed in to change notification settings - Fork 21.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Beyond Ludicrous Speed #21057
Beyond Ludicrous Speed #21057
Changes from all commits
f80aa59
1bf50ba
83ee043
9b82588
097ec6f
0cbec58
1a14074
2a4d430
2e95d2e
9b189a3
e76a843
bff61ba
045cdd3
3fb9e80
1993e2c
57ba9cb
0d7a714
1f831fe
005541b
4d2ccc1
61dae88
22f5924
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ def initialize(routes) | |
|
||
def generate(name, options, path_parameters, parameterize = nil) | ||
constraints = path_parameters.merge(options) | ||
missing_keys = [] | ||
missing_keys = nil # need for variable scope | ||
|
||
match_route(name, constraints) do |route| | ||
parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize) | ||
|
@@ -25,22 +25,22 @@ def generate(name, options, path_parameters, parameterize = nil) | |
next unless name || route.dispatcher? | ||
|
||
missing_keys = missing_keys(route, parameterized_parts) | ||
next unless missing_keys.empty? | ||
next if missing_keys && missing_keys.any? | ||
params = options.dup.delete_if do |key, _| | ||
parameterized_parts.key?(key) || route.defaults.key?(key) | ||
end | ||
|
||
defaults = route.defaults | ||
required_parts = route.required_parts | ||
parameterized_parts.delete_if do |key, value| | ||
value.to_s == defaults[key].to_s && !required_parts.include?(key) | ||
parameterized_parts.keep_if do |key, value| | ||
defaults[key].nil? || value.to_s != defaults[key].to_s || required_parts.include?(key) | ||
end | ||
|
||
return [route.format(parameterized_parts), params] | ||
end | ||
|
||
message = "No route matches #{Hash[constraints.sort_by{|k,v| k.to_s}].inspect}" | ||
message << " missing required keys: #{missing_keys.sort.inspect}" unless missing_keys.empty? | ||
message << " missing required keys: #{missing_keys.sort.inspect}" if missing_keys && missing_keys.any? | ||
|
||
raise ActionController::UrlGenerationError, message | ||
end | ||
|
@@ -54,12 +54,12 @@ def clear | |
def extract_parameterized_parts(route, options, recall, parameterize = nil) | ||
parameterized_parts = recall.merge(options) | ||
|
||
keys_to_keep = route.parts.reverse.drop_while { |part| | ||
keys_to_keep = route.parts.reverse_each.drop_while { |part| | ||
!options.key?(part) || (options[part] || recall[part]).nil? | ||
} | route.required_parts | ||
|
||
(parameterized_parts.keys - keys_to_keep).each do |bad_key| | ||
parameterized_parts.delete(bad_key) | ||
parameterized_parts.delete_if do |bad_key, _| | ||
!keys_to_keep.include?(bad_key) | ||
end | ||
|
||
if parameterize | ||
|
@@ -110,15 +110,36 @@ def non_recursive(cache, options) | |
routes | ||
end | ||
|
||
module RegexCaseComparator | ||
DEFAULT_INPUT = /[-_.a-zA-Z0-9]+\/[-_.a-zA-Z0-9]+/ | ||
DEFAULT_REGEX = /\A#{DEFAULT_INPUT}\Z/ | ||
|
||
def self.===(regex) | ||
DEFAULT_INPUT == regex | ||
end | ||
end | ||
|
||
# Returns an array populated with missing keys if any are present. | ||
def missing_keys(route, parts) | ||
missing_keys = [] | ||
missing_keys = nil | ||
tests = route.path.requirements | ||
route.required_parts.each { |key| | ||
if tests.key?(key) | ||
missing_keys << key unless /\A#{tests[key]}\Z/ === parts[key] | ||
case tests[key] | ||
when nil | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changes a presence check to a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think you can have a required key be nil. I ran this against the test suite and came up with nothing: puts tests.inspect if tests.values.include?(nil) |
||
unless parts[key] | ||
missing_keys ||= [] | ||
missing_keys << key | ||
end | ||
when RegexCaseComparator | ||
unless RegexCaseComparator::DEFAULT_REGEX === parts[key] | ||
missing_keys ||= [] | ||
missing_keys << key | ||
end | ||
else | ||
missing_keys << key unless parts[key] | ||
unless /\A#{tests[key]}\Z/ === parts[key] | ||
missing_keys ||= [] | ||
missing_keys << key | ||
end | ||
end | ||
} | ||
missing_keys | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -267,9 +267,13 @@ def handle_positional_args(controller_options, inner_options, args, result, path | |||||
path_params -= controller_options.keys | ||||||
path_params -= result.keys | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don’t know how often this code path is reached (inside an Just a thought: Doesn’t the refactoring from |
||||||
end | ||||||
path_params -= inner_options.keys | ||||||
path_params.take(args.size).each do |param| | ||||||
result[param] = args.shift | ||||||
inner_options.each do |key, _| | ||||||
path_params.delete(key) | ||||||
end | ||||||
|
||||||
args.each_with_index do |arg, index| | ||||||
param = path_params[index] | ||||||
result[param] = arg if param | ||||||
end | ||||||
end | ||||||
|
||||||
|
@@ -594,8 +598,8 @@ class Generator | |||||
|
||||||
def initialize(named_route, options, recall, set) | ||||||
@named_route = named_route | ||||||
@options = options.dup | ||||||
@recall = recall.dup | ||||||
@options = options | ||||||
@recall = recall | ||||||
@set = set | ||||||
|
||||||
normalize_recall! | ||||||
|
@@ -617,7 +621,7 @@ def current_controller | |||||
def use_recall_for(key) | ||||||
if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key]) | ||||||
if !named_route_exists? || segment_keys.include?(key) | ||||||
@options[key] = @recall.delete(key) | ||||||
@options[key] = @recall[key] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Red flag, kind of thing that may cause subtle/untested regressions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, it raised some red flags with me when I first did it. Here's the specific commit: It's a fairly large savings in memory. Here's why I think it's okay This method moves a key/value pair from recall to options def use_recall_for(key)
if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
if !named_route_exists? || segment_keys.include?(key)
@options[key] = @recall[key]
end
end
end I changed it so that recall is preserved i.e. the value is copied not "moved." This method gets called 3 times for the keys # This pulls :controller, :action, and :id out of the recall.
# The recall key is only used if there is no key in the options
# or if the key in the options is identical. If any of
# :controller, :action or :id is not found, don't pull any
# more keys from the recall.
def normalize_controller_action_id!
use_recall_for(:controller) or return
use_recall_for(:action) or return
use_recall_for(:id)
end Based on the comment every time @set.formatter.generate(named_route, options, recall, PARAMETERIZE) Here the first thing that is done is to merge the two hashes: def generate(name, options, path_parameters, parameterize = nil)
constraints = path_parameters.merge(options) So whether we delete the key in You might be thinking "well i bet It's used here: parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize) Yet once again, it's immediately merged: def extract_parameterized_parts(route, options, recall, parameterize = nil)
parameterized_parts = recall.merge(options) In both cases we will use the key in options if it exists. I'm pretty comfortable with this change, but I would like some more eyes. Maybe I missed a really subtle and untested use-case, for which we should certainly add some tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tenderlove stubbed some toes on the same thing—ring a bell? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via campfire AP says he's OK with it. If any weirdness comes up on master about URL generation for the next while i'll be happy to take a look. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #24438 what was overlooked is a few mutations to
this is now going to mutate the recall passed in by callers. |
||||||
end | ||||||
end | ||||||
end | ||||||
|
@@ -671,12 +675,18 @@ def use_relative_controller! | |||||
|
||||||
# Remove leading slashes from controllers | ||||||
def normalize_controller! | ||||||
@options[:controller] = controller.sub(%r{^/}, ''.freeze) if controller | ||||||
if controller | ||||||
if m = controller.match(/\A\/(?<controller_without_leading_slash>.*)/) | ||||||
@options[:controller] = m[:controller_without_leading_slash] | ||||||
else | ||||||
@options[:controller] = controller | ||||||
end | ||||||
end | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While this may save on String allocations, I suspect the regex matching will actually make it slower. While the readability may be questionable, using require 'benchmark/ips'
def sub(controller)
controller.sub(%r{^/}, ''.freeze) if controller
end
def match(controller)
if controller
if m = controller.match(/\A\/(?<controller_without_leading_slash>.*)/)
m[:controller_without_leading_slash]
else
controller
end
end
end
def start_with(controller)
if controller
if controller.start_with?('/'.freeze)
controller[1..-1]
else
controller
end
end
end
Benchmark.ips do |x|
x.report("sub") { sub("no_leading_slash") }
x.report("match") { match("no_leading_slash") }
x.report("start_with") { start_with("no_leading_slash") }
x.compare!
end
Benchmark.ips do |x|
x.report("sub") { sub("/a_leading_slash") }
x.report("match") { match("/a_leading_slash") }
x.report("start_with") { start_with("/a_leading_slash") }
x.compare!
end
|
||||||
end | ||||||
|
||||||
# Move 'index' action from options to recall | ||||||
def normalize_action! | ||||||
if @options[:action] == 'index' | ||||||
if @options[:action] == 'index'.freeze | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it's a stupid question. But why do you freeze strings? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This explains the freeze method for strings This article explains why you might want I to use frozen strings for performance with some benchmarks http://www.sitepoint.com/unraveling-string-key-performance-ruby-2-2/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't it better to have a constant defined in a class/module? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check out https://github.com/rails/rails/pull/21057/files#r36532115 for why we're not doing that |
||||||
@recall[:action] = @options.delete(:action) | ||||||
end | ||||||
end | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,9 +22,10 @@ module TagHelper | |
|
||
TAG_PREFIXES = ['aria', 'data', :aria, :data].to_set | ||
|
||
PRE_CONTENT_STRINGS = { | ||
:textarea => "\n" | ||
} | ||
PRE_CONTENT_STRINGS = Hash.new { "".freeze } | ||
PRE_CONTENT_STRINGS[:textarea] = "\n" | ||
PRE_CONTENT_STRINGS["textarea"] = "\n" | ||
|
||
|
||
# Returns an empty HTML tag of type +name+ which by default is XHTML | ||
# compliant. Set +open+ to true to create an open tag compatible | ||
|
@@ -143,24 +144,25 @@ def escape_once(html) | |
def content_tag_string(name, content, options, escape = true) | ||
tag_options = tag_options(options, escape) if options | ||
content = ERB::Util.unwrapped_html_escape(content) if escape | ||
"<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe | ||
"<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe | ||
end | ||
|
||
def tag_options(options, escape = true) | ||
return if options.blank? | ||
attrs = [] | ||
output = "" | ||
sep = " ".freeze | ||
options.each_pair do |key, value| | ||
if TAG_PREFIXES.include?(key) && value.is_a?(Hash) | ||
value.each_pair do |k, v| | ||
attrs << prefix_tag_option(key, k, v, escape) | ||
output << sep + prefix_tag_option(key, k, v, escape) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was of the understanding that output << sep
output << prefix_tag_option(key, k, v, escape) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like it's faster require 'benchmark/ips'
sep = " ".freeze
Benchmark.ips do |x|
x.report("string +") {
output = ""
sep = " ".freeze
output << sep + "foo"
}
x.report("string <<") {
output = ""
sep = " ".freeze
output << sep
output << "foo"
}
x.report("array") {
array = []
array << "foo"
array.join(" ")
}
end gives us
Looks like 10% faster. Care to give me a PR and @-mention me? |
||
end | ||
elsif BOOLEAN_ATTRIBUTES.include?(key) | ||
attrs << boolean_tag_option(key) if value | ||
output << sep + boolean_tag_option(key) if value | ||
elsif !value.nil? | ||
attrs << tag_option(key, value, escape) | ||
output << sep + tag_option(key, value, escape) | ||
end | ||
end | ||
" #{attrs * ' '}" unless attrs.empty? | ||
output unless output.empty? | ||
end | ||
|
||
def prefix_tag_option(prefix, key, value, escape) | ||
|
@@ -177,7 +179,7 @@ def boolean_tag_option(key) | |
|
||
def tag_option(key, value, escape) | ||
if value.is_a?(Array) | ||
value = escape ? safe_join(value, " ") : value.join(" ") | ||
value = escape ? safe_join(value, " ".freeze) : value.join(" ".freeze) | ||
else | ||
value = escape ? ERB::Util.unwrapped_html_escape(value) : value | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't
any?
doing quite a lot more than!empty?
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're correct, I never knew. it looks like the big difference is this line: https://github.com/ruby/ruby/blob/trunk/array.c#L5524
I'm actually looking to see if we can optimize the case of
any?
a little for when the array is empty. It will still be slightly slower though. I think it would make sense for this to be:Want to give me a PR and @ mention me?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure.