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
- #clear_recognize_optimized! ⇒ 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_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 198 |
# File 'lib/action_controller/routing/route_set.rb', line 194 def initialize self.routes = [] self.named_routes = NamedRouteCollection.new clear_recognize_optimized! 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
267 268 269 270 271 |
# File 'lib/action_controller/routing/route_set.rb', line 267 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
261 262 263 264 265 |
# File 'lib/action_controller/routing/route_set.rb', line 261 def add_route(path, = {}) route = builder.build(path, ) routes << route route end |
#build_expiry(options, recall) ⇒ Object
291 292 293 294 295 296 |
# File 'lib/action_controller/routing/route_set.rb', line 291 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.
202 203 204 |
# File 'lib/action_controller/routing/route_set.rb', line 202 def builder @builder ||= RouteBuilder.new end |
#clear! ⇒ Object
212 213 214 215 216 217 218 219 220 |
# File 'lib/action_controller/routing/route_set.rb', line 212 def clear! routes.clear named_routes.clear @combined_regexp = nil @routes_by_controller = nil # This will force routing/recognition_optimisation.rb # to refresh optimisations. clear_recognize_optimized! end |
#clear_recognize_optimized! ⇒ Object
71 72 73 74 75 76 77 78 |
# File 'lib/action_controller/routing/recognition_optimisation.rb', line 71 def clear_recognize_optimized! instance_eval %{ def recognize_optimized(path, env) write_recognize_optimized! recognize_optimized(path, env) end }, __FILE__, __LINE__ end |
#draw {|Mapper.new(self)| ... } ⇒ Object
206 207 208 209 210 |
# File 'lib/action_controller/routing/route_set.rb', line 206 def draw clear! yield Mapper.new(self) install_helpers end |
#empty? ⇒ Boolean
227 228 229 |
# File 'lib/action_controller/routing/route_set.rb', line 227 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.
300 301 302 |
# File 'lib/action_controller/routing/route_set.rb', line 300 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.
431 432 433 |
# File 'lib/action_controller/routing/route_set.rb', line 431 def extract_request_environment(request) { :method => request.method } end |
#generate(options, recall = {}, method = :generate) ⇒ Object
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 370 |
# File 'lib/action_controller/routing/route_set.rb', line 308 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
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 145 146 147 148 |
# File 'lib/action_controller/routing/recognition_optimisation.rb', line 116 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
304 305 306 |
# File 'lib/action_controller/routing/route_set.rb', line 304 def generate_extras(, recall={}) generate(, recall, :generate_extras) end |
#install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) ⇒ Object
222 223 224 225 |
# File 'lib/action_controller/routing/route_set.rb', line 222 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!
231 232 233 234 235 236 |
# File 'lib/action_controller/routing/route_set.rb', line 231 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
252 253 254 255 256 257 258 259 |
# File 'lib/action_controller/routing/route_set.rb', line 252 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
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/action_controller/routing/route_set.rb', line 273 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
373 374 375 376 377 378 379 380 381 382 |
# File 'lib/action_controller/routing/route_set.rb', line 373 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
384 385 386 387 388 |
# File 'lib/action_controller/routing/route_set.rb', line 384 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_path(path, environment = {}) ⇒ Object
390 391 392 |
# File 'lib/action_controller/routing/route_set.rb', line 390 def recognize_path(path, environment={}) raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path." end |
#reload ⇒ Object
241 242 243 244 245 246 247 248 249 250 |
# File 'lib/action_controller/routing/route_set.rb', line 241 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
394 395 396 397 398 399 400 401 402 |
# File 'lib/action_controller/routing/route_set.rb', line 394 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
404 405 406 407 408 409 410 411 |
# File 'lib/action_controller/routing/route_set.rb', line 404 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
413 414 415 416 417 418 |
# File 'lib/action_controller/routing/route_set.rb', line 413 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
420 421 422 423 424 425 426 427 |
# File 'lib/action_controller/routing/route_set.rb', line 420 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
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/action_controller/routing/recognition_optimisation.rb', line 97 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
151 152 153 154 155 156 157 158 |
# File 'lib/action_controller/routing/recognition_optimisation.rb', line 151 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
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/action_controller/routing/recognition_optimisation.rb', line 80 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 |