#150 Rails Metal (revised)
- Download:
- source codeProject Files in Zip (81.1 KB)
- mp4Full Size H.264 Video (13.9 MB)
- m4vSmaller H.264 Video (7.64 MB)
- webmFull Size VP8 Video (9.06 MB)
- ogvFull Size Theora Video (17.9 MB)
As this request polls frequently we should make it as efficient as possible. Before we start optimizing it we’ll walk through the code so that you have a better idea as to how it works. Most of it work is done in a CoffeeScript file which repeatedly makes a call to the server to get the list of processes.
jQuery -> if $('#processes').length update = -> $.get '/processes/list', (list) -> $('#processes').text(list) setInterval(update, 3000) update()
The update
function in this code fetches the list of processes and then updates an element with the id
of processes
with that list. It does this every three seconds.
class ProcessesController < ApplicationController def index end def list render text: `ps -axcr -o 'pid,pcpu,pmem,time,comm'` end end
The path ProcessController
’s list
action. This action runs the ps
command to fetch the list of processes and renders it as text to the client.
The issue here is that we’re going through the entire Rails stack for the simple list
action which just renders some text. We don’t need the entire stack here and we can shave a few milliseconds off the request by bypassing it.
Bypassing The Stack
In the past we’ve done this by using Rails Metal which provides a way to respond to a request without using the whole Rails stack. Rails Metal was removed in Rails 3 and rightfully so as there are alternatives which work better. There is still something called ActionController::Metal
which, despite having the name, isn’t quite the same, being basically a stripped-down Rails controller. This won’t give us as much of a performance boost as we’re looking for so we won’t use that here.
The core of this issue is that much of the request processing now happens through Rack middleware. When a request comes in it goes through around twenty pieces of middleware before it hits the application itself. Our controller may be highly efficient but a request still needs to go through a lot of middleware before it gets there. To get around this we can write our own middleware component and place it at the top of the list so that it is hit first before any of the other processing happens. This is the solution we’ll be using in this episode.
Own Own Middleware Class
We need to write a piece of middleware that has the same behaviour as our list
action. Where should we put this code? We could put in the application’s /lib
directory but in this case we’ll put it in a new /app/middleware
directory. This way it will be automatically loaded. A middleware component is a simple Ruby class that meets two requirements. One is that it can be initialized with a app argument; the other is that is responds to call just like a Rack app does. Here’s our ProcessList
class.
class ProcessesList def initialize(app) @app = app end def call(env) if env["PATH_INFO"] == "/processes/list" list = `ps -axcr -o 'pid,pcpu,pmem,time,comm'` [200, {"Content-Type" => "text/plain"}, [list]] else @app.call(env) end end end
In call
we see if the path passed in matches the processes list path. If it does we respond in the same way the list
action does by passing the list of processes back. We need to respond in the way that Rack does, passing an array containing a status code, a hash of headers and the body of the response. If the path doesn’t match the call is forwarded on to the application that was passed in.
This piece of middleware now has roughly the same behaviour as the controller action but we still need to add it to our application. To do that we modify the app’s config file and add this line within the Application
class.
config.middleware.use ProcessesList
This won’t quite work as our class hasn’t been loaded by the time the config file is loaded. If we pass the class in as a string it will work and load it later.
config.middleware.use "ProcessesList"
Now when we run rake middleware it we’ll see our ProcessesList
class is listed but it’s near the end of the stack and we need it to be near the top.
$ rake middleware use ActionDispatch::Static use Rack::Lock use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007fb0eb0586d0> use Rack::Runtime use Rack::MethodOverride use ActionDispatch::RequestId use Rails::Rack::Logger use ActionDispatch::ShowExceptions use ActionDispatch::DebugExceptions use ActionDispatch::RemoteIp use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::ConnectionAdapters::ConnectionManagement use ActiveRecord::QueryCache use ActionDispatch::Cookies use ActionDispatch::Session::CookieStore use ActionDispatch::Flash use ActionDispatch::ParamsParser use ActionDispatch::Head use Rack::ConditionalGet use Rack::ETag use ActionDispatch::BestStandardsSupport use ProcessesList run Processes::Application.routes
Where we place a custom middleware component in the stack depends on what it needs. For example if it needs to use cookies we could place it after ActionDispatch::Cookies
. Each piece of middleware in this list was covered in episode 319 so if you’re curious as to what a particular one does you’ll find more information there. Our ProcessesList
class is very simple and so we can move it to the very top of the stack. To specify where it’s placed we use either insert_before
or insert_after
instead of use
when adding it, specifying the class it should some before or after.
config.middleware.insert_before Rack::Lock, "ProcessesList"
Alternatively we can use an index the place the middleware in the stack.
config.middleware.insert_before 0, "ProcessesList"
This will place ProcessesList at the top of the stack which we can see when we run rake middleware
again.
$ rake middleware use ProcessesList use ActionDispatch::Static use Rack::Lock <rest of list omitted>
When we visit our application now it still works as it did before and it’s using our ProcessesList
middleware to handle updating the list so that it doesn’t go through the entire stack.
Benchmarking
We could delete our ProcessesController
’s list
action now but before we do we’ll use it to do some performance benchmarking. We’ll update the routes file so that the /processes/old_list path
points to that controller action so that we can benchmark each one.
Processes::Application.routes.draw do root to: 'processes#index' match 'processes/old_list', to: 'processes#list' end
Whenever we’re benchmarking it’s a good idea to start the server in the production environment so that we can get an idea of the performance in production. We’ll detach the server too so that it’s not in the same process.
$ rails s -e production -d
We’ll use the ab
command to do the benchmarking and we’ll trigger 100 requests to our old process list first.
$ ab -n 100 -r http://localhost:3000/processes/old_list
When we run this we get an average time per request of around 15.5ms, which is around 65 requests per second. Let’s try our new list for comparison.
$ ab -n 100 -r http://localhost:3000/processes/list
This gives us an average of around 13.5ms and 74 requests per second. There’s not a huge improvement in speed so there’s not a huge improvement in speed but if you do this to a process that’s called extremely frequently then it may be enough to make a difference.
That’s it for this episode. This technique isn’t something that should be done everywhere as there aren’t that many places where saving a new milliseconds is vital but for those cases where it does this is a great approach.