| | 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_recognition_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_recognition_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 | cond = (i == 1 ? 'if' : 'elsif') |
|---|
| | 124 | tag, sub = item |
|---|
| | 125 | if tag == :dynamic |
|---|
| | 126 | body += padding + "#{cond} true\n" |
|---|
| | 127 | body += generate_recognition_code(sub, padding + " ", level + 1) |
|---|
| | 128 | break |
|---|
| | 129 | elsif tag == nil && !was_nil |
|---|
| | 130 | was_nil = true |
|---|
| | 131 | body += padding + "#{cond} seg.nil?\n" |
|---|
| | 132 | body += generate_recognition_code(sub, padding + " ", level + 1) |
|---|
| | 133 | else |
|---|
| | 134 | body += padding + "#{cond} seg == '#{tag}'\n" |
|---|
| | 135 | body += generate_recognition_code(sub, padding + " ", level + 1) |
|---|
| | 136 | end |
|---|
| | 137 | end |
|---|
| | 138 | end |
|---|
| | 139 | body += padding + "else\n" |
|---|
| | 140 | body += padding + " #{list[0]}\n" |
|---|
| | 141 | body += padding + "end)\n" |
|---|
| | 142 | body |
|---|
| | 143 | end |
|---|
| | 144 | |
|---|
| | 145 | # this must be really fast |
|---|
| | 146 | def to_plain_segments(str) |
|---|
| | 147 | str = str.dup |
|---|
| | 148 | str.sub!(/^\/+/,'') |
|---|
| | 149 | str.sub!(/\/+$/,'') |
|---|
| | 150 | segments = str.split(/\.[^\/]+\/+|\/+|\.[^\/]+\Z/) # cut off ".format" also |
|---|
| | 151 | segments << nil |
|---|
| | 152 | segments |
|---|
| | 153 | end |
|---|
| | 154 | |
|---|
| | 155 | end |
|---|
| | 156 | end |
|---|
| | 157 | end |