CakePHP reverse routing is slow, … and what you can do

Reverse routing is a very handy feature, but also very expensive.

In my cake application, I’ve noticed that rendering the views often takes more time than processing the controller action. It takes like 2s to render the view, while processing the action takes 0.5s. Very weird, since views are just a matter of printing variables (and maybe some include() when using elements).

A little debugging point out that printing the tag cloud in my footer takes ~200ms. A tag cloud with 50 tags, using $this->Html() to print each links. It costs ~5ms per links. Combine that with the others links in my views (menu, sidebar etc… : another ~70 links = total of 120 links) and I ended up waiting ~2s, just for processing the various $this->Html(). After looking how reverse works, I found out the “bottleneck”.

– Each $this->Html() calls AppHelpers’ h()
h() calls core Helper’s url()
url() calls the router’s Router::url()

Router::url() is expensive. It loops though all your Router::connect() rules in app/Config/routes.php to find a match. How expensive the reverse routing depends on the number of rules you have.

My routes.php contain a little less than 600 rules. If the matching rule is at the bottom of the file (worst case), Router::url() will have to loop through all of the 600 rules. Multiply that by the number of url() calls (120 links) : 600*120 = It’s looping through 72.000 rules to print all the links … in the worst case … even if you print 120 times the same link.

Since your routing rules rarely change, a way of saving unnecessary operations is to cache the matching rules. Instead of looping through each rules, we’ll just cache the final url matching the url array() in $this->Html(). You need to have Memcached, Apc or xcache installed, since links will be cached in the ram, else it’s useless.

Create the file app/Views/Helper/AppHelper.php (if it doesn’t already exists) :

<?php
App::uses('Helper', 'View');

class AppHelper extends Helper
{
    public function url($url = null, $full = false)
    {
        if (is_array($url))
        {
            $url = array_merge(
                array(
                    'controller' => $this->request->controller,
                    'plugin' => $this->request->plugin,
                    'action' => $this->action
                ),
            $url);
            ksort($url);
        }
        Cache::config('router', array(
            'engine' => 'Memcached', // Or Xcache, or Apc
            'prefix' => 'routes_',
            'duration' => '+7 days'
        ));

        $key = md5(serialize($url).$full);
        if (($link = Cache::read($key, 'router')) === false)
        {
            $link = h(Router::url($url, $full));
            Cache::write($key, $link, 'router');
        }
        return $link;
    }
}

You can take out the Cache::config() bit, and put it in your core.php instead.

Going back to my example, rendering the tag cloud with 50 links, that was taking 200ms, is now takes 13ms. Rendering time for the complete view goes down from 2s to 150ms. Big win.

Cache duration will depends on your traffic. You wouldn’t like to have your ram filled with rearly used/expired routes.

Do want to cut more time ?

If you have more controller on you server, we can go a little farther.
I see no way of improving the md5() bit, that is already very fast. Falling back to crc32() is not so faster.

But we can increase the serialization time, if you install igbinary. It’s a replacement for the PHP default serialization engine, and benchmark [1][2] all says it’s good. It’s faster than php default serialization engine, and data takes less memory.

You just have to replace serialize() by igbinary_serialize() in the code.

By the way, the Memcached (notice the d) pecl extension uses igbinary as serialization engine by default, if installed.