// you’re reading...

codeigniter

Optimizing and Scaling your CodeIgniter Application - with Benchmarks!

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

Discussion

24 comments for “Optimizing and Scaling your CodeIgniter Application - with Benchmarks!”

  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, [...]

    Posted by Optimizing a CodeIgniter Application - MichaelWales.com | February 13, 2008, 2:01 pm
  2. 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()):

    Posted by Edemilson Lima | February 13, 2008, 2:23 pm
  3. “active record debug on”

    How?!

    Posted by John | February 13, 2008, 2:28 pm
  4. @John…

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

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

    Posted by Elliot | February 13, 2008, 2:41 pm
  5. 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.

    Posted by Henrik | February 13, 2008, 3:19 pm
  6. There we go guys… see the ‘further notes’ addon at the bottom of the post.

    Elliot

    Posted by Elliot | February 13, 2008, 3:55 pm
  7. And i should see the output in my log files? As far as i can see, i don’t get any extra info!

    Posted by John | February 13, 2008, 4:27 pm
  8. John,

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

    So SQL errors are outputted with this as true.

    Elliot

    Posted by Elliot | February 13, 2008, 4:47 pm
  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 [...]

    Posted by Optimizing and Scaling your CodeIgniter Application | David Bisset: Web Designer, Coder, Wordpress Guru | February 13, 2008, 6:18 pm
  10. 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?

    Posted by Jakob Buis | February 14, 2008, 3:59 am
  11. Ah right, i see. Cheers!

    Posted by John | February 14, 2008, 4:54 am
  12. Hey Jakob…

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

    Elliot

    Posted by Elliot | February 14, 2008, 7:54 am
  13. What are you using to generate the benchmark information?

    Posted by Dan M | February 19, 2008, 1:45 pm
  14. 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 :)

    Posted by christoph | February 19, 2008, 4:30 pm
  15. 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

    Posted by piker | February 19, 2008, 4:54 pm
  16. 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).

    Posted by Elliot | February 19, 2008, 5:24 pm
  17. 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

    Posted by christoph | March 1, 2008, 5:12 pm
  18. 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

    Posted by Elliot | March 2, 2008, 10:43 am
  19. 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.

    Posted by louis w | March 9, 2008, 11:18 am
  20. 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 :)

    Posted by Pete | March 21, 2008, 2:49 pm
  21. 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

    Posted by Frank M | March 26, 2008, 6:42 pm
  22. 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

    Posted by Elliot | March 28, 2008, 4:51 pm
  23. 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

    Posted by Frank M | March 29, 2008, 2:18 pm
  24. [...] Optimizing and Scaling your CodeIgniter Application - with Benchmarks! (tags: codeigniter optimization performance scalability php ci memcache eAccelerator caching) [...]

    Posted by links for 2008-05-15 | Digitalistic - Mashup or die trying | May 15, 2008, 3:31 pm

Post a comment