Improved performance with byte code cache
Propel generates a lot of large class files. These can create a significant parse time performance hit. A byte code cache can significantly reduce parse time cost.
Any half serious project (> 10 tables) using propel will suffer significant parse time based performance problems. This is however inherent in propels ORM approach and using other big classes like Smarty adds to the problem.
Unfortunately the whole subject of byte code cache is not well addressed in php:
1. Zend's commercial Accelerator is now buried in the "Platform" which is an expensive subscription.
2. eAccelerator is probably the best option right now. However it is not completely compatible with PHP5.1.x, although is this is being addressed in 0.9.5 (and seems to be very stable - please test!).
3. APC is also plagued with php 5.1.x bugs. However it is supported by Rasmus and there has been discussion about including it in the core of PHP6.
With a byte code cache, the class size becomes less of an issue. The following test setup can be used to get an impression of the benefit of byte-code cache.
Environment: Single AMD Athlon 64 3200+ 2.0 GHz, FreeBSD 5.3, PHP 5.0.4, eAccelerator 0.93
test script
<?php
/**
* performance profiling for propel
* focusing on parse times for classes
*/
require_once 'lib/Timer.php';
// include propel 1.2 branch classes rather than shared 1.1.1 classes
ini_set('include_path', '.:/usr/home/oliver/propelsvn/packages');
// the order we require these classes in is important
// as it allow us to time then individually despite their own require statements
$requires = array(
// start of overhead classes
'creole/ResultSet.php',
'creole/Connection.php',
'creole/PreparedStatement.php',
'creole/common/ConnectionCommon.php',
'creole/common/PreparedStatementCommon.php',
'creole/CreoleTypes.php',
'creole/common/ResultSetCommon.php',
'creole/drivers/mysql/MySQLResultSet.php',
'creole/drivers/mysql/MySQLConnection.php',
'creole/drivers/mysql/MySQLPreparedStatement.php',
'creole/SQLException.php',
'creole/Creole.php',
'propel/adapter/DBAdapter.php',
'propel/adapter/DBMySQL.php',
'propel/PropelException.php',
'propel/Propel.php',
'propel/om/Persistent.php',
'propel/om/BaseObject.php',
'propel/util/Criteria.php',
'propel/map/ValidatorMap.php',
'propel/map/ColumnMap.php',
'propel/map/TableMap.php',
'propel/map/DatabaseMap.php',
'propel/map/MapBuilder.php',
'propel/validator/ValidationFailed.php',
'propel/util/BasePeer.php',
// end of overhead classes
// start of generated classes
// lib/propel/CustomerOrder.php has had
// require 'BaseCustomerOrderPeer.php' commented out, so we can time the
// parsing of that separately
'lib/propel/CustomerOrder.php',
'lib/propel/map/CustomerOrderMapBuilder.php',
'lib/propel/CustomerOrderPeer.php'
// end of generated classes
);
$master_timer = new Timer('Master Timer');
$master_timer->start();
foreach ($requires as $require)
{
require_once $require;
$master_timer->lap($require);
}
// implies the parsing of the conf file
// but this should be minimal
Propel::init('conf/order-conf.php');
$master_timer->lap('Propel::init()');
// also interesting to see how long it takes to
// get a connection
$con = Propel::getConnection();
$master_timer->lap('Propel::getConnection()');
// lets pretend to do something useful
$c = new Criteria();
$c->setLimit(30);
$orders = CustomerOrderPeer::doSelect($c);
$master_timer->lap('CustomerOrderPeer::doSelect($c)');
// and produce a report
?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>propel peformance - parse time</title>
<meta http-equiv='Content-Type' content='text/html; charset=iso-8859-1' />
</head>
<body>
<pre>
<?php
echo 'number of orders retrieved: ' . count($orders) . "\n";
foreach ($orders as $order)
{
echo $order->getDatePurchased() . "\n";
}
$master_timer->lap('foreach echo $order->getDatePurchased()');
$master_timer->stop();
echo $master_timer->report();
?>
</pre>
</body>
</html>
The Timer class is not included here for brevity, but you can probably guess what it does.
This produces the following results with eAccelerator disabled. Times are in ms. The page was refreshed until the minimum runtime was obtained. This time is very consistent on an unloaded server.
---------------------------------------------------------------------- Label Split Lap ---------------------------------------------------------------------- start1 0.028 0.028 creole/ResultSet.php 0.930 0.902 creole/Connection.php 1.599 0.669 creole/PreparedStatement.php 2.193 0.594 creole/common/ConnectionCommon.php 2.843 0.650 creole/common/PreparedStatementCommon.php 4.810 1.967 creole/CreoleTypes.php 5.457 0.647 creole/common/ResultSetCommon.php 7.068 1.611 creole/drivers/mysql/MySQLResultSet.php 8.492 1.424 creole/drivers/mysql/MySQLConnection.php 10.196 1.704 creole/drivers/mysql/MySQLPreparedStatement.php 10.918 0.722 creole/SQLException.php 11.343 0.425 creole/Creole.php 12.895 1.552 propel/adapter/DBAdapter.php 13.619 0.724 propel/adapter/DBMySQL.php 14.251 0.632 propel/PropelException.php 14.604 0.353 propel/Propel.php 16.547 1.943 propel/om/Persistent.php 16.941 0.394 propel/om/BaseObject.php 17.689 0.748 propel/util/Criteria.php 22.208 4.519 propel/map/ValidatorMap.php 22.722 0.514 propel/map/ColumnMap.php 23.689 0.967 propel/map/TableMap.php 25.285 1.596 propel/map/DatabaseMap.php 25.910 0.625 propel/map/MapBuilder.php 26.243 0.333 propel/validator/ValidationFailed.php 26.663 0.420 propel/util/BasePeer.php 31.610 4.947 lib/propel/CustomerOrder.php 38.457 6.847 lib/propel/map/CustomerOrderMapBuilder.php 39.568 1.111 lib/propel/CustomerOrderPeer.php 43.522 3.954 Propel::init() 45.020 1.498 Propel::getConnection() 45.985 0.965 CustomerOrderPeer::doSelect($c) 56.712 10.727 foreach echo $order->getDatePurchased() 57.138 0.426 stop34 57.152 0.014
And with the eAccelerator enabled:
---------------------------------------------------------------------- Label Split Lap ---------------------------------------------------------------------- start1 0.027 0.027 creole/ResultSet.php 0.303 0.276 creole/Connection.php 0.702 0.399 creole/PreparedStatement.php 0.933 0.231 creole/common/ConnectionCommon.php 1.174 0.241 creole/common/PreparedStatementCommon.php 1.450 0.276 creole/CreoleTypes.php 1.695 0.245 creole/common/ResultSetCommon.php 1.955 0.260 creole/drivers/mysql/MySQLResultSet.php 2.584 0.629 creole/drivers/mysql/MySQLConnection.php 3.390 0.806 creole/drivers/mysql/MySQLPreparedStatement.php 4.000 0.610 creole/SQLException.php 4.223 0.223 creole/Creole.php 4.782 0.559 propel/adapter/DBAdapter.php 5.183 0.401 propel/adapter/DBMySQL.php 5.603 0.420 propel/PropelException.php 5.820 0.217 propel/Propel.php 6.401 0.581 propel/om/Persistent.php 6.629 0.228 propel/om/BaseObject.php 7.027 0.398 propel/util/Criteria.php 7.486 0.459 propel/map/ValidatorMap.php 7.723 0.237 propel/map/ColumnMap.php 8.168 0.445 propel/map/TableMap.php 8.770 0.602 propel/map/DatabaseMap.php 9.163 0.393 propel/map/MapBuilder.php 9.387 0.224 propel/validator/ValidationFailed.php 9.615 0.228 propel/util/BasePeer.php 11.069 1.454 lib/propel/CustomerOrder.php 12.213 1.144 lib/propel/map/CustomerOrderMapBuilder.php 12.689 0.476 lib/propel/CustomerOrderPeer.php 13.556 0.867 Propel::init() 14.873 1.317 Propel::getConnection() 15.829 0.956 CustomerOrderPeer::doSelect($c) 26.071 10.242 foreach echo $order->getDatePurchased() 26.481 0.410 stop34 26.494 0.013
Brief analysis
Without acceleration the propel overhead classes chew up 31ms. Plus ~12ms for each table (depending on the number of fields and foreign keys). For a reasonable project with 10 tables this would create a parse time overhead of 31 + (10 * 12) = approx 151ms.
With eAccelerator enabled the propel overhead shrinks to 11ms. Each table now requires ~2.5ms so a projection for the same 10 table project would be: 11 + (10 * 2.5) = 36ms. A very significant reduction in overhead time (your total execution time will vary of course), both in relative (4.2x) and absolute terms (115ms saved per request).
Testing was done with php 5.0.4 because apc and eAccelerator are currently not reliable with php 5.1.x.
Further analysis to follow....
