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

Ticket #9450 (closed enhancement: fixed)

Opened 2 years ago

Last modified 2 years ago

[RESEARCH] Optimisation code for named_routes

Reported by: nzkoz Assigned to: core
Priority: normal Milestone: 2.0
Component: ActionPack Version: edge
Severity: normal Keywords:
Cc: michael@novemberain.com

Description (Last modified by nzkoz)

The attached patch implements an optimisation for the methods generated by named routes. The generated methods will avoid the costly call to url_for if all of these conditions hold:

  1. Positional arguments are used (i.e. foos_url(1) not foos_url(:id=>1))
  2. Every argument is passed to the helper method
  3. there are no :requirements specified for that named route

If all those conditions hold, the helper will simply do string interpolation, which results in a significant speed increase. A loop of 50 000 generations for a standard resourceful named route shows around a 10x speed improvement.

Before  7.920000   0.050000   7.970000 (  8.098796)

After   0.590000   0.000000   0.590000 (  0.615036)

Testers are requested, especially in apps with large numbers of named routes.

Attachments

9450.diff (17.5 kB) - added by nzkoz on 09/06/07 08:42:08.
Another version of the patch
named_route_optimisation_unit_test_fix.diff (1.1 kB) - added by norbert on 09/11/07 15:53:57.
Fix for cch1's problem
de-optimize_named_route_generation.diff (0.6 kB) - added by cch1 on 09/17/07 18:31:54.
Disable named route optimization code of r7421
routing_optimisation_revert_applicable.diff (0.5 kB) - added by wildchild on 09/29/07 00:28:08.
revert changes for aplicable?

Change History

08/31/07 03:13:10 changed by nzkoz

  • description changed.

08/31/07 03:13:39 changed by nzkoz

  • description changed.

08/31/07 19:11:31 changed by bitsweat

  • keywords set to routes performance.
  • component changed from ActiveRecord to ActionPack.

I'm seeing 15-20x speedup. It'd be nice to allow hash params also, so long as every param is provided. Commit!

09/01/07 02:12:57 changed by nzkoz

Assuming the code that gets generated is sound, and doesn't break anything, there are a few other cases I can add:

  • customer_url(@customer, :some=>params) # => /customers/1?some=params
  • customer_url(:id=>@customer) # => /customers/1
  • customer_url(:id=>@customer, :some=>params) # => /customers/1?some=params

The hash cases will have a slightly more complicated 'guard condition', but will still be faster. We'll also have to check that we don't make the other cases too much slower, but I'm guessing that testing the guard conditions is going to be a tiny piece of overhead relative to the cost of url_for.

09/04/07 11:50:47 changed by norbert

  • type changed from defect to enhancement.

As stated on sunday I believe this patch broke one odd case in an integration test, all my other tests (in other applications, too) run fine. I am looking at the problem now, it hardly seems like a serious issue. And I'm all for improvements like these. +1

09/04/07 12:13:49 changed by norbert

Here's the code and the reason why it failed:

def test_should_create_cookies_after_login_and_delete_cookies_after_logout
    assert_difference('Session.count', 1) do
      post sessions_url, :username => 'manfred', :password => 'secret'
      assert_response :redirect
    end

    ...

When sessions_url gets called, it triggers line 1209 in the patched routing.rb:

elements << '#{request.protocol}'

Apparently request hasn't been set yet at that time, leading to a typical "unexpected nil" error.

This is not a problem with this patch, I just shouldn't be using named routes in places like these. :)

09/05/07 04:34:45 changed by nzkoz

The new version of this patch should address the errors in integration tests, as you could previously get foos_url without an @request, I've disabled the optimisation code for integration tests.

09/05/07 09:53:09 changed by lifofifo

Works great for me. +1

nzkoz : Could you please upload your benchmark script as well ?

Thanks.

09/05/07 20:38:02 changed by nzkoz

The benchmark script is simply:

    n = 50000
    Benchmark.bm do |x|
      x.report("_url") { n.times {post_url(1)}}
      x.report("_path") { n.times {post_path(1)}}
    end

Run through curl and results sucked out of the logs.

09/06/07 08:42:08 changed by nzkoz

  • attachment 9450.diff added.

Another version of the patch

09/06/07 08:47:24 changed by nzkoz

I've extended the patch to also support:

  • customer_url(@customer, :some=>params) # => /customers/1?some=params

This caused a few issues with the assertions in resources test, but my application appears to be working as expected.

Assuming there are no regressions, I intend to commit this in the next few days.

09/09/07 00:19:00 changed by nzkoz

  • status changed from new to closed.
  • resolution set to fixed.

(In [7421]) Optimise named route generation when using positional arguments. Closes #9450 [Koz]

This change delivers significant performance benefits for the most common usage scenarios for modern rails applications by avoiding the costly trip through url_for. Initial benchmarks indicate this is between 6 and 20 times as fast.

09/11/07 15:07:55 changed by cch1

  • status changed from closed to reopened.
  • resolution deleted.

I'm having problems with this change in a Unit Test for ActionMailer. Tests fail with "undefined local variable or method 'request'..."

Reverting to r7420 resolves the problem. Here is a snippet from my code:

class UserNotify < ActionMailer::Base

  def signup(options)
    default_url_options[:host] = "myhost.com"
    @body["url"] = verify_account_url(:token => sender.token)
  end

The unit test itself is not shown -it's pretty simple.

The problem seems to be that named routes can't be used in ActionMailer for Unit Tests. I think the optimization needs to be disabled for Unit Tests as well.

09/11/07 15:53:57 changed by norbert

  • attachment named_route_optimisation_unit_test_fix.diff added.

Fix for cch1's problem

09/11/07 15:56:21 changed by norbert

cch1, could you try the patch I just uploaded? It disables the optimisation for all tests from test_help, which is usually included somewhere at the top of test_helper.

09/11/07 16:42:11 changed by cch1

I've applied your patch (named_route_optimisation_unit_test_fix.diff) successfully to r7440 and it does not fix the problem. Same error.

-Chris

09/11/07 23:19:23 changed by norbert

I thought this was only a problem in the tests, but apparently more people had this problem.

Generally, if you need to use absolute URLs from models it's usually best to use the :host parameter for _url helpers. You can either set it in some constant or, as I would recommend, dispatch the mail from your controller instead and pass the host as an argument.

09/12/07 01:27:01 changed by cch1

I thought using the default_url_options[:host] = "constant" approach was equivalent to using the :host parameter in the _url helpers -and DRYer. Am I wrong?

FWIW, I also tried something like this: verify_account_url(:host => "fred.com", ...) and still got the same error.

09/12/07 01:36:33 changed by jamie007

Even if you do use the :host option in _url methods, you get something like <"undefined local variable or method `request' for #<UserNotifier:0x279e738>">

There are a couple of cases where it is beneficial to use ActionMailer outside of the context of a controller i.e. tests and models (sticking to fat model, skinny controller philosophy). It feels dirty do have to pass in a url as one of the deliver_xyz arguments rather than using named routed directly in ActionMailer.

As mentioned in http://dev.rubyonrails.org/ticket/9533, it is only _url methods causing the 'request' error, _path methods work fine. Couldn't we make ActionMailer::Base.default_url_options[:host] and ActionMailer::Base.default_url_options[:protocol] get used in preference to request.protocol and request.host_and_port?

09/12/07 01:37:26 changed by jamie007

Sorry cch1, you beat m to it.

09/12/07 01:44:09 changed by nzkoz

It's probably worth simply disabling the optimisation code for cases where you include UrlWriter (such as action mailer).

I'll look into this in the next few days.

Anyone care to contribute a failing test case?

09/12/07 13:44:56 changed by jamie007

Below is a simple test case:

class AnyClass
  include ActionController::UrlWriter
  
  default_url_options[:protocol]  = 'http'
  default_url_options[:host]      = 'example.com'
  default_url_options[:port]      = 9999
end

def test_should_use_default_url_options_of_subject_class
  with_routing do |set|
    set.draw { |map| map.resources :shiny_products }
    
    assert_equal 'http://example.com:9999/shiny_products',        shiny_products_url
    assert_equal 'http://example.com:9999/shiny_products/1',      shiny_product_url(1)
    assert_equal 'http://example.com:9999/shiny_products/2/edit', edit_shiny_product_url(2)
  end
end

Note that a @controller instance is purposefully not available.

09/12/07 13:48:01 changed by jamie007

Sorry, amended below:

class AnyClass
  include ActionController::UrlWriter
  
  default_url_options[:protocol]  = 'http'
  default_url_options[:host]      = 'example.com'
  default_url_options[:port]      = 9999
end

def test_should_use_default_url_options_of_subject_class
  with_routing do |set|
    set.draw { |map| map.resources :shiny_products }
    
    instance = AnyClass.new

    assert_equal 'http://example.com:9999/shiny_products',        instance.shiny_products_url
    assert_equal 'http://example.com:9999/shiny_products/1',      instance.shiny_product_url(1)
    assert_equal 'http://example.com:9999/shiny_products/2/edit', instance.edit_shiny_product_url(2)
  end
end

09/17/07 12:23:52 changed by cch1

Any progress likely soon here? I'm dying with these <"undefined local variable or method `request' for #<Whatever:0x279e738>"> errors. I'm going to have to bail out of edge from this unless we can get a patch or a workaround soon.

nzkoz, you mentioned that I could disable the optimisation code. How? I tried ActionController::Routing.optimise_named_routes = false in several places and it doesn't seem to have any effect.

-Chris

09/17/07 18:09:56 changed by norbert

cch1, did [7501] fix this for you?

09/17/07 18:31:54 changed by cch1

  • attachment de-optimize_named_route_generation.diff added.

Disable named route optimization code of r7421

09/17/07 18:33:13 changed by cch1

I've added a patch to completely disable the functionality of r7421. Use it until such time as the named route optimization can be safely applied. I've found that my unit and functional work again. Particularly useful when used in conjunction with the resources_controller plugin.

09/17/07 18:34:35 changed by cch1

Whoa! Just as I upload my patch to remove the optimization, I see r7501. I'll give it a try and report back.

09/17/07 20:31:01 changed by cch1

Good News: 7501 seems to have fixed my problems with named route optimization. Incidental Bad News: 7500 has introduced another bug!

For this ticket, I am happy. Pending some suitable time for the others to review, let's close it.

(follow-up: ↓ 29 ) 09/18/07 03:48:42 changed by notahat

I still get "undefined local variable or method `request'" when running tests for my ActionMailers.

09/22/07 17:13:23 changed by nzkoz

  • keywords changed from routes performance to rails2.

(in reply to: ↑ 27 ) 09/22/07 17:36:25 changed by nzkoz

Replying to notahat:

I still get "undefined local variable or method `request'" when running tests for my ActionMailers.

Could you provide an example of what's failing and what it did before?

09/22/07 19:20:10 changed by nzkoz

(In [7572]) Disable the routing optimisation code when dealing with foo_url helpers. Add test to actionmailer to expose the problem they introduced. References #9450 [Koz]

09/22/07 22:35:21 changed by nzkoz

  • keywords deleted.

[7572] fixes the breakage in actionmailer, and I'll investigate a proper fix after the preview release is out.

09/24/07 00:42:38 changed by notahat

Yep, this fixes the ActionMailer test problems I had.

09/27/07 14:17:57 changed by jamie007

This seems to have fixed the _url helpers but broken _path helpers. They now complain that a "undefined local variable or method `request'". I don't understand why _path methods need to know anything about requests?

09/28/07 00:56:50 changed by nzkoz

I think what you're seeing is [7605] coming into play

09/29/07 00:13:13 changed by wildchild

Please take a look at this issue with ActionMailer:

ActionView::TemplateError (You have a nil object when you didn't expect it! The error occurred while evaluating nil.protocol) on line #2 of app/views/notifier/signup_notification.erb: 1: Hello, <%= @account.login %>! 2: <%= formatted_activate_account_url(@account, 'html', :key => @account.activation_key.key) %>

Seems to be something affected by latest changesets.

09/29/07 00:26:06 changed by wildchild

I found that issue with 'nil.protocol' caused by http://dev.rubyonrails.org/changeset/7673 at line 47.

09/29/07 00:28:08 changed by wildchild

  • attachment routing_optimisation_revert_applicable.diff added.

revert changes for aplicable?

09/29/07 02:28:17 changed by nzkoz

[7676] should fix this stuff once and for all

It will only use the fast route if request is 'defined?' and set.

09/29/07 02:28:37 changed by nzkoz

  • status changed from reopened to closed.
  • resolution set to fixed.

10/22/07 13:36:51 changed by cch1

  • status changed from closed to reopened.
  • resolution deleted.

My ActionMailer is now throwing the same errors as seen earlier: undefined method 'request' for <MyActionMailerModel>. In my example, it is happening in a <%= link_to image_tag('logo.jpg') %>.

Are there residual problems in ActionMailer?

10/22/07 14:10:50 changed by cch1

To expound on my previous message, I am creating an ActionMailer model within an ActiveRecord model. The AM call is dynamically generated [Model.send(dynamically_generated_create_method_name)].

This problem was not happening on 18 October.

10/22/07 14:21:59 changed by cch1

Even more information: Changset [7970] revealed this problem, and I'm pretty sure it's in the image_tag call.

Can you put a solution up for AM that works as well as [7676] worked for other cases?

-Chris

10/23/07 07:46:04 changed by nzkoz

We have tests in ActionMailer which ensure that this works.

Do you have a sample that's failing? defined?(request) && request should handle ... well anything ;)

10/23/07 13:07:03 changed by cch1

NZKoz, The failure in my environment is a bit long, but here is the condensed version:

In my AR model (which is the only tricky part):

class Message < ActiveRecord::Base
...
  def initialize
    ...
    self.msg = UserNotify.send('create_' + template, options)
    ...
  end
...
end

And my AM model:

class UserNotify < ActionMailer::Base
  ...
  def documentation_request(options)
    ...
    default_url_options[:host] = 'www.myapp.com'
    ...
  end
  ...
end

And finally, in my AM template (documentation_request.text.html.rhtml):

  ...
            <%= link_to image_tag("logo.jpg"), "http://www.mywebsite.com" %>
  ...

The error (undefined method 'request') is throwing in asset_tag_helper:388 in compute_public_path, within the new 'path_to_image'.

Reverting to r7969 solves the problem and moving to r7970 introduces it. Hope this helps...if not, let me know if I can provide any other information. -Chris

10/23/07 23:24:34 changed by nzkoz

OK, I can reproduce the error in the unit tests now and will look into it.

(follow-up: ↓ 46 ) 10/23/07 23:29:43 changed by nzkoz

  • milestone changed from 1.x to 2.0.

(in reply to: ↑ 45 ) 10/24/07 17:36:56 changed by cch1

Replying to nzkoz: Excellent. Any idea of a workaround I can employ while waiting for a resolution?

10/24/07 20:18:43 changed by nzkoz

You'll have to freeze to an earlier revision that doesn't exhibit the problems.

10/28/07 11:58:02 changed by evolving_jerk

  • cc set to michael@novemberain.com.

10/29/07 01:52:21 changed by nzkoz

  • status changed from reopened to closed.
  • resolution set to fixed.

This is actually caused by the reference to @controller.request in compute_public_path.

#10014 can track this