Class: ActionController::Routing::RouteSet
- Defined in:
- lib/action_controller/routing/route_set.rb,
lib/action_controller/routing/recognition_optimisation.rb
Overview
BEFORE: 0.191446860631307 ms/url AFTER: 0.029847304022858 ms/url Speed up: 6.4 times
Route recognition is slow due to one-by-one iterating over a whole routeset (each map.resources generates at least 14 routes) and matching weird regexps on each step.
We optimize this by skipping all URI segments that 100% sure can’t be matched, moving deeper in a tree of routes (where node == segment) until first possible match is accured. In such case, we start walking a flat list of routes, matching them with accurate matcher. So, first step: search a segment tree for the first relevant index. Second step: iterate routes starting with that index.
How tree is walked? We can do a recursive tests, but it’s smarter: We just create a tree of if-s and elsif-s matching segments.
We have segments of 3 flavors: 1) nil (no segment, route finished) 2) const-dot-dynamic (like “/posts.:xml”, “/preview.:size.jpg”) 3) const (like “/posts”, “/comments”) 4) dynamic (“/:id”, “file.:size.:extension”)
We split incoming string into segments and iterate over them. When segment is nil, we drop immediately, on a current node index. When segment is equal to some const, we step into branch. If none constants matched, we step into ‘dynamic’ branch (it’s a last). If we can’t match anything, we drop to last index on a level.
Note: we maintain the original routes order, so we finish building
steps on a first dynamic segment.
Example. Given the routes:
0 /posts/
1 /posts/:id
2 /posts/:id/comments
3 /posts/blah
4 /users/
5 /users/:id
6 /users/:id/profile
request_uri = /users/123
There will be only 4 iterations:
1) segm test for /posts prefix, skip all /posts/* routes
2) segm test for /users/
3) segm test for /users/:id
(jump to list index = 5)
4) full test for /users/:id => here we are!
Defined Under Namespace
Classes: Mapper, NamedRouteCollection
Instance Attribute Summary collapse
-
#configuration_file ⇒ Object
Returns the value of attribute configuration_file.
-
#named_routes ⇒ Object
Returns the value of attribute named_routes.
-
#routes ⇒ Object
Returns the value of attribute routes.
Instance Method Summary collapse
- #add_named_route(name, path, options = {}) ⇒ Object
- #add_route(path, options = {}) ⇒ Object
- #build_expiry(options, recall) ⇒ Object
-
#builder ⇒ Object
Subclasses and plugins may override this method to specify a different RouteBuilder instance, so that other route DSL’s can be created.
- #clear! ⇒ Object
- #draw {|Mapper.new(self)| ... } ⇒ Object
- #empty? ⇒ Boolean
-
#extra_keys(options, recall = {}) ⇒ Object
Generate the path indicated by the arguments, and return an array of the keys that were not used to generate it.
-
#extract_request_environment(request) ⇒ Object
Subclasses and plugins may override this method to extract further attributes from the request, for use by route conditions and such.
- #generate(options, recall = {}, method = :generate) ⇒ Object
- #generate_code(list, padding = ' ', level = 0) ⇒ Object
- #generate_extras(options, recall = {}) ⇒ Object
-
#initialize ⇒ RouteSet
constructor
A new instance of RouteSet.
- #install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) ⇒ Object
- #load! ⇒ Object (also: #reload!)
- #load_routes! ⇒ Object
- #options_as_params(options) ⇒ Object
-
#raise_named_route_error(options, named_route, named_route_name) ⇒ Object
try to give a helpful error message when named route generation fails.
- #recognize(request) ⇒ Object
- #recognize_optimized(path, env) ⇒ Object
- #recognize_path(path, environment = {}) ⇒ Object
- #reload ⇒ Object
- #routes_by_controller ⇒ Object
- #routes_for(options, merged, expire_on) ⇒ Object
- #routes_for_controller_and_action(controller, action) ⇒ Object
- #routes_for_controller_and_action_and_keys(controller, action, keys) ⇒ Object
- #segment_tree(routes) ⇒ Object
-
#to_plain_segments(str) ⇒ Object
this must be really fast.
- #write_recognize_optimized ⇒ Object
Constructor Details
#initialize ⇒ RouteSet
Returns a new instance of RouteSet.
194 195 196 197 |
# File 'lib/action_controller/routing/route_set.rb', line 194 def initialize self.routes = [] self.named_routes = NamedRouteCollection.new end |
Instance Attribute Details
#configuration_file ⇒ Object
Returns the value of attribute configuration_file.
192 193 194 |
# File 'lib/action_controller/routing/route_set.rb', line 192 def configuration_file @configuration_file end |
#named_routes ⇒ Object
Returns the value of attribute named_routes.
192 193 194 |
# File 'lib/action_controller/routing/route_set.rb', line 192 def named_routes @named_routes end |
#routes ⇒ Object
Returns the value of attribute routes.
192 193 194 |
# File 'lib/action_controller/routing/route_set.rb', line 192 def routes @routes end |
Instance Method Details
#add_named_route(name, path, options = {}) ⇒ Object
266 267 268 269 270 |
# File 'lib/action_controller/routing/route_set.rb', line 266 def add_named_route(name, path, = {}) # TODO - is options EVER used? name = [:name_prefix] + name.to_s if [:name_prefix] named_routes[name.to_sym] = add_route(path, ) end |
#add_route(path, options = {}) ⇒ Object
260 261 262 263 264 |
# File 'lib/action_controller/routing/route_set.rb', line 260 def add_route(path, = {}) route = builder.build(path, ) routes << route route end |
#build_expiry(options, recall) ⇒ Object
290 291 292 293 294 295 |
# File 'lib/action_controller/routing/route_set.rb', line 290 def build_expiry(, recall) recall.inject({}) do |expiry, (key, recalled_value)| expiry[key] = (.key?(key) && [key].to_param != recalled_value.to_param) expiry end end |
#builder ⇒ Object
Subclasses and plugins may override this method to specify a different RouteBuilder instance, so that other route DSL’s can be created.
201 202 203 |
# File 'lib/action_controller/routing/route_set.rb', line 201 def builder @builder ||= RouteBuilder.new end |
#clear! ⇒ Object
211 212 213 214 215 216 217 218 219 |
# File 'lib/action_controller/routing/route_set.rb', line 211 def clear! routes.clear named_routes.clear @combined_regexp = nil @routes_by_controller = nil # This will force routing/recognition_optimization.rb # to refresh optimisations. @compiled_recognize_optimized = nil end |
#draw {|Mapper.new(self)| ... } ⇒ Object
205 206 207 208 209 |
# File 'lib/action_controller/routing/route_set.rb', line 205 def draw clear! yield Mapper.new(self) install_helpers end |
#empty? ⇒ Boolean
226 227 228 |
# File 'lib/action_controller/routing/route_set.rb', line 226 def empty? routes.empty? end |
#extra_keys(options, recall = {}) ⇒ Object
Generate the path indicated by the arguments, and return an array of the keys that were not used to generate it.
299 300 301 |
# File 'lib/action_controller/routing/route_set.rb', line 299 def extra_keys(, recall={}) generate_extras(, recall).last end |
#extract_request_environment(request) ⇒ Object
Subclasses and plugins may override this method to extract further attributes from the request, for use by route conditions and such.
430 431 432 |
# File 'lib/action_controller/routing/route_set.rb', line 430 def extract_request_environment(request) { :method => request.method } end |
#generate(options, recall = {}, method = :generate) ⇒ Object
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 |
# File 'lib/action_controller/routing/route_set.rb', line 307 def generate(, recall = {}, method=:generate) named_route_name = .delete(:use_route) generate_all = .delete(:generate_all) if named_route_name named_route = named_routes[named_route_name] = named_route.parameter_shell.merge() end = () expire_on = build_expiry(, recall) if [:controller] [:controller] = [:controller].to_s end # if the controller has changed, make sure it changes relative to the # current controller module, if any. In other words, if we're currently # on admin/get, and the new controller is 'set', the new controller # should really be admin/set. if !named_route && expire_on[:controller] && [:controller] && [:controller][0] != ?/ old_parts = recall[:controller].split('/') new_parts = [:controller].split('/') parts = old_parts[0..-(new_parts.length + 1)] + new_parts [:controller] = parts.join('/') end # drop the leading '/' on the controller name [:controller] = [:controller][1..-1] if [:controller] && [:controller][0] == ?/ merged = recall.merge() if named_route path = named_route.generate(, merged, expire_on) if path.nil? raise_named_route_error(, named_route, named_route_name) else return path end else merged[:action] ||= 'index' [:action] ||= 'index' controller = merged[:controller] action = merged[:action] raise RoutingError, "Need controller and action!" unless controller && action if generate_all # Used by caching to expire all paths for a resource return routes.collect do |route| route.send!(method, , merged, expire_on) end.compact end # don't use the recalled keys when determining which routes to check routes = routes_by_controller[controller][action][.keys.sort_by { |x| x.object_id }] routes.each do |route| results = route.send!(method, , merged, expire_on) return results if results && (!results.is_a?(Array) || results.first) end end raise RoutingError, "No route matches #{.inspect}" end |
#generate_code(list, padding = ' ', level = 0) ⇒ Object
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/action_controller/routing/recognition_optimisation.rb', line 112 def generate_code(list, padding=' ', level = 0) # a digit return padding + "#{list[0]}\n" if list.size == 1 && !(Array === list[0]) body = padding + "(seg = segments[#{level}]; \n" i = 0 was_nil = false list.each do |item| if Array === item i += 1 start = (i == 1) final = (i == list.size) tag, sub = item if tag == :dynamic body += padding + "#{start ? 'if' : 'elsif'} true\n" body += generate_code(sub, padding + " ", level + 1) break elsif tag == nil && !was_nil was_nil = true body += padding + "#{start ? 'if' : 'elsif'} seg.nil?\n" body += generate_code(sub, padding + " ", level + 1) else body += padding + "#{start ? 'if' : 'elsif'} seg == '#{tag}'\n" body += generate_code(sub, padding + " ", level + 1) end end end body += padding + "else\n" body += padding + " #{list[0]}\n" body += padding + "end)\n" body end |
#generate_extras(options, recall = {}) ⇒ Object
303 304 305 |
# File 'lib/action_controller/routing/route_set.rb', line 303 def generate_extras(, recall={}) generate(, recall, :generate_extras) end |
#install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) ⇒ Object
221 222 223 224 |
# File 'lib/action_controller/routing/route_set.rb', line 221 def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) Array(destinations).each { |d| d.module_eval { include Helpers } } named_routes.install(destinations, regenerate_code) end |
#load! ⇒ Object Also known as: reload!
230 231 232 233 234 235 |
# File 'lib/action_controller/routing/route_set.rb', line 230 def load! Routing.use_controllers! nil # Clear the controller cache so we may discover new ones clear! load_routes! install_helpers end |
#load_routes! ⇒ Object
251 252 253 254 255 256 257 258 |
# File 'lib/action_controller/routing/route_set.rb', line 251 def load_routes! if configuration_file load configuration_file @routes_last_modified = File.stat(configuration_file).mtime else add_route ":controller/:action/:id" end end |
#options_as_params(options) ⇒ Object
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/action_controller/routing/route_set.rb', line 272 def () # If an explicit :controller was given, always make :action explicit # too, so that action expiry works as expected for things like # # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) # # (the above is from the unit tests). In the above case, because the # controller was explicitly given, but no action, the action is implied to # be "index", not the recalled action of "show". # # great fun, eh? = .clone [:action] ||= 'index' if [:controller] [:action] = [:action].to_s if [:action] end |
#raise_named_route_error(options, named_route, named_route_name) ⇒ Object
try to give a helpful error message when named route generation fails
372 373 374 375 376 377 378 379 380 381 |
# File 'lib/action_controller/routing/route_set.rb', line 372 def raise_named_route_error(, named_route, named_route_name) diff = named_route.requirements.diff() unless diff.empty? raise RoutingError, "#{named_route_name}_url failed to generate from #{.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff().inspect}" else required_segments = named_route.segments.select {|seg| (!seg.optional?) && (!seg.is_a?(DividerSegment)) } required_keys_or_values = required_segments.map { |seg| seg.key rescue seg.value } # we want either the key or the value from the segment raise RoutingError, "#{named_route_name}_url failed to generate from #{.inspect} - you may have ambiguous routes, or you may need to supply additional parameters for this route. content_url has the following required parameters: #{required_keys_or_values.inspect} - are they all satisfied?" end end |
#recognize(request) ⇒ Object
383 384 385 386 387 |
# File 'lib/action_controller/routing/route_set.rb', line 383 def recognize(request) params = recognize_path(request.path, extract_request_environment(request)) request.path_parameters = params.with_indifferent_access "#{params[:controller].camelize}Controller".constantize end |
#recognize_optimized(path, env) ⇒ Object
71 72 73 74 |
# File 'lib/action_controller/routing/recognition_optimisation.rb', line 71 def recognize_optimized(path, env) write_recognize_optimized recognize_optimized(path, env) end |
#recognize_path(path, environment = {}) ⇒ Object
389 390 391 |
# File 'lib/action_controller/routing/route_set.rb', line 389 def recognize_path(path, environment={}) raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path." end |
#reload ⇒ Object
240 241 242 243 244 245 246 247 248 249 |
# File 'lib/action_controller/routing/route_set.rb', line 240 def reload if @routes_last_modified && configuration_file mtime = File.stat(configuration_file).mtime # if it hasn't been changed, then just return return if mtime == @routes_last_modified # if it has changed then record the new time and fall to the load! below @routes_last_modified = mtime end load! end |
#routes_by_controller ⇒ Object
393 394 395 396 397 398 399 400 401 |
# File 'lib/action_controller/routing/route_set.rb', line 393 def routes_by_controller @routes_by_controller ||= Hash.new do |controller_hash, controller| controller_hash[controller] = Hash.new do |action_hash, action| action_hash[action] = Hash.new do |key_hash, keys| key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys) end end end end |
#routes_for(options, merged, expire_on) ⇒ Object
403 404 405 406 407 408 409 410 |
# File 'lib/action_controller/routing/route_set.rb', line 403 def routes_for(, merged, expire_on) raise "Need controller and action!" unless controller && action controller = merged[:controller] merged = if expire_on[:controller] action = merged[:action] || 'index' routes_by_controller[controller][action][merged.keys] end |
#routes_for_controller_and_action(controller, action) ⇒ Object
412 413 414 415 416 417 |
# File 'lib/action_controller/routing/route_set.rb', line 412 def routes_for_controller_and_action(controller, action) selected = routes.select do |route| route.matches_controller_and_action? controller, action end (selected.length == routes.length) ? routes : selected end |
#routes_for_controller_and_action_and_keys(controller, action, keys) ⇒ Object
419 420 421 422 423 424 425 426 |
# File 'lib/action_controller/routing/route_set.rb', line 419 def routes_for_controller_and_action_and_keys(controller, action, keys) selected = routes.select do |route| route.matches_controller_and_action? controller, action end selected.sort_by do |route| (keys - route.significant_keys).length end end |
#segment_tree(routes) ⇒ Object
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/action_controller/routing/recognition_optimisation.rb', line 93 def segment_tree(routes) tree = [0] i = -1 routes.each do |route| i += 1 # not fast, but runs only once segments = to_plain_segments(route.segments.inject("") { |str,s| str << s.to_s }) node = tree segments.each do |seg| seg = :dynamic if seg && seg[0] == ?: node << [seg, [i]] if node.empty? || node[node.size - 1][0] != seg node = node[node.size - 1][1] end end tree end |
#to_plain_segments(str) ⇒ Object
this must be really fast
147 148 149 150 151 152 153 154 |
# File 'lib/action_controller/routing/recognition_optimisation.rb', line 147 def to_plain_segments(str) str = str.dup str.sub!(/^\/+/,'') str.sub!(/\/+$/,'') segments = str.split(/\.[^\/]+\/+|\/+|\.[^\/]+\Z/) # cut off ".format" also segments << nil segments end |
#write_recognize_optimized ⇒ Object
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/action_controller/routing/recognition_optimisation.rb', line 76 def write_recognize_optimized tree = segment_tree(routes) body = generate_code(tree) instance_eval %{ def recognize_optimized(path, env) segments = to_plain_segments(path) index = #{body} return nil unless index while index < routes.size result = routes[index].recognize(path, env) and return result index += 1 end nil end }, __FILE__, __LINE__ end |