aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/journey
Commit message (Collapse)AuthorAgeFilesLines
* set route precedence at allocation timeAaron Patterson2015-08-201-6/+4
| | | | This way we can make the Route object a read-only data structure.
* drop array allocations when building pathsAaron Patterson2015-08-181-0/+2
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | ```ruby require 'action_pack' require 'action_dispatch' require 'benchmark/ips' route_set = ActionDispatch::Routing::RouteSet.new routes = ActionDispatch::Routing::Mapper.new route_set ObjectSpace::AllocationTracer.setup(%i{path line type}) result = ObjectSpace::AllocationTracer.trace do 500.times do routes.resources :foo end end sorted = ObjectSpace::AllocationTracer.allocated_count_table.sort_by(&:last) sorted.each do |k,v| next if v == 0 p k => v end __END__ Before: {:T_SYMBOL=>11} {:T_REGEXP=>17} {:T_STRUCT=>6500} {:T_MATCH=>12004} {:T_OBJECT=>99009} {:T_DATA=>100088} {:T_HASH=>122015} {:T_STRING=>159637} {:T_IMEMO=>363134} {:T_ARRAY=>433056} After: {:T_SYMBOL=>11} {:T_REGEXP=>17} {:T_STRUCT=>6500} {:T_MATCH=>12004} {:T_OBJECT=>91009} {:T_DATA=>100088} {:T_HASH=>114013} {:T_STRING=>159637} {:T_ARRAY=>321056} {:T_IMEMO=>351133} ```
* drop string allocations for each resourceAaron Patterson2015-08-181-0/+2
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Eagerly calculate and cache the name of Symbol objects in the path AST. This drops about 26 string allocations per resource: ```ruby require 'action_pack' require 'action_dispatch' require 'benchmark/ips' route_set = ActionDispatch::Routing::RouteSet.new routes = ActionDispatch::Routing::Mapper.new route_set ObjectSpace::AllocationTracer.setup(%i{path line type}) result = ObjectSpace::AllocationTracer.trace do 500.times do routes.resources :foo end end sorted = ObjectSpace::AllocationTracer.allocated_count_table.sort_by(&:last) sorted.each do |k,v| next if v == 0 p k => v end __END__ Before: {:T_SYMBOL=>11} {:T_REGEXP=>17} {:T_STRUCT=>6500} {:T_MATCH=>12004} {:T_OBJECT=>99009} {:T_DATA=>116084} {:T_HASH=>122015} {:T_STRING=>172647} {:T_IMEMO=>371132} {:T_ARRAY=>433056} After: {:T_SYMBOL=>11} {:T_REGEXP=>17} {:T_STRUCT=>6500} {:T_MATCH=>12004} {:T_OBJECT=>99009} {:T_DATA=>100088} {:T_HASH=>122015} {:T_STRING=>159637} {:T_IMEMO=>363134} {:T_ARRAY=>433056} ```
* Remove unreached default valueRafael Mendonça França2015-08-171-1/+1
| | | | verb_matcher never returns nil.
* use the strategy pattern to match request verbsAaron Patterson2015-08-171-6/+48
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Rather than building a regexp for every route, lets use the strategy pattern to select among objects that can match HTTP verbs. This commit introduces strategy objects for each verb that has a predicate method on the request object like `get?`, `post?`, etc. When we build the route object, look up the strategy for the verbs the user specified. If we can't find it, fall back on string matching. Using a strategy / null object pattern (the `All` VerbMatcher is our "null" object in this case) we can: 1) Remove conditionals 2) Drop boot time allocations 2) Drop run time allocations 3) Improve runtime performance Here is our boot time allocation benchmark: ```ruby require 'action_pack' require 'action_dispatch' route_set = ActionDispatch::Routing::RouteSet.new routes = ActionDispatch::Routing::Mapper.new route_set result = ObjectSpace::AllocationTracer.trace do 500.times do routes.resources :foo end end sorted = ObjectSpace::AllocationTracer.allocated_count_table.sort_by(&:last) sorted.each do |k,v| next if v == 0 p k => v end __END__ Before: $ be ruby -rallocation_tracer route_test.rb {:T_SYMBOL=>11} {:T_REGEXP=>4017} {:T_STRUCT=>6500} {:T_MATCH=>12004} {:T_DATA=>84092} {:T_OBJECT=>99009} {:T_HASH=>122015} {:T_STRING=>216652} {:T_IMEMO=>355137} {:T_ARRAY=>441057} After: $ be ruby -rallocation_tracer route_test.rb {:T_SYMBOL=>11} {:T_REGEXP=>17} {:T_STRUCT=>6500} {:T_MATCH=>12004} {:T_DATA=>84092} {:T_OBJECT=>99009} {:T_HASH=>122015} {:T_STRING=>172647} {:T_IMEMO=>355136} {:T_ARRAY=>433056} ``` This benchmark adds 500 resources. Each resource has 8 routes, so it adds 4000 routes. You can see from the results that this patch eliminates 4000 Regexp allocations, ~44000 String allocations, and ~8000 Array allocations. With that, we can figure out that the previous code would allocate 1 regexp, 11 strings, and 2 arrays per route *more* than this patch in order to handle verb matching. Next lets look at runtime allocations: ```ruby require 'action_pack' require 'action_dispatch' require 'benchmark/ips' route_set = ActionDispatch::Routing::RouteSet.new routes = ActionDispatch::Routing::Mapper.new route_set routes.resources :foo route = route_set.routes.first request = ActionDispatch::Request.new("REQUEST_METHOD" => "GET") result = ObjectSpace::AllocationTracer.trace do 500.times do route.matches? request end end sorted = ObjectSpace::AllocationTracer.allocated_count_table.sort_by(&:last) sorted.each do |k,v| next if v == 0 p k => v end __END__ Before: $ be ruby -rallocation_tracer route_test.rb {:T_MATCH=>500} {:T_STRING=>501} {:T_IMEMO=>1501} After: $ be ruby -rallocation_tracer route_test.rb {:T_IMEMO=>1001} ``` This benchmark runs 500 calls against the `matches?` method on the route object. We check this method in the case that there are two methods that match the same path, but they are differentiated by the verb (or other conditionals). For example `POST /users` vs `GET /users`, same path, different action. Previously, we were using regexps to match against the verb. You can see that doing the regexp match would allocate 1 match object and 1 string object each time it was called. This patch eliminates those allocations. Next lets look at runtime performance. ```ruby require 'action_pack' require 'action_dispatch' require 'benchmark/ips' route_set = ActionDispatch::Routing::RouteSet.new routes = ActionDispatch::Routing::Mapper.new route_set routes.resources :foo route = route_set.routes.first match = ActionDispatch::Request.new("REQUEST_METHOD" => "GET") no_match = ActionDispatch::Request.new("REQUEST_METHOD" => "POST") Benchmark.ips do |x| x.report("match") do route.matches? match end x.report("no match") do route.matches? no_match end end __END__ Before: $ be ruby -rallocation_tracer runtime.rb Calculating ------------------------------------- match 17.145k i/100ms no match 24.244k i/100ms ------------------------------------------------- match 259.708k (± 4.3%) i/s - 1.303M no match 453.376k (± 5.9%) i/s - 2.279M After: $ be ruby -rallocation_tracer runtime.rb Calculating ------------------------------------- match 23.958k i/100ms no match 29.402k i/100ms ------------------------------------------------- match 465.063k (± 3.8%) i/s - 2.324M no match 691.956k (± 4.5%) i/s - 3.469M ``` This tests tries to see how many times it can match a request per second. Switching to method calls and string comparison makes the successful match case about 79% faster, and the unsuccessful case about 52% faster. That was fun!
* split the verb regex from the constraints hashAaron Patterson2015-08-171-6/+17
| | | | | | verb matching is very common (all routes besides rack app endpoints require one). We will extract verb matching for now, and use a more efficient method of matching (then regexp) later
* introduce an alternate constructor for Route objectsAaron Patterson2015-08-171-0/+4
| | | | | I want to change the real constructor to take a particular parameter for matching the request method
* drop object allocation during routes setupAaron Patterson2015-08-172-44/+89
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | This commit introduces a functional Path AST visitor and implements `each` on the AST in terms of the functional visitor. The functional visitor doesn't maintain state, so we only need to allocate one of them. Given this benchmark route file: ```ruby require 'action_pack' require 'action_dispatch' route_set = ActionDispatch::Routing::RouteSet.new routes = ActionDispatch::Routing::Mapper.new route_set ObjectSpace::AllocationTracer.setup(%i{path line type}) result = ObjectSpace::AllocationTracer.trace do 500.times{|i| routes.resource :omglol } end result.find_all { |k,v| k.first =~ /git\/rails/ }.sort_by { |k,v| v.first }.each { |k,v| p k => v } ``` node.rb line 17 was in our top 3 allocation spot: ``` {["/Users/aaron/git/rails/actionpack/lib/action_dispatch/journey/nodes/node.rb", 17, :T_OBJECT]=>[31526, 0, 28329, 0, 2, 1123160]} {["/Users/aaron/git/rails/actionpack/lib/action_dispatch/routing/mapper.rb", 2080, :T_IMEMO]=>[34002, 0, 30563, 0, 2, 1211480]} {["/Users/aaron/git/rails/actionpack/lib/action_dispatch/routing/mapper.rb", 2071, :T_IMEMO]=>[121934, 1, 109608, 0, 7, 4344400]} ``` This commit eliminates allocations at that place.
* avoid is_a? checksAaron Patterson2015-08-172-2/+4
| | | | add another predicate method so we can avoid is_a checks
* pull RegexpOffsets in to a methodAaron Patterson2015-08-171-27/+14
| | | | we don't really need this visitor
* `required_defaults` is always passed in, remove conditionalAaron Patterson2015-08-171-1/+1
| | | | | Routes are always constructed with a list of required_defaults, so there's no need to check whether or not it's nil
* use predicate methods to avoid is_a? checksAaron Patterson2015-08-172-1/+3
| | | | | we may want to change the name of the class at some point, so it's better to use a predicate
* default pattern to use a joined stringAaron Patterson2015-08-171-2/+2
| | | | | The string we create is almost always the same, so rather than joining all the time, lets join once, then reuse that string everywhere.
* move route allocation to a factory method on the mapping objectAaron Patterson2015-08-151-8/+1
| | | | | | I would like to change the signature of the Route constructor. Since the mapping object has all the data required to construct a Route object, move the allocation to a factory method.
* only keep one hash of named routesAaron Patterson2015-08-142-5/+2
| | | | | The outer router object already keeps a hash of named routes, so we should just use that.
* rm add_route2Aaron Patterson2015-08-141-7/+1
| | | | | refactor the tests with a backwards compatible method call so we can rm add_route2 from the journey router
* pass pass the mapping object down the add_route stackAaron Patterson2015-08-141-2/+12
| | | | | then we can let the mapping object derive stuff that the Route object needs.
* pull up path parsingAaron Patterson2015-08-141-0/+4
| | | | | `add_route` needs the AST, so rather than shove it in a hash and delete later, lets move parsing up the stack so we can pass down later
* use predicate methods instead of hard coding verb stringsAaron Patterson2015-08-141-1/+1
| | | | | also change the feeler to subclass AD::Request so that it has all the methods that Request has
* remove hard coded regular expressionAaron Patterson2015-08-142-1/+5
|
* remove StrexpAaron Patterson2015-08-133-34/+11
| | | | | This was a useless object. We can just directly construct a Path::Pattern object without a Strexp object.
* pass anchor directly to `Pattern`Aaron Patterson2015-08-132-8/+7
| | | | | the caller already has it, there is no reason to pack it in to an object and just throw that object away.
* Array#any? is slower and not the inverse of Array#empty?Jean Boussier2015-07-301-2/+2
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | ``` empty_array = [] small_array = [1] * 30 bigger_array = [1] * 300 Benchmark.ips do |x| x.report('empty !empty?') { !empty_array.empty? } x.report('small !empty?') { !small_array.empty? } x.report('bigger !empty?') { !bigger_array.empty? } x.report('empty any?') { empty_array.any? } x.report('small any?') { small_array.any? } x.report('bigger any?') { bigger_array.any? } end ``` ``` Calculating ------------------------------------- empty !empty? 132.059k i/100ms small !empty? 133.974k i/100ms bigger !empty? 133.848k i/100ms empty any? 106.924k i/100ms small any? 85.525k i/100ms bigger any? 86.663k i/100ms ------------------------------------------------- empty !empty? 8.522M (± 7.9%) i/s - 42.391M small !empty? 8.501M (± 8.5%) i/s - 42.202M bigger !empty? 8.434M (± 8.6%) i/s - 41.894M empty any? 4.161M (± 8.3%) i/s - 20.743M small any? 2.654M (± 5.2%) i/s - 13.256M bigger any? 2.642M (± 6.4%) i/s - 13.173M ``` Ref: https://github.com/rails/rails/pull/21057#discussion_r35902468
* Use delete_if instead of each; delete(key)schneems2015-07-301-3/+2
| | | | | | | | | | | | | It is slightly faster: ``` Calculating ------------------------------------- each; delete 35.166k i/100ms delete_if 36.416k i/100ms ------------------------------------------------- each; delete 478.026k (± 8.5%) i/s - 2.391M delete_if 485.123k (± 7.9%) i/s - 2.440M ```
* Remove (another) array allocationschneems2015-07-301-5/+14
| | | | | | We don't always need an array when generating a url with the formatter. We can be lazy about allocating the `missing_keys` array. This saves us: 35,606 bytes and 889 objects per request
* Remove array allocationschneems2015-07-301-2/+2
| | | | | | THe only reason we were allocating an array is to get the "missing_keys" variable in scope of the error message generator. Guess what? Arrays kinda take up a lot of memory, so by replacing that with a nil, we save: 35,303 bytes and 886 objects per request
* Avoid calling to_s on nil in journey/formatterschneems2015-07-301-2/+2
| | | | | | | | When `defaults[key]` in `generate` in the journey formatter is called, it often returns a `nil` when we call `to_s` on a nil, it allocates an empty string. We can skip this check when the default value is nil. This change buys us 35,431 bytes of memory and 887 fewer objects per request. Thanks to @matthewd for help with the readability
* Speed up journey missing_keysschneems2015-07-291-3/+15
| | | | | | | | Most routes have a `route.path.requirements[key]` of `/[-_.a-zA-Z0-9]+\/[-_.a-zA-Z0-9]+/` yet every time this method is called a new regex is generated on the fly with `/\A#{DEFAULT_INPUT}\Z/`. OBJECT ALLOCATIONS BLERG! This change uses a special module that implements `===` so it can be used in a case statement to pull out the default input. When this happens, we use a pre-generated regex. This change buys us 1,643,465 bytes of memory and 7,990 fewer objects per request.
* Speed up journey extract_parameterized_partsschneems2015-07-291-2/+3
| | | | | | | | Micro optimization: `reverse.drop_while` is slower than `reverse_each.drop_while`. This doesn't save any object allocations. Second, `keys_to_keep` is typically a very small array. The operation `parameterized_parts.keys - keys_to_keep` actually allocates two arrays. It is quicker (I benchmarked) to iterate over each and check inclusion in array manually. This change buys us 1774 fewer objects per request
* Freeze string literals when not mutated.schneems2015-07-192-4/+4
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | I wrote a utility that helps find areas where you could optimize your program using a frozen string instead of a string literal, it's called [let_it_go](https://github.com/schneems/let_it_go). After going through the output and adding `.freeze` I was able to eliminate the creation of 1,114 string objects on EVERY request to [codetriage](codetriage.com). How does this impact execution? To look at memory: ```ruby require 'get_process_mem' mem = GetProcessMem.new GC.start GC.disable 1_114.times { " " } before = mem.mb after = mem.mb GC.enable puts "Diff: #{after - before} mb" ``` Creating 1,114 string objects results in `Diff: 0.03125 mb` of RAM allocated on every request. Or 1mb every 32 requests. To look at raw speed: ```ruby require 'benchmark/ips' number_of_objects_reduced = 1_114 Benchmark.ips do |x| x.report("freeze") { number_of_objects_reduced.times { " ".freeze } } x.report("no-freeze") { number_of_objects_reduced.times { " " } } end ``` We get the results ``` Calculating ------------------------------------- freeze 1.428k i/100ms no-freeze 609.000 i/100ms ------------------------------------------------- freeze 14.363k (± 8.5%) i/s - 71.400k no-freeze 6.084k (± 8.1%) i/s - 30.450k ``` Now we can do some maths: ```ruby ips = 6_226k # iterations / 1 second call_time_before = 1.0 / ips # seconds per iteration ips = 15_254 # iterations / 1 second call_time_after = 1.0 / ips # seconds per iteration diff = call_time_before - call_time_after number_of_objects_reduced * diff * 100 # => 0.4530373333993266 miliseconds saved per request ``` So we're shaving off 1 second of execution time for every 220 requests. Is this going to be an insane speed boost to any Rails app: nope. Should we merge it: yep. p.s. If you know of a method call that doesn't modify a string input such as [String#gsub](https://github.com/schneems/let_it_go/blob/b0e2da69f0cca87ab581022baa43291cdf48638c/lib/let_it_go/core_ext/string.rb#L37) please [give me a pull request to the appropriate file](https://github.com/schneems/let_it_go/blob/b0e2da69f0cca87ab581022baa43291cdf48638c/lib/let_it_go/core_ext/string.rb#L37), or open an issue in LetItGo so we can track and freeze more strings. Keep those strings Frozen ![](https://www.dropbox.com/s/z4dj9fdsv213r4v/let-it-go.gif?dl=1)
* Merge pull request #19431 from hmarr/head-routingRafael Mendonça França2015-06-221-1/+2
|\ | | | | Respect routing precedence for HEAD requests
| * Respect routing precedence for HEAD requestsHarry Marr2015-03-201-1/+2
| | | | | | | | | | | | | | Fixes the issue described in #18764 - prevents Rack middleware from swallowing up HEAD requests that should have been matched by a higher-precedence `get` route, but still allows Rack middleware to respond to HEAD requests.
* | extract required_defaults from the conditions hash before constructing the routeAaron Patterson2015-06-082-6/+5
| | | | | | | | | | this way we can remove the strange "respond_to?" conditional in the `matches?` loop
* | Use block variable instead of globalschneems2015-06-011-1/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | ```ruby require 'benchmark/ips' Benchmark.ips do |x| x.report("$&") { "foo".sub(/f/) { $&.upcase } } x.report("block var") { "foo".sub(/f/) {|match| match.upcase } } end ``` ``` Calculating ------------------------------------- $& 48.658k i/100ms block var 49.666k i/100ms ------------------------------------------------- $& 873.156k (± 9.3%) i/s - 4.331M block var 969.744k (± 9.2%) i/s - 4.818M ``` It's faster, and gets rid of a few "magic" global variables
* | ActionDispatch::Journey::Routes#empty? test casesValentine Valyaeff2015-05-191-1/+1
| |
* | Added ActionDispatch::Journey::Routes#empty?juggernaut-2015-05-181-0/+4
| |
* | Merge pull request #18392 from brainopia/fix_route_requirementsArthur Nogueira Neves2015-04-271-1/+1
|\ \ | | | | | | Correct route requirements by overriding defaultls
| * | Correct route requirements by overriding defaultls (fixes #18373)brainopia2015-01-081-1/+1
| | |
* | | sort_by instead of sortYang Bo2015-04-081-1/+1
| |/ |/| | | | | | | | | it is avoid sort errot within different and mixed keys. used `sort_by` + `block` to list parameter by keys. keep minimum changes
* | Merge pull request #15806 from tgxworld/partition_routes_during_setupAaron Patterson2015-03-022-8/+13
|\ \ | | | | | | Partition routes during setup.
| * | Partition routes during setup.Guo Xiang Tan2015-02-262-8/+13
| | | | | | | | | | | | | | | | | | | | | Partitioning of all the routes is currently being done during the first request. Since there is no need to clear the cache for `partitioned_routes` when adding a new route. We can move the partitioning of the routes during setup time.
* | | Revert "Merge pull request #18764 from tsun1215/master"Jeremy Kemper2015-02-261-1/+0
|/ / | | | | | | | | This reverts commit b6dd0c4ddebf5e7aab0a669915cb349ec65e5b88, reversing changes made to de9a3748c436f849dd1877851115cd94663c2725.
* / Explicitly ignored wildcard verbs from head_routesTerence Sun2015-02-081-0/+1
|/ | | | | | In match_head_routes, deleted the routes in which request.request_method was empty (matches all HTTP verbs) when responding to a HEAD request. This prevents catch-all routes (such as Racks) from intercepting the HEAD request. Fixes #18698
* Remove some comments about Ruby 1.9 behaviorsRafael Mendonça França2015-01-041-1/+1
|
* Remove deprecated option `use_route` in controller testsRafael Mendonça França2015-01-041-15/+0
|
* remove unused `#optional_parts`Sergey Alekseev2014-12-051-4/+0
| | | | | This method was copied from journey at https://github.com/rails/rails/commit/56fee39c392788314c44a575b3fd66e16a50c8b5#diff-2cfaf53c860732fea8689d6f2002594bR78. `grep -nr 'optional_parts' .`
* remove unused `#generalized_table`Sergey Alekseev2014-12-051-45/+0
| | | | This method wass copied from journey at https://github.com/rails/rails/commit/56fee39c392788314c44a575b3fd66e16a50c8b5#diff-d89de8881fc4b9f10cb3e4fc7b2463f3R53. However it looks the method was unused in journey at those point as well.
* Merge pull request #17827 from rkh/rkh-fix-or-patternAaron Patterson2014-12-011-0/+5
|\ | | | | Fix OR in Journey patterns
| * make OR in journey patterns compile to a valid regular expressionKonstantin Haase2014-11-291-0/+5
| |
* | Pass symbol as an argument instead of a blockErik Michaels-Ober2014-11-294-8/+8
|/