Status Update -- A Fresh Look at Callbacks
After I finished the first stage of the ActionView refactor I was working on (I'm currently working on some small issues with Josh and getting ready to merge it into the official Rails3 branch), I decided to take a fresh look at the performance numbers. As expected, by initial refactor did not really improve performance (it wasn't intended to -- I mainly just moved things around and cleaned up the code path), but one thing that stuck out at me was how expensive the callback system is.
This is the case in both Rails and Merb, although Merb's smaller callback feature set makes it somewhat faster than Rails' at present (but not significantly so; both systems use the same basic approach). So I decided to spend some time today trying to make the callbacks faster. The first thing I noticed was that the ActiveSupport callbacks are fairly limited (for instance, missing around filters), and that ActionPack uses a small part of the ActiveSupport system and then layers on scads of additional functionality.
After spending a few hours trying to improve the performance of the callbacks, I remembered that Carl had considered an interesting approach for the Merb callbacks that involved dispensing with iteration in favor of a single compiled method that inlined the filters. In his original experiments (which supported before, after, and around filters, but not conditions), he was able to make filters run about an order of magnitude faster than Merb's system.
Effectively, the approach boils down to:
before_filter :foo
after_filter :bar
around_filter :baz
def _run_hookz
foo
baz do
yield
baz
end
end
This completely removes the need for iteration at runtime, and compiles down the hooks into a single, speedy method. What complicated matters a bit was:
- Rails' support for conditions (also supported in Merb, but not in Carl's original prototype)
- Rails' support for various filter types, including strings (which get evalled), procs, blocks, and arbitrary objects
Also, and this is a big caveat: this is most certainly not an optimization that is likely to significantly help most apps where the overhead is in database access or rendering. However, one of my personal goals for the work I'm doing is to reduce the need for things like Rack Metal by reducing the overhead of rendering a request through the full stack. Filters are used throughout the render path and we shockingly using 10-20% of the 4ms a render :text => "Hello world" was taking up. I was surprised that the action itself was only about 7% of the default render path.
So again, shaving off under 1ms is not likely to help many apps, but it is a step in the direction of making it possible to build high-performance APIs and service tiers on top of the full Rails request cycle. You can follow along this callback work on my optimize_callbacks branch.
P.S. Merb 1.0.8 should be released tomorrow. It'll include a bunch of pending fixes (including a fix for PIDs being overwritten in production when using merb -i)