Hi. I am travis and I work in a place long ago abandoned by the gods: New York City. By day, I am a Software Engineer at Patreon. By night, i am a normal person.

A Better Time with Rails url_helpers

A boy was given a knife. He was told "this is a sharp knife," but then when he tried to cut something with it, like a piece of meat or something, it didn't really work that well. And so the boy thought, "this knife isn't actually very good," but then the person who gave him the knife said "here...tilt it a little bit and do this other obscure thing with the way you're holding the knife", and lo and behold...the knife cut the meat.

Life and open source software sometimes give us knives that seem dull until we learn how to use them properly. And sometimes, knives just legitimately have problems with them. And sometimes, internet tutorials just give us bad advice on how to use the knife, but maybe at one point it was good advice. Life is complicated, so who knows, but in any case the knife I'm talking about is Rails.application.routes.url_helpers.

It's very common to generate URLs outside of the Rails controller context, and so it's very common to need the functionality of this module in a place where it doesn't come for free (a serializer, a job, etc). The internet has been innocently telling people for years to access the module directly, and this worked perfectly fine for a time.

But unfortunately, there are issues with using it this way in more recent versions of Rails (Github issue describing it, PR attempting to fix it). Per the fix description, every call to url_helpers generates a hefty new Module which is not cheap when invoked repeatedly. To illustrate the detriment of this, I set up a quick test Rails application. Here is the relevant code:

# routes.rb
Rails.application.routes.draw do
  resources :things do
    collection do
      get :faster
    end
  end
end

# app/whatever/url_helper.rb
class UrlHelper
  include Singleton
  include Rails.application.routes.url_helpers
end

# app/controllers/things_controller.rb
class ThingsController < ApplicationController
  def index
    things_json = (1..100).map do |i|
      {
        id: i,
        url: Rails.application.routes.url_helpers.thing_url(i, host: 'localhost')
      }
    end

    render json: things_json
  end

  def faster
    things_json = (1..100).map do |i|
      {
        id: i,
        url: UrlHelper.instance.thing_url(i, host: 'localhost')
      }
    end

    render json: things_json
  end
end

In the index action, we call the module directly in the code, per the StackOverflow advice. In the faster action, we use a helper class that includes the module, which seems to be how Rails would suggest that the module be used.

Let's see how these two approaches perform side-by-side. I ran a little test using ab and a single Rails instance (a lot of output omitted due to uselessness):

➜ ab -n 1000 http://127.0.0.1:3000/things

Concurrency Level:      1
Time taken for tests:   18.155 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      4815000 bytes
HTML transferred:       4485000 bytes
Requests per second:    55.08 [#/sec] (mean)
Time per request:       18.155 [ms] (mean)
Time per request:       18.155 [ms] (mean, across all concurrent requests)
Transfer rate:          259.00 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       1
Processing:    15   18   2.2     17      42
Waiting:       15   18   2.2     17      42
Total:         15   18   2.2     18      42
 ➜  ab -n 1000 http://127.0.0.1:3000/things/faster

 Concurrency Level:      1
 Time taken for tests:   8.540 seconds
 Complete requests:      1000
 Failed requests:        0
 Total transferred:      4815000 bytes
 HTML transferred:       4485000 bytes
 Requests per second:    117.09 [#/sec] (mean)
 Time per request:       8.540 [ms] (mean)
 Time per request:       8.540 [ms] (mean, across all concurrent requests)
 Transfer rate:          550.58 [Kbytes/sec] received

 Connection Times (ms)
               min  mean[+/-sd] median   max
 Connect:        0    0   0.1      0       1
 Processing:     7    8   1.3      8      23
 Waiting:        7    8   1.3      8      23
 Total:          7    8   1.3      8      23

In the first example (the index method) where we call the module directly, the request takes an average of 18 ms resulting in a throughput of 55 requests per second. "Not bad!" you might say, since all of your requests in production take 5 seconds on a good day. But what about when we simply include the module once instead of calling it directly? In that case (the faster method), the request takes an average of 8 ms resulting in a throughput of 117 requests per second, more than 2x as fast as the previous approach. Now I'm no chef, but that knife cuts meat adequately if you hold it the right way.

TL;DR don't call Rails.application.routes.url_helpers directly. include the module in your class and your code will be faster.

Postgres: Adding Foreign Keys With Zero Downtime