A few weeks ago I was surprised by a PHP out-of-memory error in the production
environment for one of my symfony 1.2 powered sites. This error was being
thrown by sfViewCacheManager, the class
responsible for deciding whether to render a cached or freshly executed
template. The particular line in question generated a "cache key" by running a
template's variables through md5(serialize($vars))
. This key is then
included in a URI that is used to find a cached template.
These are very resource-intensive functions, particularly when passing them
large model or form objects. The cause of my problem seemed apparent. I went
through my application and added sf_cache_key
variables to all my calls to
include cached components and partials. With that variable provided by me,
symfony would no longer have to run cached paritals' variables through
md5(serialize($vars))
. Problem solved, right?
No. The errors came back the next day. I dug into the symfony core once again
found the root cause for these errors. When the cache was switched on in
settings.yml
a partial's variables were always passed through
md5(serialize($vars))
, regardless of whether the cache is enabled for that
particular partial in cache.yml
. This is why this error was only happening
on servers with the cache enabled and why only adding a sf_cache_key
variable to cached partials didn't solve the problem.
I wrote a custom partial view class for my application that made sure every
partial had at least a random sf_cache_key
variable, insuring that
md5(serialize($vars))
would never have to be called. That was a temporary
fix to keep this particular site online. Today I committed a more reliable fix
to all versions of symfony and the performance improvement is quite stunning.
The Benchmarks
To benchmark this improvement I created a very simple project with one action and one partial:
$ mkdir ~/Sites/cache_key_fix
$ cd ~/Sites/cache_key_fix
$ symfony generate:project cache_key_fix
$ symfony generate:app frontend
$ symfony generate:module frontend main
The main/index
action is very simple, although you can see I'm passing a
very large object to my partial, the sfContext
instance.
// apps/frontend/modules/main/actions/actions.class.php class mainActions extends sfActions { public function executeIndex() { } } // apps/frontend/modules/main/templates/indexSuccess.php <?php for ($i = 0; $i < 100; $i++): ?> <?php include_partial('foo', array( 'foo' => sfContext::getInstance(), 'bar' => sfContext::getInstance())) ?> <?php endfor; ?> // apps/frontend/modules/main/templates/_foo.php <p>foo</p>
The cache key is never generated when the sf_cache
setting is disabled, so
I enabled that:
# apps/frontend/config/settings.yml
prod:
.settings:
cache: on
Since I'm comparing two revisions of symfony, I also added a request variable to the project configuration to be able to switch between checkouts of the core.
// config/ProjectConfiguration.class.php if (array_key_exists('before', $_GET)) { require_once dirname(__FILE__).'/../../sf_before/lib/autoload/sfCoreAutoload.class.php'; } else { require_once dirname(__FILE__).'/../../sf_after/lib/autoload/sfCoreAutoload.class.php'; } sfCoreAutoload::register(); class ProjectConfiguration extends sfProjectConfiguration { }
If you want to run this benchmark comparison on your own project, checkout a before and after copy of the symfony core.
$ svn co http://svn.symfony-project.com/branches/1.2 -r16904 ~/Sites/sf_before $ svn co http://svn.symfony-project.com/branches/1.2 -r16905 ~/Sites/sf_after
Once the project is in place, hit it once so the config cache is established:
$ curl http://localhost/cache_key_fix/web/index.php/main/index > /dev/null
Now you're ready for the actual benchmarking. Compare the output of the following commands:
$ ab -t 60 -n 20 http://localhost/cache_key_fix/web/index.php/main/index?before
$ ab -t 60 -n 20 http://localhost/cache_key_fix/web/index.php/main/index?after
You should see a significant difference. This is what I got on my MacBook:
Before
Connection Times (ms)
min mean[+/-sd] median max
Connect: 289 319 19.4 321 365
Processing: 127 181 54.4 182 388
Waiting: 127 181 54.4 182 387
Total: 462 501 57.0 488 731
After
Connection Times (ms)
min mean[+/-sd] median max
Connect: 72 112 60.9 96 308
Processing: 0 143 45.6 157 192
Waiting: 0 143 45.6 157 192
Total: 228 255 24.5 253 347
Whoa. The fastest "before" request (462 ms) is still longer than the slowest "after" request (347 ms), and the mean average shows nearly a 50% decrease in total response time (from 501 to 255 ms).
Of course, these numbers should be taken with a grain of salt. Not all applications pass large objects to partials, although the form object passed around the admin generator templates can be pretty big, depending on what you're storing in your model objects.
Please let us know if you notice any marked performance improvements in any of your applications!
Hello,
seem to be really great but how can we download this fix ?
thx
Yoni
Kris, just upped my sites and I was able to see a noticeable improvement in memory and CPU. good work.
Yoni, the change is in latest svn version which I would recommend to use anyway. I will release symfony 1.2.6 for example soon, but not having my sites running a few days after big changes. By doing so I avoid rereleasing too many versions
@Fabian: Well it might be an idea to release a special bugfix release, since this is a major issue. A version 1.2.5a wouldn hurt that much if it does not become a regular case. So far I would also vote for a 1.2.5a that does only add this patch to 1.2.5. Or at least release patches until new version come. Latest SVN is no option for people using externals that need to be as stable as a tag, not living like a branch.
@Kris: WOW, nice work, waited for that ticket to get closed. Fake cache keys look that ugly =)
Where can I get the patch?
@Yuriy If you're not using an svn:external and can't wait until the next release you can always download the patch from Trac: http://trac.symfony-project.org/changeset/16905/branches/1.2
my sites feel way snappier (I do have fairly large forms).
isn't it a good style to always pass a "sf_cache_key" parameter to your partials/components?
but anyway... nice fix!
Nice enhancement. Thanks a lot!
Waw big performance enhancement !
Is this kind of enhancement could be apply to SF1.0 or SF1.0 works differently ?
@Christoph that's up to you, but passing the sf_cache_key var should have no effect if the partial is not cached, which is now the case
The benefit to Doctrine users from this fix is HUGE. Because Doctrine is extremely OOP and the objects are large and cyclic. In some very raw tests things do seem about twice as fast under dev mode. On a page with about 4-5 queries and a fairly large amount of Doctrine data objects being passed to templates before was about 700ms and is now 350ms consistently.
@Sylvio - a change for 1.0 is coming. The svn comments added by Kris indicate that a backport for this branch will be done in the future (presumably because the architecture is quite different).
And, since we're still on 1.0, we look forward to this enormously. We're in no major rush - personally I am happy to wait a while for releases containing this fix.
@halfer @Sylvio This fix was backported to symfony 1.0 in r16907
@Kris - many thanks. We'll give this a try on our test server :o)
Hello,
A few of us are having similar problem with memory usage. But this is not related to caching. Please see http://forum.symfony-project.org/index.php?t=rview&goto=76230#msg_76230
Thanks. Alex.
Is this likely to make its way to the 1.1 branch also?
@Daniel - already available for 1.1:
http://trac.symfony-project.org/ticket/5814
@Alex - if you know your issue is unrelated please don't post it here. There is an ongoing discussion in the forum for that.
Great to read about these improvements, sounds like my questions raised in the forum (http://forum.symfony-project.org/index.php/m/76200 and http://forum.symfony-project.org/index.php/m/72996) got answered.
It would be great to have an intermediate bugfix release, my site on production is regularly suffering from this malady.
@all a version containing this stuff will come soon, however I had an issue on one of my test boxes, that I want to sort out before releasing. I take that responsibility for the stability of your boxes seriously and we have had a few nontrivial changes on the branch lately. Thanks for your understanding
We updated our 1.0 project yesterday and made some tests but nothing seems to have changed :/
Where are we supposed to see the changes ? With lazy_cache_key on or off (found in the SVN log, is there any documentation about this ?), there is no difference in the displayed execution time or memory in debug toolbar.
More information about where there is a real difference ?
@naholyr You now need to turn the sf_cache_key setting on in order to see these improvements. This will be documented in the coming minor releases. You also need to have the cache turned on, as noted above.
This may be a problem with the cache enabled I suppose (but I think it's ok as we have the "reload ignoring cache" button in the debug toolbar).
I mean, are we supposed to notice some change in the times and/or memory displayed in the debug toolbar ? Or is it something that will be seen only in the server's stats ?
@naholyr The patch improves the performance of calls to render a template partial or component that is passed one or more large objects without a sf_cache_key parameter when the cache is on.