Optimizing and Scaling 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.

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!