Elliot Haughin

I'm a freelance web developer.
I build clever, engaging, ass-kicking,
web applications. You want me?

email me, tweet me, skype me
Posted: Feb 13th

Scaling and Optimizing your CodeIgniter Application, with Benchmarks

There’s been a few discussions recently about optimizing CodeIgniter applications to make them faster, more reliable, and scalable.
So, let’s look at some methods of doing this, with ‘real’ numbers and benchmarks.

‘Baseline’ – The Bog Standard

First of all, we need to build a little controller that does ’stuff’.
It’s not extremely intensive, but it gives us a good base to test these optimization techniques.

function index()
{
$query = $this->db->get('module_pages');
 
$links = '';
 
if ( $query->num_rows > 0)
{
foreach ($query->result_array() as $page):
 
$links .= '<a href="'.site_url($page['uri']).'">';
$links .= ucwords($page['title']).'</a><br />';
 
endforeach;
 
$data['links'] = $links;
}
 
for ($i=0; $i < 10; $i++)
{ 
$this->db->like('title', 'London');
$query = $this->db->get('module_pages', 1);
 
if ( $query->num_rows == 1 )
{
$row = $query->row_array();
$row['body'] = str_replace('Getting', 'booya', $row['body']);
 
$data['body'] = $row['body'];
}
}
 
$this->load->view('welcome2', $data);
}

And our view file is like this:

<?=$links?>
<?=str_replace('booya', 'Getting', ucwords($body))?>

So, now let’s benchmark this little app… with the default CodeIgniter settings… no caching, active record debug on.

Concurrency Level:      10
Time taken for tests:   22.206642 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      857000 bytes
HTML transferred:       665000 bytes
Requests per second:    45.03 [#/sec] (mean)
Time per request:       222.066 [ms] (mean)
Time per request:       22.207 [ms] (mean, across all concurrent requests)
Transfer rate:          37.65 [Kbytes/sec] received

So, 45.03 Requests per second, not bad… but I think we can make some changes and see that number improve.

Result Looping

First off, let’s change:

foreach ($query->result_array() as $page):

to:

$pages = $query->result_array();
foreach ($pages as $page):

Let’s have a look at the performance difference:

Concurrency Level:      10
Time taken for tests:   21.391068 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      857000 bytes
HTML transferred:       665000 bytes
Requests per second:    46.75 [#/sec] (mean)
Time per request:       213.911 [ms] (mean)
Time per request:       21.391 [ms] (mean, across all concurrent requests)
Transfer rate:          39.08 [Kbytes/sec] received

So, as we can see… the improvement is very small, just 1.75 request/second quicker…
But they do all add up, so make sure you take a note of this optimization and use it from now on.

Memcache

Now, onto Memcache, the ‘king’ of caching objects, arrays, you-name-it, in PHP

I start up the memcache service, then change the controller code accordingly:

$memcache = new Memcache;
$memcache->connect('localhost', 11211) or die ("Could not connect");
$data = $memcache->get('view_data');
 
if ( !$data )
{
$query = $this->db->get('module_pages');
 
$links = '';
 
if ( $query->num_rows > 0)
{
$pages = $query->result_array();
 
foreach ($pages as $page):
 
$links .= '<a href="'.site_url($page['uri']).'">';
$links .= ucwords($page['title']).'</a><br />';
 
endforeach;
 
$data['links'] = $links;
}
 
for ($i=0; $i < 10; $i++)
{ 
$this->db->like('title', 'London');
$query = $this->db->get('module_pages', 1);
 
if ( $query->num_rows == 1 )
{
$row = $query->row_array();
$row['body'] = str_replace('Getting', 'booya', $row['body']);
 
$data['body'] = $row['body'];
}
}
 
$memcache->set('view_data', $data, false, 3600) or die ("Failed to save data at the server");
}
 
$this->load->view('welcome2', $data);

Now, let’s run the benchmark to see how this improves things.

Concurrency Level:      10
Time taken for tests:   17.124866 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      857000 bytes
HTML transferred:       665000 bytes
Requests per second:    58.39 [#/sec] (mean)
Time per request:       171.249 [ms] (mean)
Time per request:       17.125 [ms] (mean, across all concurrent requests)
Transfer rate:          48.82 [Kbytes/sec] received

Well, it’s faster, but not by much… I wonder how an opcode cache could improve things?

eAccelerator

eAccelerator is an opcode cache extension for php… basically, the way that it works in ’stupid’ terms is like this:

  1. We write our application in code (almost english)
  2. We ‘execute’ the code (by running the request)
  3. PHP converts our code into another language (machine-code… let’s call it matrix-esque)
  4. PHP the executes the matrix-esque code

eAccelerator interrupts just after PHP converts our code to its machine code and ‘caches’ it, so next time, it can skip the whole translation phase of execution.
Pretty simple huh?

Lets try the baseline code, with eaccelerator on the top of it.

Concurrency Level:      10
Time taken for tests:   10.122468 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      857000 bytes
HTML transferred:       665000 bytes
Requests per second:    98.79 [#/sec] (mean)
Time per request:       101.225 [ms] (mean)
Time per request:       10.122 [ms] (mean, across all concurrent requests)
Transfer rate:          82.59 [Kbytes/sec] received

Now, that’s more like it… here we’re still doing our SQL queries, and rendering all the data dynamically, but just caching the machine code, with a drastic improvement.

CodeIgniter Output Cachining

My main reason for doing this test was just to show off one of the features that I love most in CI, output caching.
Let’s see how well full-output caching works in CodeIgniter…

Some people believe that the overhead involved in writing the cache file outweighs the speed increase of actually rendering from a cache file.

$this->output->cache(3600);

Now we run our benchmark again, (baseline code, with the output caching on) and see if there’s a difference.

Concurrency Level:      10
Time taken for tests:   6.221538 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      857000 bytes
HTML transferred:       665000 bytes
Requests per second:    160.73 [#/sec] (mean)
Time per request:       62.215 [ms] (mean)
Time per request:       6.222 [ms] (mean, across all concurrent requests)
Transfer rate:          134.37 [Kbytes/sec] received

So, the difference huge, serving 160.73 requests per second, that’s a massive improvement… but I’m not going to stop there!

Output Caching + eAccelerator

How about the ultimate? – Output caching, and opcode caching! – eAccelerator with CI Output caching:

Concurrency Level:      10
Time taken for tests:   2.565136 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      857000 bytes
HTML transferred:       665000 bytes
Requests per second:    389.84 [#/sec] (mean)
Time per request:       25.651 [ms] (mean)
Time per request:       2.565 [ms] (mean, across all concurrent requests)
Transfer rate:          325.91 [Kbytes/sec] received

Who’s the daddy then?

CodeIgniter!

For those of you struggling to digest those numbers… here’s a pretty little graph (made with codeigniter… yeah, I’m hardcore!)

CodeIgniter Benchmark Results

Further Notes

As requested, here’s the results from a couple of other tests.

First off, serving the exact same output as static index.html:
1834.95 Requests per second… Amazingly fast.

And, replacing:

foreach ($query->result_array() as $page):
$links .= '<a href="'.site_url($page['uri']).'">';
$links .= ucwords($page['title']).'</a><br />';
endforeach;

with:

foreach ($query->result() as $page):
$links .= '<a href="'.site_url($page->uri).'">';
$links .= ucwords($page->title).'</a><br />';
endforeach;

Produces 46.19 [#/sec] (mean)…. again, a slight improvement over using arrays..
But hell, I love arrays!

Elliot

Related Posts

  1. CodeIgniter Multicache Library 0.1
  2. Building Web Applications That Scale
  3. Amazon S3 – The future of all our storage needs?
  4. Handling Passwords In CodeIgniter
  5. Kick-Start Your Web Application With These Tools
Sponsored Links

Comments

  1. [...] Haughin, one of my CodeIgniter All-Star brethren, has posted an excellent article entitled Optimizing and Scaling your CodeIgniter Application. In this article, Elliot takes you from a baseline CodeIgniter installation, through memcache, [...]

  2. Edemilson Lima says:

    First off, let’s change:
    foreach ($query->result_array() as $page):
    to:
    $pages = $query->result_array();
    foreach ($pages as $page):
    —————————————————-
    These two are almost the same thing.
    Can you benchmark the example below for us?

    while ($page = $query->_fetch_assoc()):

  3. John says:

    “active record debug on”

    How?!

  4. Elliot says:

    @John…

    Active Record (DB) Debug is an option in config/database.php

    $db['default']['db_debug'] = TRUE;

  5. Henrik says:

    Just do one more thing for me, just for the fun of it:

    Could you take the rendered HtML-code from that benchmarked page, save it as index.html, and run the benchmark again.

    The reason: how fast is a ‘dumb non-php page’ on that same server. This way we can see the impact of just running php and CodeIgniter. I think the comparison would be interesting.

  6. Elliot says:

    There we go guys… see the ‘further notes’ addon at the bottom of the post.

    Elliot

  7. John says:

    And i should see the output in my log files? As far as i can see, i don’t get any extra info!

  8. Elliot says:

    John,

    db_debug – TRUE/FALSE (boolean) – Whether database errors should be displayed.

    So SQL errors are outputted with this as true.

    Elliot

  9. [...] Haughin’s article on Optimizing and Scaling your CodeIgniter Application looks like a must read for anyone designing application and websites that wouldn’t qualify as [...]

  10. Jakob Buis says:

    I was one of those people who never bothered with caching, but you showed me the error of my ways today :p

    I’m still struggling with caching in situations where the results must be real-time. Any tips?

  11. John says:

    Ah right, i see. Cheers!

  12. Elliot says:

    Hey Jakob…

    For an optimization that still produces ‘live data’, try using eaccelerator, that came out about twice as fast as baseline.

    Elliot

  13. Dan M says:

    What are you using to generate the benchmark information?

  14. christoph says:

    well done.

    you wrote “For those of you struggling to digest those numbers… here’s a pretty little graph (made with codeigniter… yeah, I’m hardcore!)”

    Hmmm, that would be nice to share :)

  15. piker says:

    In your first screencast you talking about file routes.php and $installed_modules
    IMHO shuld be $installed_controllers. Why, because name “module” is used for view modules. Some beginners may have problem what is routed controllers or view modules.

    Nice work
    piker

  16. Elliot says:

    Hey guys, to answer a question….

    Dan M: I’m using ‘apache benchmark’, (command: ab)

    It’s a unix based benchmark tool that comes with apache. (httpd or httpd-devel packages).

  17. christoph says:

    why not modifying the cache logic & bypass php completely!

    do not name the cached file cache/md5(something)
    but cache/url

    e.g. cache/catalog/page/5540

    modify mod_rewrites .htaccess

    RewriteEngine On
    RewriteBase /
    RewriteCond %{REQUEST_FILENAME} !-f

    RewriteCond cache/%{REQUEST_FILENAME} !-f

    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)$ index.php/$1 [L]

    So you should have the ultimate cache at the second request of a ressource.

    Greetings from
    chris@belvini.de

  18. Elliot says:

    Hi christoph,

    That’s not a bad idea… but then you’d have to develop a cron task to delete the cache files after a certain age…
    And, there may be some issues with headers that you’d need to sort out in your httpd.conf… but I’m definitely going to look into it.

    Thanks,

    Elliot

  19. louis w says:

    Great article, very useful and informative. Also thanks for turning me on to apache benchmark util, I was not aware of this.

    Do you have any other recommendations on fine tuning our applications? Maybe an ongoing article series.

  20. Pete says:

    It would be useful to cache to completely static html files, as Christoph mentioned… but instead of a cron job, just provide an easy way to clear certain pages. Then, in your admin or form submission handlers you could simply specify which urls should be deleted (or directly regenerated then and there).

    Result: fully static website where possible, with fully dynamic back-end. Best speed *and* most power :)

  21. Frank M says:

    Elliot,

    Awesome article!

    I noticed you’ve got 10 concurrent requests taking place with “ab”. I tried the same (on my localhost), and noticed that I had 72 failed requests. Is there something I should be doing to allow for concurrent requests?

    Thanks,
    Frank

  22. Elliot says:

    Hi Frank,

    Thanks for the comment.
    To answer your question….

    A great deal of the ab configuration lies in your ’server’ spec.
    These benchmarks were done on a brand new macbook pro, running apache2, mysql5, and php5. If your machine isn’t quite as high-performance, or your apache isn’t set up in a way to handle concurrent requests with ease, then you may experience failed requests.

    You’ll need to finely tune your Apache’s StartServers, MaxServers, KeepAlives etc to handle more concurrency.

    Elliot

  23. Frank M says:

    Hey Elliot,

    Thanks for getting back to me.

    I’m running this test on a PowerBook G4, but I’m also running MAMP for my quick-dev setup.

    I’ve checked the Apache conf and I see a few “StartServers” in different conditionals that check whether or not certain modules are loaded (ie: prefork.c, worker.c, etc.). Is there any specific module I need to make the change on? I’d love to get a better idea of the concurrency load my apps/sites are able to handle.

    Thanks,
    Frank

  24. [...] Optimizing and Scaling your CodeIgniter Application – with Benchmarks! (tags: codeigniter optimization performance scalability php ci memcache eAccelerator caching) [...]

  25. [...] cached, there’s no need to parse the original (bigger) files. If you still doubt this, check these benchmarks and get on with [...]

  26. nathan says:

    any ideas on whether or not ‘block’ caching is possible? Such as part of a template which is mostly static and the block has dynamic data?.. such as for user profiles, where data is stored in sessions on login and drawn from there for database requests (like a user profile page), so cache files wouldn’t be exactly url-specific.

    I’m playing with Template [http://www.williamsconcepts.com/ci/codeigniter/libraries/template/index.html], and thinking about scripting in region-specific caching for pages that need it. Any ideas how to go about this?

    thx for the article, btw, always nice to feel more jazzed about CI than i already am :P

  27. hSATAC says:

    Great article!
    Could you tell me what’s the difference between result() and result_array()?
    And also what is the reason that using Result Looping is faster?
    I googled for a while, it seems there’s no performence difference between mysql_fetch_array and mysql_fetch_object.
    I also did my own ApacheBench test, they are all the same.
    (I did with -n 1000 -c 10 just like you did.)
    And I did them several times. Sometimes the requests per second changed in all those condition…therefore I hope you could tell me the reason or Result Looping Optim…Thx a lot!

Leave a Comment

© Copyright 2009 Elliot Haughin