8 Ways to Speed Up the Performance of CakePHP Apps
Intro
It's a not so well kept secret that CakePHP is slow. What isn't well know is that this is done by design. I could get in a lot of trouble by revealing this, but I'm willing to take that risk. I have records, a paper trail a mile long, showing members of the Cake dev team investing heavily in the stock of Dell, IBM, Cisco and other server companies. We've all heard the expression "hardware is cheap, and programmers are expensive." The Cake team figure out how to monetize that by making a framework that is fast to develop with, but slow to run. They want you to throw more hardware at it. Ingenious, right? Well I'm here to end all of that. Every time you use one of the tips in this article it's one less gold chain on the neck of a Cake developer.
Notes
- I assume you're already using the ContainableBehavior and have optimized+indexed your SQL queries.
- I used ab to benchmark each of these changes and compared to the base benchmark, which is the plain app with debug at 0. I'm not including the actually benchmark numbers since they will vary by the application and the machine. Instead I'll include the approximate change in terms of percentage increase.
- No, you can't see my sample app.
1) Set Debug to 0
A no brainer, right? Well there are plenty of posts on the google group that say otherwise. Before even thinking about tuning your Cake app make sure debug is 0.
Here's the difference. For the Cake engine to run it generates two cache sections.
The first is /tmp/cache/models
. In there you'll find a file for every model your system containing the table schema. You know those "DESC table;" queries you see in the query output? That's what there for. Those queries go away when debug is 0.
The seconds cache is /tmp/cache/persistent
. There are a couple different files in there that are used by Cake when running your app. The one that generally causes the most slow down to generate is cake_core_file_map. This file stores the paths to various classes in your app. To build the file Cake does a logical, but still time consuming, search of your directory tree looking for the right file.
So what is the difference between debug 0 and debug >0. Oh, about 2.73517104 years. When debug is >0 the cache lifetime on these files is 10 seconds. Switching debug to 0 pushes the expiration to 999 days.
<tangent>
This actually brings up an important question: if something is a "no brainer," do the people that still don't do it have less then no brain? If people without brains still figured it out, are there people walking around with black holes where there brain would be. Am I in danger of having my brain pulled into it, like light when it's slips past the event horizon?
</tangent>
Approximate Increase
+80% to 100%
2) Cache your slow queries/web service requests/whatever
The Cake cache lib is a great tool for caching single parts of your application. It handles all the gory work of writing to a file or tying into a memory based caching engine. All you need to do is figure out what to cache.
Let's say you have a query that has been indexed and optimized, but is still too slow. The Cookbook provides an example of how to wrap it with the cache lib so that you don't need to run it every request.
Or if you have a part of your site that is filled with data returned from a web service, like a recent tweets block (not a great example, since most of the Twitter widgets are JavaScript, but roll with me here). There really is no reason to make the call to the web service on every request. Just wrap it with the cache lib like the above example.
Approximate Increase
+0% to 1000000% Really depends on your app and what your caching.
3) View Caching
Think of this as entire page caching. The Cookbook covers the basics and since rendering the page still runs through PHP there is some flexibility for maintaining dynamic parts of the page. For example, if you were running a store you could cache the product pages, but still have a block showing the user's shopping cart.
Note
There's a section in the Cookbook mixed in here that covers the various caching engines CakePHP supports. However, at the moment (1.2.1.8004) view caching uses file based caching and is independent of the cache library described in #2 .
Approximate Increase
+130% to 160%
4) HTML Caching
This one is my own creation. It's based on the same principal of the Super Cache for WordPress. Basically it takes the rendered page and writes it to your webroot as straight HTML. The next time the page is hit your web server can serve it directly without even having to go to PHP.
There are obvious limitations for this, such as no dynamic content on the page, and the cache won't be automatically cleared. Still it's great for things like RSS feeds or something like popurls where the anonymous viewers all get the same page.
Approximate Increase
~60000% - This isn't hyperbole, that's the real increase.
5) APC (or some other opcode cache)
Wikipedia describes APC as "a free, open source framework that optimizes PHP intermediate code and caches data and compiled code from the PHP bytecode compiler in shared memory." Whatever. It makes shit fast. And you don't have to change any of your code. Fuck yea. Where do I sign up, right?
Approximate Increase
+25% to 100%
6) Persistent Models
This one isn't mentioned in the Cookbook (I'll add it in the next few days if no one beats me to it. I put it on my todo whiteboard, right below "figure out why putting computers in the clouds is more efficient then their traditional ground based counter parts"). This one is simple to turn on. In your controller (or AppController) add the attribute:
var $persistModel = true;
After a page refresh you'll notice two new files in /tmp/cache/persistent
for each model included in the controller. One is the cache of the model and the other is a cache of the objects in the ClassRegistry. Like view caching mentioned above, this cache can only be saved on the file system.
Approximate Increase
+0% to 200%
How much this one helps depends on your application. If your controller only has one model and it isn't associated with any others you're not going to see much of a boost. In my demo app there was around 100% increase. There was one model in the controller, which was associated with 3 other models, which had associations of their own.
7) Store The Persistent Cache in APC
To turn enable this you need be using APC and set your "_cake_core_" cache to use APC. In your core.php put:
Cache::config('_cake_core_', array('engine' => 'Apc', 'duration'=> 3600, 'probability'=> 100, ));
This takes the cache files normally stored in /tmp/cache/persistent
(not including the persistent models) and stores them in memory.
Approximate Increase
~25%
This is a hard one to measure. I tried enabling APC without opcode caching to measure just this change, but never found a setting that didn't provide a speed bump over the base setup.
8) Speed Up Reverse Routing
There are two methods for doing this. The first is described in a post by Tim at Debuggable.com. Tim's method only works for certain link types and breaks the reverse routing feature. Mine uses caching and made it to Hollywood week of CakePHP Idol where Nate (the Simon of the core team) called it "clever", but it was ultimately sent home when it forgot the lyrics to Kansas' "Dust in the Wind." Yes, I'm drinking and watching American Idol as I write this.
Approximate Increase
~50%
Like all of these tips, the actual increase depends on your app. If you don't use many custom routes and don't have many links on your page your not going to see much of a benefit.
The End
It's up to you now. Figure out which of these works best for you. Go forth and produce speedy CakePHP apps.
Wait, One More Thing
I'm sure I missed something. Leave your best tips in the comments. If I get enough I'll make a second post and claim credit for myself.
52 Comments
Don't use $uses - It causes extra loops and hits on the ClassRegistry, and often is a signal of 'wrongness'.
Also in addition to APC there is Xcache and Memcached which will both boost performance. And lastly concatenate and compress your assets. A lot of 'slowness' can come from assets being loaded/transferred, and while proper use of e-tags can help increase browser cache use, there's always some goof like me who has all his browser caching turned off.
Thanks for the extra tips. It would be really cool if someone wrote a helper that automatically concatenated and compressed css/js.
causes the same problem like var $uses = array('Model');
?
Just follow the association along the chain from whatever your default $users attribute is populated with.
Anyway this article was a good read. Cheers. In regards to Persistent Models if I enable this is there going to be any adverse effect on my application? What is the impact?
( ! ) Fatal error: BehaviorCollection::trigger() [behaviorcollection.trigger]: The script tried to execute a method or access a property of an incomplete object. Please ensure that the class definition "MetaBehavior" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide a __autoload() function to load the class definition in /Users/damien/Sites/cake.loc/cake/libs/model/model_behavior.php on line 494
Call Stack
# Time Memory Function Location
1 0.0003 669872 {main}( ) ../index.php:0
2 0.5161 6011712 Dispatcher->dispatch( ) ../index.php:87
3 0.5685 6559144 Dispatcher->_invoke( ) ../dispatcher.php:171
4 0.8720 15333872 call_user_func_array ( ) ../dispatcher.php:204
5 0.8720 15334256 SongsController->listen( ) ../dispatcher.php:0
6 0.8720 15334400 Model->read( ) ../songs_controller.php:232
7 0.8720 15335168 Model->find( ) ../model.php:1129
8 0.8721 15338272 BehaviorCollection->trigger( ) ../model.php:2108
Why write pages on disk, when you can serve them from server's RAM memory directly ...:)
In addition to Mark Story's tip on concatenate and compress your assets: Read about it on the yahoo developer website: http://developer.yahoo.com/performance/rules.html
Good article, well written. Kudos.
but some weeks ago we had performance isses anyway. so checked my code with the xdebug profiler.
cookies are encryped by default and the cipher crypt method is very slow. i don't store sensitive data in these cookies - so i turned encryption off - now the sites runs about 0.3s faster.
It bothers me that cakePHP adds both javascript and css to the same location($scripts_for_layout). Why is that? CSS goes at the top and javascript goes at the bottom. The only time you would put javascript at the top is if you were injecting it into the DOM so it didn't block and loaded asynchronously... a more complicated matter which cakePHP doesn't do.
Thanks for the tip. I think the simplest solution would be for me to write a new helper(CssHelper) and utilize View::set as you mentioned to set $stylesheet_links_for_layout and $styles_for_layout.
@echo
The problem with that is html->css('file.css', null, null, false) would add a css link element to $scripts_for_layout also.
I am curious about removing $uses per the comment above and will benchmark that when I get a chance.
Thanks for the article, though. Good stuff.
I'd also be interested in hearing more about what happened w/ persistModel. I would guess the unserializing of the cached files could take awhile if they were really big. You'd probably have to run it through a profiler to know for sure.
,Wil
Anyway your reverse routing seems to work great - I reckon its knocking off about 0.5 seconds (times from firebug). I tried the var $persistModel = true, it seems to work for about 3 requests then breaks the app completely - really odd (that's using 1.2.1.8004)
John
Anyway upgraded the core to 1.2.2.8120 - anyway $persistModel = true definitely breaks 2 of my models and I spent hours trying to figure out why (but no answer yet). I'm getting the following errors:
Catchable fatal error: Object of class __PHP_Incomplete_Class could not be converted to string in C:\xampp\htdocs\cake1.2\cake\libs\debugger.php on line 451
And on my other model:
Fatal error: BehaviorCollection::trigger() [
Cheers
To my understanding you just have to define __toString().
E.g.
function __toString() {
return "something";
}
Works for me on PHP < 5.2.9.
I still have this error on PHP 5.2.9 and I'm still searching.
Hope this helps.
John
I think there are some limitations to the way that CakePHP handles cached models. The tmp file for the model and it's registry is just based on the model name, without any context as to which it was stored. So you may use the same model in multiple places in your app, but the registry may not match.
I've been working on this code lately:
http://bin.cakephp.org/view/2029293743
It attempts to create the related model objects as needed. If you have time I'd be interested in seeing how it works for you. Thanks.
I've tried your fix. I'm not using ACL on this app, only Auth.
Notice (8): Indirect modification of overloaded property User::$Aro has no effect [CORE/cake/libs/model/behaviors/acl.php, line 60]
Code | Context
$model = User
User::$name = "User"
User::$actsAs = array
User::$validationSets = array
User::$validate = array
User::$hasMany = array
User::$belongsTo = array
User::$recursive = -1
User::$__definedAssociations = array
User::$__loadAssociations = array
User::$useDbConfig = "default"
User::$useTable = "users"
User::$displayField = "name"
User::$id = false
User::$data = array
User::$table = "users"
User::$primaryKey = "id"
User::$_schema = array
User::$validationErrors = array
User::$tablePrefix = ""
User::$alias = "User"
User::$tableToModel = array
User::$logTransactions = false
User::$transactional = false
User::$cacheQueries = false
User::$hasOne = array
User::$hasAndBelongsToMany = array
User::$Behaviors = BehaviorCollection object
User::$whitelist = array
User::$cacheSources = true
User::$findQueryType = NULL
User::$order = NULL
User::$__exists = NULL
User::$__associationKeys = array
User::$__associations = array
User::$__backAssociation = array
User::$__insertID = NULL
User::$__numRows = NULL
User::$__affectedRows = NULL
User::$_findMethods = array
User::$_log = NULL
$config = array(
"requester"
)
$type = "Aro"
uses('model' . DS . 'db_acl');
}
$model->{$type} =& ClassRegistry::init($type);
AclBehavior::setup() - CORE/cake/libs/model/behaviors/acl.php, line 60
BehaviorCollection::attach() - CORE/cake/libs/model/behavior.php, line 291
BehaviorCollection::init() - CORE/cake/libs/model/behavior.php, line 252
Model::__construct() - CORE/cake/libs/model/model.php, line 418
AppModel::__construct() - APP/models/app_model.php, line 32
ClassRegistry::init() - CORE/cake/libs/class_registry.php, line 140
Controller::loadModel() - CORE/cake/libs/controller/controller.php, line 478
Controller::constructClasses() - CORE/cake/libs/controller/controller.php, line 423
Dispatcher::_invoke() - CORE/cake/dispatcher.php, line 224
Dispatcher::dispatch() - CORE/cake/dispatcher.php, line 211
[main] - APP/webroot/index.php, line 88
Fatal error: Cannot assign by reference to overloaded object in /cake/libs/model/behaviors/acl.php on line 60
Thanks for giving it a shot. If you remove the "&" from that line (CORE/cake/libs/model/behaviors/acl.php, line 60) it won't give you the error. Weird that you'd be even hitting that if you're not using ACL. If you're using PHP5 the "&" doesn't make a difference. You'll see other places in the Cake code where they do this:
There was also a brain fart from my behalf.
$config = array(
“requesterâ€
)
I initially had ACL and forgot to remove the ACL behavior.
However:
Fatal error: BehaviorCollection::trigger() [
I have Node and SharedItem but between them is a Polymorphic join table which is represented by two LeftNodesSharedItem and RightNodesSharedItem.
(By way of explanation Node is basically just a page of content for a webpage, SharedContent is data that can populate side columns)
If I access a node (which is the usual way) directly, $persistModel = true breaks the app, but if I access the relevant node methods indirectly via ClassRegistry::init and then use $this->render() - which I occasionally need to do from an unrelated controller - it still works.
I'm a bit swamped at the moment as we are trying to get this live tomorrow - but latter this week I want to dig into this and find out what is going on.
Catchable fatal error: Object of class __PHP_Incomplete_Class could not be converted to string in cake\libs\debugger.php on line 451
Any ideas? Other than that everything is working fine.
i have lots of repeated data in every pages like categories, site names and some settings variable mey be putting them in session will boost the speed but then....when update any of those values from admin panel i need to refresh those values from session....
i am thinking what to do
thanks for the tip, persistModel really worked well for me.
Just one thing that took a loong time to figure out : if your cake app is VERY slow (like 3-4 seconds for each page load), check that you have all the tmp directories set up, cause Cake won't create them for you :
app/tmp/cache
app/tmp/cache/models
app/tmp/cache/persistent
app/tmp/cache/views
app/tmp/sessions
app/tmp/logs
app/tmp/tests
That may sound obvious, but I developped my site locally and when I uploaded it to the webserver, I did not upload the tmp folder, as I knew it was only temporary data.
As a result, my site was running fine locally, but with HORRIBLE performance on the server.
However, validation errors were not showing for one of my models (but not others - weird).
I removed persistModel and the validation errors started showing again.
I post this here to help anyone else who might come across the same problem!
I have one query :
Can somebody tell me what is the difference between Cake1.1 and 1.2 as aplication codes are almost same, but the results are different.
This is also being used by popular portals like yahoo, yatra etc
One problem I continue to have is that my AJAX calls won't cache (view cache). If I access the url directly, the cache file is created, but when I access it with an AJAX call it does not. Does anyone know if this is by design and if there is a way around it?
app_controller.php
function loadModel($model, $id){
if (!isset($this->{$model}){
parent::loadMOdel($model, $id)
}
Maybe that could warrant a blog post.