1 module ActionController
3 # BEFORE: 0.191446860631307 ms/url
4 # AFTER: 0.029847304022858 ms/url
7 # Route recognition is slow due to one-by-one iterating over
8 # a whole routeset (each map.resources generates at least 14 routes)
9 # and matching weird regexps on each step.
11 # We optimize this by skipping all URI segments that 100% sure can't
12 # be matched, moving deeper in a tree of routes (where node == segment)
13 # until first possible match is accured. In such case, we start walking
14 # a flat list of routes, matching them with accurate matcher.
15 # So, first step: search a segment tree for the first relevant index.
16 # Second step: iterate routes starting with that index.
18 # How tree is walked? We can do a recursive tests, but it's smarter:
19 # We just create a tree of if-s and elsif-s matching segments.
21 # We have segments of 3 flavors:
22 # 1) nil (no segment, route finished)
23 # 2) const-dot-dynamic (like "/posts.:xml", "/preview.:size.jpg")
24 # 3) const (like "/posts", "/comments")
25 # 4) dynamic ("/:id", "file.:size.:extension")
27 # We split incoming string into segments and iterate over them.
28 # When segment is nil, we drop immediately, on a current node index.
29 # When segment is equal to some const, we step into branch.
30 # If none constants matched, we step into 'dynamic' branch (it's a last).
31 # If we can't match anything, we drop to last index on a level.
33 # Note: we maintain the original routes order, so we finish building
34 # steps on a first dynamic segment.
37 # Example. Given the routes:
40 # 2 /posts/:id/comments
44 # 6 /users/:id/profile
46 # request_uri = /users/123
48 # There will be only 4 iterations:
49 # 1) segm test for /posts prefix, skip all /posts/* routes
50 # 2) segm test for /users/
51 # 3) segm test for /users/:id
52 # (jump to list index = 5)
53 # 4) full test for /users/:id => here we are!
55 def recognize_path(path
, environment
={})
56 result
= recognize_optimized(path
, environment
) and return result
58 # Route was not recognized. Try to find out why (maybe wrong verb).
59 allows
= HTTP_METHODS
.select
{ |verb
| routes
.find
{ |r
| r
.recognize(path
, :method => verb
) } }
61 if environment
[:method] && !HTTP_METHODS
.include?(environment
[:method])
62 raise NotImplemented
.new(*allows
)
64 raise MethodNotAllowed
.new(*allows
)
66 raise RoutingError
, "No route matches #{path.inspect} with #{environment.inspect}"
70 def segment_tree(routes
)
74 routes
.each
do |route
|
76 # not fast, but runs only once
77 segments
= to_plain_segments(route
.segments
.inject("") { |str
,s
| str
<< s
.to_s
})
80 segments
.each
do |seg
|
81 seg
= :dynamic if seg
&& seg
[0] == ?:
82 node
<< [seg
, [i
]] if node
.empty
? || node
[node
.size
- 1][0] != seg
83 node
= node
[node
.size
- 1][1]
89 def generate_code(list
, padding
=' ', level
= 0)
91 return padding
+ "#{list[0]}\n" if list
.size
== 1 && !(Array
=== list
[0])
93 body
= padding
+ "(seg = segments[#{level}]; \n"
101 final
= (i
== list
.size
)
104 body
+= padding
+ "#{start ? 'if' : 'elsif'} true\n"
105 body
+= generate_code(sub
, padding
+ " ", level
+ 1)
107 elsif tag
== nil && !was_nil
109 body
+= padding
+ "#{start ? 'if' : 'elsif'} seg.nil?\n"
110 body
+= generate_code(sub
, padding
+ " ", level
+ 1)
112 body
+= padding
+ "#{start ? 'if' : 'elsif'} seg == '#{tag}'\n"
113 body
+= generate_code(sub
, padding
+ " ", level
+ 1)
117 body
+= padding
+ "else\n"
118 body
+= padding
+ " #{list[0]}\n"
119 body
+= padding
+ "end)\n"
123 # this must be really fast
124 def to_plain_segments(str
)
128 segments
= str
.split(/\.[^\/]+\
/+|\/+|\
.[^\
/]+\Z/) # cut off ".format" also
134 def write_recognize_optimized
!
135 tree
= segment_tree(routes
)
136 body
= generate_code(tree
)
138 remove_recognize_optimized
!
141 def recognize_optimized(path, env)
142 segments = to_plain_segments(path)
144 return nil unless index
145 while index < routes.size
146 result = routes[index].recognize(path, env) and return result
151 }, '(recognize_optimized)', 1
154 def clear_recognize_optimized!
155 remove_recognize_optimized!
156 write_recognize_optimized!
159 def remove_recognize_optimized!
160 if respond_to?(:recognize_optimized)
162 remove_method :recognize_optimized