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....