How to measure and optimize performance of applications that use Zend Framework 1.x. A talk presented at the New York City Zend Framework Meetup (http://www.meetup.com/ZendFramework-NYCmetro/) on August 23, 2011.
2. Why a ZF Performance topic?
• I’ve recently helped several clients with
performance of their ZF apps
• Performance is important to everyone
today
3. What we’ll cover tonight
• Question: Does ZF performance differ
from regular PHP performance?
• Using ZF performance tools
– Zend_Db_Profiler
– Zend_Cache
• Other ZF performance optimizations
• Client side measurement and
optimizations
4. ZF vs. regular PHP
• ZF is PHP
– Framework is PHP
– Your app is PHP
• But it’s more PHP code than your app would use if built
from scratch
– Meant to cover common use cases
• With ZF’s MVC, each request goes through routing,
dispatch
• Each class contains redundant require_once() calls
– Redundant if you use class autoloader (best performance)
– Only in ZF 1.x. To be corrected in ZF 2.0
5. Zend_Db query profiler
• A good reason to use Zend_Db
• Better than manual profiling because you
won’t miss any queries
• See the actual SQL created by Zend_Db
• One way: Firebug/FirePHP
– In application.ini:
resources.db.params.profiler.enabled = true
resources.db.params.profiler.class =
"Zend_Db_Profiler_Firebug"
7. Profiling to a log file
// a good place to put this profiling code is in the postDispatch() event of a front
controller plugin
$db = Zend_Registry::get('db'); // defined in bootstrap
$profiler = $db->getProfiler();
$totalTime = $profiler->getTotalElapsedSecs();
$queryCount = $profiler->getTotalNumQueries();
foreach ($profiler->getQueryProfiles() as $i=>$query) {
$secs = $query->getElapsedSecs();
$msg = $i . ' - "' . $query->getQuery() . '"';
$msg .= ', Params: ' . implode(',', $query->getQueryParams());
$msg .= ', Time: ' . number_format($secs, 6). ' seconds';
$messages[] = $msg;
}
$log = $queryCount . ' queries in ' . number_format($totalTime, 6)
. ' seconds' . "n";
$log .= "Queries:n";
$log .= implode("n", $messages);
$logger = Zend_Registry::get(‘logger’); // defined in bootstrap
$logger->debug($log);
8. Log file results
2011-08-18T11:34:06-04:00 DEBUG (7): 2 queries in 0.937705 seconds
Queries:
0 - "SELECT COUNT(1) AS "zend_paginator_row_count" FROM "SQHMSTP"
LEFT JOIN "XUPMSTP" AS "UP1" ON QHAFSR = UP1.UPUID
LEFT JOIN "XUPMSTP" AS "UP2" ON QHAUSR = UP2.UPUID
INNER JOIN "XTVMSTP" AS "TV1" ON TV1.TVFLD = 'QHSTAT' and TV1.TVCODE = QHSTAT
INNER JOIN "XTVMSTP" AS "TV2" ON TV2.TVFLD = 'RPTTYP' and TV2.TVCODE = QHTYPE WHERE
(QHCOCD = '01')",
Params: , Time: 0.820897 seconds
1 - "SELECT "SQHMSTP"."QHCASE", "SQHMSTP"."QHCHAS", (QHADMM * 10000 + QHADDD * 100 +
QHADYY) AS "QHADDT", "SQHMSTP"."QHTYPE", "SQHMSTP"."QHDLR", "SQHMSTP"."QHSTAT",
"SQHMSTP"."QHRPRF", "SQHMSTP"."QHCREF", "SQHMSTP"."QHSTAT", CASE WHEN (QHSTAT =
'20' OR (QHSTAT = '40' AND QHRPRF = '')) THEN 1 ELSE 0 END AS "EDITABLE", CASE WHEN
(QHSTAT = '20' OR QHSTAT = '40') THEN 1 ELSE 0 END AS "DELETABLE", "UP1"."UPNAME"
AS "QHASSNAME", "UP2"."UPNAME" AS "QHAUSRNAME", "TV1"."TVDESC" AS "QHSTATDESC",
"TV2"."TVDESC" AS "QHTYPEDESC" FROM "SQHMSTP"
LEFT JOIN "XUPMSTP" AS "UP1" ON QHAFSR = UP1.UPUID
LEFT JOIN "XUPMSTP" AS "UP2" ON QHAUSR = UP2.UPUID
INNER JOIN "XTVMSTP" AS "TV1" ON TV1.TVFLD = 'QHSTAT' and TV1.TVCODE = QHSTAT
INNER JOIN "XTVMSTP" AS "TV2" ON TV2.TVFLD = 'RPTTYP' and TV2.TVCODE = QHTYPE WHERE
(QHCOCD = '01') ORDER BY "QHCASE" DESC FETCH FIRST 40 ROWS ONLY",
Params: , Time: 0.116808 seconds
9. Zend_Cache
• Flexible caching component
• Caches any kind of data: output from PHP
scripts, complete web pages, ACL objects, query
results
• Zend_Cache API stores cached data in your
choice of “backends” (next slide)
10. Zend_Cache
• Back-ends where cached data can be stored
– Zend Server memory or disk cache
– Disk (your choice of location)
– Memcached
– APC
– SQLite
– Xcache
– Static (for generating static files for Apache to serve)
– Two-tier fast/slow
11. Zend_Cache configuration
• Easiest way is in application.ini
– If you set up your app using Zend_Tool
; front-end
resources.cachemanager.database.frontend.name = Core
; lifetime of 3600 means one hour
resources.cachemanager.database.frontend.options.lifetime = 3600
; automatic_serialization enables non-strings (objects) to be cached
resources.cachemanager.database.frontend.options.automatic_serialization = true
; back-end
; ZendServer_ShMem is Zend Server’s shared memory cache
resources.cachemanager.database.backend.name = "ZendServer_ShMem"
resources.cachemanager.database.backend.customBackendNaming = true
12. Caching tip for Zend_Db_Table
• Do cache metadata (table/field definitions) if you
use Zend_Db_Table
• Otherwise you will have a performance hit
• The degree of performance penalty of always
reading metadata depends on the database
server
• Play it safe and cache this metadata
– Assuming tables/fields are relatively constant
// in application.ini
// (“database” cache was defined on previous slide)
resources.db.defaultMetadataCache = "database"
13. Use an opcode/bytecode cache
• Frameworks add classes and code to an app
• PHP ordinarily must read/interpret/compile all
that code on each request
• A bytecode cache stores the “compiled”
bytecode in memory after first execution,
speeding subsequent runs
• Examples of bytecode caches:
– Zend Server’s Optimizer+
– APC
– XCache
– Windows Cache Extension for PHP
14. ZF Performance Guide
http://framework.zend.com/manual/en/performance.html
• Covers several topics related to ZF performance
• Written by the ZF development team
• Among its recommendations:
– Avoid “action view helper”: invokes dispatch cycle
• Replace with view helpers that query a model directly
– “Use partial() only when really necessary”
• Partial() clones the whole View object. Use render() if do not
need a new, clean View object
– And…
15. Class loading
• The issues around class loading are given
special attention in the Performance Guide
• In particular, the “autoloader/require_once()”
issue is the most frequently discussed
performance “flaw” of ZF 1.x
• It will be fixed in ZF 2.0
• Details of 1.x “flaw” on next slide......
16. Autoloader/require_once() issue
• The good:
– ZF’s autoloader is deemed a well performing component
• Enabled in /public/index.php like so:
require_once 'Zend/Loader/Autoloader.php';
Zend_Loader_Autoloader::getInstance();
• The bad:
– Even though autoloader loads classes as needed, each class
executes require_once() statements at the top for each class it
might need
• Solution: remove require_once() statements from almost
every ZF class
– P.S. Matthew Weier O’Phinney says, “this will only improve
speed if an opcode cache is used.”
17. How to remove require_once()
Official UNIX way
% cd path/to/ZendFramework/library
% find . -name '*.php' -not -wholename
'*/Loader/Autoloader.php' -not -wholename
'*/Application.php' -print0 | xargs -0 sed --regexp-
extended --in-place 's/(require_once)/// 1/g'
Doesn’t remove it from Autoloader.php and
Application.php because it’s needed there!
19. Keep an eye on the front end
• Otherwise known as the “client” side
• Includes .js, .css, images, and AJAX calls
• Check it out with Firebug’s “Net” panel or your
favorite tool
• Example coming up...
20. HTTP requests
In particular, beware if several AJAX calls must execute on page load
(not shown here) in order for page to render
21. Apache rewrite rule
.htaccess usually looks like this:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME}
RewriteRule ^.*$ – [NC,L]
RewriteRule ^.*$ index.php [NC,L]
• Any request that’s not a real file gets routed into
ZF/PHP
• What’s the performance flaw?
22. Nonexistent files
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME}
RewriteRule ^.*$ – [NC,L]
RewriteRule ^.*$ index.php [NC,L]
• Nonexistent files (whether favicon.ico or
my.hacker.getya) get routed to ZF, putting load
on app server, before generating a 404 not
found error
• Shouldn’t the web server handle 404?
23. Solution
• I haven’t found a perfect solution
• To intercept normal “file not found” errors in Apache:
– RewriteRule !.(js|ico|gif|jpg|png|css|html|txt|log)$ index.php
• If I’m confident that app URLs shouldn’t have
any periods/dots in ZF URLs:
– RewriteRule !.([^.]+)$ index.php
– ZF will only receive period-free URLs
– Apache can then catch “weird” URLs such as
“w00tw00t.at.ISC.SAN” (I found this in a customer’s Apache log)
• Demonstration on next slide
• Better idea? Send to alan@alanseiden.com
25. Further learning
• Attend the NYC Web Performance Meetup
• Follow me at @alanseiden
• Keep coming to our ZF meetup:
http://www.meetup.com/ZendFramework-NYCmetro/
• Attend ZendCon, Oct. 17-20, 2011
• Share your discoveries—you are welcome to present at
the ZF Meetup
26. New York City area Zend Framework Meetup
http://www.meetup.com/ZendFramework-NYCmetro/
Affiliated with http://www.nyphp.org/
Thanks for attending Performance Tuning with Zend Framework
presented on Aug. 23, 2011 by
Alan Seiden http://www.alanseiden.com
alan@alanseiden.com
Twitter: @alanseiden
Sign up to hear about all our ZF meetups at
http://www.meetup.com/ZendFramework-NYCmetro/