Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/actionpack/lib/action_controller/routing/recognition_optimisation.rb

Revision 8674, 5.3 kB (checked in by bitsweat, 2 years ago)

Performance: optimize route recognition. Large speedup for apps with many resource routes. Closes #10835.

Line 
1 module ActionController
2   module Routing
3     # BEFORE:   0.191446860631307 ms/url
4     # AFTER:    0.029847304022858 ms/url
5     # Speed up: 6.4 times
6     #
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.
10     #
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.
17     #
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.
20     #
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")
26     #
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.
32     #
33     # Note: we maintain the original routes order, so we finish building
34     #       steps on a first dynamic segment.
35     #
36     #
37     # Example. Given the routes:
38     #   0 /posts/
39     #   1 /posts/:id
40     #   2 /posts/:id/comments
41     #   3 /posts/blah
42     #   4 /users/
43     #   5 /users/:id
44     #   6 /users/:id/profile
45     #
46     # request_uri = /users/123
47     #
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!
54
55     class RouteSet
56       def recognize_path(path, environment={})
57         result = recognize_optimized(path, environment) and return result
58
59         # Route was not recognized. Try to find out why (maybe wrong verb).
60         allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } }
61
62         if environment[:method] && !HTTP_METHODS.include?(environment[:method])
63           raise NotImplemented.new(*allows)
64         elsif !allows.empty?
65           raise MethodNotAllowed.new(*allows)
66         else
67           raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}"
68         end
69       end
70
71       def recognize_optimized(path, env)
72         write_recognize_optimized
73         recognize_optimized(path, env)
74       end
75
76       def write_recognize_optimized
77         tree = segment_tree(routes)
78         body = generate_code(tree)
79         instance_eval %{
80           def recognize_optimized(path, env)
81             segments = to_plain_segments(path)
82             index = #{body}
83             return nil unless index
84             while index < routes.size
85               result = routes[index].recognize(path, env) and return result
86               index += 1
87             end
88             nil
89           end
90         }, __FILE__, __LINE__
91       end
92
93       def segment_tree(routes)
94         tree = [0]
95
96         i = -1
97         routes.each do |route|
98           i += 1
99           # not fast, but runs only once
100           segments = to_plain_segments(route.segments.inject("") { |str,s| str << s.to_s })
101
102           node  = tree
103           segments.each do |seg|
104             seg = :dynamic if seg && seg[0] == ?:
105             node << [seg, [i]] if node.empty? || node[node.size - 1][0] != seg
106             node = node[node.size - 1][1]
107           end
108         end
109         tree
110       end
111
112       def generate_code(list, padding='  ', level = 0)
113         # a digit
114         return padding + "#{list[0]}\n" if list.size == 1 && !(Array === list[0])
115
116         body = padding + "(seg = segments[#{level}]; \n"
117
118         i = 0
119         was_nil = false
120         list.each do |item|
121           if Array === item
122             i += 1
123             start = (i == 1)
124             final = (i == list.size)
125             tag, sub = item
126             if tag == :dynamic
127               body += padding + "#{start ? 'if' : 'elsif'} true\n"
128               body += generate_code(sub, padding + "  ", level + 1)
129               break
130             elsif tag == nil && !was_nil
131               was_nil = true
132               body += padding + "#{start ? 'if' : 'elsif'} seg.nil?\n"
133               body += generate_code(sub, padding + "  ", level + 1)
134             else
135               body += padding + "#{start ? 'if' : 'elsif'} seg == '#{tag}'\n"
136               body += generate_code(sub, padding + "  ", level + 1)
137             end
138           end
139         end
140         body += padding + "else\n"
141         body += padding + "  #{list[0]}\n"
142         body += padding + "end)\n"
143         body
144       end
145
146       # this must be really fast
147       def to_plain_segments(str)
148         str = str.dup
149         str.sub!(/^\/+/,'')
150         str.sub!(/\/+$/,'')
151         segments = str.split(/\.[^\/]+\/+|\/+|\.[^\/]+\Z/) # cut off ".format" also
152         segments << nil
153         segments
154       end
155
156     end
157   end
158 end
Note: See TracBrowser for help on using the browser.