debuggable

 
Contact Us
 

How To Save Half A Second On Every CakePHP Request

Posted on 26/2/09 by Tim Koschützki

Hey folks,

as an application comes closer and closer to its launch date, not having cared about performance during development becomes more and more of a problem.
There are several ways to improve the performance of your CakePHP application. The first thing one would think about is generally caching of your views and database queries. However, during development stage implementing caching can cause a lot of confusion and phantom bugs. In short, it might waste your time which is better put into features. Any performance improvement that does not effect how data is retrieved, stored and cached is welcome. If it affects your entire site and not only parts of it, it's all the better.

With the help of Mark Story's excellent CakePHP DebugKit I got the idea of disabling Cake's reverse route lookup to gain performance. Almost half a second per request for link-heavy sites.

The Hack

As you use the HtmlHelper to create links with the helper's link() method, you throw an url in the form of an array at Router::url() everytime. With the complex routes parsing done for every $html->link() call, this becomes a big issue for link-heavy sites. There will be the most overhead if you have a lot of routes installed. So I thought if it's easily possible to override the behaviour for standard urls that don't need routes parsing. A classical example would be:

$html->link('Check this Article', array('controller' => 'articles', 'action' => 'view', $article['Article']['id']));

Since this link is dynamic (depending on the article id) it does not need routes parsing (most of the time, more on that later). There are others, like 4 parameters, 2 parameters, pagination links that have named params in the url .. or only one parameter if only the 'controller' is present and you want to access the index action, etc. I have tinkered a little and wrote some code that we are using in most of our projects now and it has worked out quite well. In fact it is saving almost half a second for every request:

<?php
class AppHelper extends Helper {
  function url($url = null, $full = false) {
    $Router =& Router::getInstance();
    if (!empty($Router->__params)) {
      if (isset($Router) && !isset($Router->params['requested'])) {
        $params = $Router->__params[0];
      } else {
        $params = end($Router->__params);
      }
    }

    if (isset($params['admin']) && $params['admin'] && !isset($url['admin'])) {
      $url['admin'] = $params['admin'];
    }

    if (is_array($url) && isset($url['controller']) && !isset($url['page'])) {
      if (!isset($url['action'])) {
        $url['action'] = 'index';
      }

      $admin = '';
      if (isset($url['admin']) && $url['admin']) {
        $admin = Configure::read('Routing.admin') . '/';
        if (strpos($url['action'], $admin . '_') === 0) {
          $url['action'] = substr($url['action'], strlen($admin));
        }
      }
      unset($url['admin']);
      unset($url['plugin']);

      $count = count($url);
      if (4 == $count) {
        return '/' . $admin . $url['controller'] . '/' . $url['action'] . '/' . $url[0] . '/' . $url[1];
      }

      if (3 == $count) {
        if (isset($url['id'])) {
          $url[0] = $url['id'];
        }
        return '/' . $admin . $url['controller'] . '/' . $url['action'] . '/' . $url[0];
      }

      if (2 == $count) {
        return '/' . $admin . $url['controller'] . '/' . $url['action'];
      }

      if (1 == $count) {
        return '/' . $admin . $url['controller'] . '/index';
      }
    }

    return parent::url($url, $full);
  }
}
?>

How does it work?

As you can see, it's simply overriding the default behaviour for Helper::url() in your AppHelper. It goes through the classical cases and builds out the url via lightweight string concatenation. Not a beauty, but fast.

What it really does is breaking the reverse routing feature. That means if you specify such a route:

Router::connect('/login', array('controller' => 'auth', 'action' => 'login'));

Then create a link with $html->link('Login Now!', array('controller' => 'auth', 'action' => 'login')), the link url will not automagially transform into '/login' anymore. This is pretty bad, however if your site hardcodes the urls used in routes, this is not a problem. This means that instead of providing array('controller' => 'auth', 'action' => 'login'), you just do '/login', which I always do for specific pages since there aren't so many of them. So the hack helps for non-dynamic cases.

Usage

To use the code, acknowledge it's more like a site-specific optimization. By the way, for those of you who think I should just go ahead and contribute a patch to CakePHP that will make it possible to disable reverse route lookup, I talked to Nate already. He said he is in the process of rewriting the Routing for Cake 1.3 and it will be much faster. In the meantime, this might help you.

Copy and paste the url() function to your AppHelper class in app_helper.php. Try it out and see if it works for you. Obviously, it doesn't affect all cases and is pretty specific to how we write urls. However, it falls back to the normal url parsing if no appropriate case is found. Any suggestions, bug reports and contributions are welcome.

Disclaimer:

In no way do I claim that this code will work as well for you as it does for us. It's specific to how urls are written using $html->link() and you might screw over your app by using this. Take extra caution when using it - especially with reverse route lookup - and make sure to test your app thoroughly.

-- Tim Koschuetzki aka DarkAngelBGE

 
&nsbp;

You can skip to the end and add a comment.

Ihti  said on Feb 26, 2009:

Why you use helper for links? Isn't it simpler use just html;
Check this Article

With this you will save much more.

Jonathan Snook said on Feb 26, 2009:

@Ihti: using the routing/reverse routing is extremely handy. I sometimes have views that get shared across various actions and I like to automate the URL generation to make this possible.

Tim Koschützki said on Feb 26, 2009:

Ihti: Broken link.

Tim Koschützki said on Feb 26, 2009:

Ihti: Also as Jonathan pointed out, this is about keeping flexibility in your routing. If you hardcode all links, then there is no need to use any of this of course.

Ihti  said on Feb 26, 2009:

I know this, but when you use helper you sacrifice perfomances.
I'm workking on app for 1M users, I have to be careful about performances. For me is chipper hardcode links then use halpers.

Afnan  said on Feb 27, 2009:

I never use HTML helper. It looks like an overhead. We can do all things in plain HMTL and design it in some WYSIWYG tool. Helper prevent this.

Tarique Sani said on Feb 27, 2009:

You saved half a second with how many routes?

Jonah Turnquist said on Feb 27, 2009:

The fact that it saves a half a second is kind of sad if you ask me. Cake is just too slow... I'm glad I switched over to Yii

Kim Biesbjerg said on Feb 27, 2009:

Another idea might be to cache generated urls and this way either create some kind of a lookup table on the disk or maybe one file per unique url?

Haven't done any tests to see if this actually performs better, but I'm guessing yes.

Tim Koschützki said on Feb 27, 2009:

Tarique Sani: 20 routes and about 60 calls to $html->link().

Jonah Turnquist: Well Cake has some performance drawbacks. But with this hack for example I am at 0.5s per request for a pretty large site, which isn't too bad. Also Cake is getting faster with every release.

Kim Biesbjerg: How would you invalidate the cache? Like once every week?

Matt Curry said on Feb 27, 2009:

I like the cache idea. You don't need to invalidate it ever, since the only time it would change is when you changed your routes. In which case you're doing a new build and should be clearing out the cache anyway.

Pretty much the same concept as App::import. How it stores the paths to the classes in /tmp/cache/persistent.

Mike said on Feb 27, 2009:

@Afnan

As Snook and other cake experts have mentioned, understanding the advantages/disadvantages of helpers is the key to using them successfully. Making a blanket statement like 'the HTML helper is nothing but needless overhead' is categorically incorrect. The html, and other, helpers do a lot of things behind the scenes for you based on the current configuration of your application. There is a good reason that it takes a lot of overhead just to produce a single html link. This fact alone should tell you it does important things that could be insanely useful.

Afnan  said on Feb 28, 2009:

@Mike

Thanks Mike. I understand, It differs from case to case.

Darren said on Mar 02, 2009:

This isn't really on topic but you let the genie out of the bottle a little by mentioning 1.3 (which I have seen on SVN) - can you give us anymore visibility on what might be going into 1.3? (Like perhaps losing PHP4 support, etc.)

Nate Abele said on Mar 02, 2009:

Darren: We're still in the planning stages. We'll let you know when we decide something.

ylcz said on Mar 04, 2009:

Well done!
And looking forward to Cake 1.3

Damn Stupid Follower of Ideas  said on Mar 05, 2009:

Well, this seems like one of the most bizarre pieces of coding ideas I have ever seen - add some code overhead to your app to REMOVE a feature! Great! :)

This post is too old. We do not allow comments here anymore in order to fight spam. If you have real feedback or questions for the post, please contact us.