PHP Accelerators: The Missing Details

by Mike Willbanks on December 23rd, 2007

Since the overwhelming response to provide more details about my test, I have here. You can see the files (some items stripped for not telling everyone on the earth what I am building — that will be another post when it’s finished). I will also retest with some of the noted information that have been in the comments. After I did this test, I took out the Zend_Loader and did require_once which then APC flew in the first post. I am looking to change these all to require but didn’t want to take the time early this morning to figure out which classes I needed to remove to have simply require. Essentially I was pretty tired from my all night code session. Once I change all of these details I will retest. All of the caching and optimization tools were only the default configurations.

Test Server Details

  • P4 2.4Ghz
  • 1GB DDR 266
  • 80GB 7200RPM IDE
  • RHEL ES 4.0
  • PHP 5.2.5
  • MySQL 5.0.45
  • Apache 2.2.6

Testing Methodology

Between every test PHP extensions were added or removed (XCache w/ and w/o Zend . After these extensions were added or removed; I restarted apache. The load was always at approximately .05 when the test was started on each individual. The test was only done with ab: ab -c 10 -t 60 http://***********/index/login (this way I would miss the redirect).

The Files

Please note the test of PHP Accelerators utilized the commented lines (the lines with Zend_Loader).

Index

require('../lib/Pmt/Base.php');
Pmt_Base::run();

Bootstrap

define('PMT_PATH', realpath(dirname(__FILE__) . '/../../'));
define('PMT_PATH_APP',  PMT_PATH . '/app');
define('PMT_PATH_WWW',  PMT_PATH . '/www');
define('PMT_PATH_LIB',  PMT_PATH . '/lib');
define('PMT_PATH_CONF', PMT_PATH . '/conf');
define('PMT_PATH_TMP',  PMT_PATH . '/tmp');
 
set_include_path(get_include_path() . PATH_SEPARATOR . PMT_PATH_LIB);
 
require_once(PMT_PATH_LIB . '/Zend/Loader.php');
Zend_Loader::loadClass('Zend_Cache');
Zend_Loader::loadClass('Zend_Config_Xml');
Zend_Loader::loadClass('Zend_Db');
Zend_Loader::loadClass('Zend_Db_Table');
Zend_Loader::loadClass('Zend_Registry');
 
final class Pmt_Base {
	/**
	 * Initialize Base
	 * This initializes all of the components and settings
	 * that must be set in order for items to remain consistant.
	 */
	public static function init() {
		$conf = new Zend_Config_Xml(PMT_PATH_CONF . '/base.xml', 'development');
		error_reporting($conf->server->php->error);
		date_default_timezone_set($conf->server->timezone);
		$db = Zend_Db::factory($conf->db->type, array(
			'host'     => $conf->db->host,
			'username' => $conf->db->user,
			'password' => $conf->db->pass,
			'dbname'   => $conf->db->name,
			'options'  => array(Zend_Db::CASE_FOLDING => Zend_Db::CASE_LOWER)
		));
		Zend_Db_Table_Abstract::setDefaultAdapter($db);
 
		$cache = Zend_Cache::factory(
			'Core',
			'File',
			array('automatic_serialization' => true),
			array('cacheDir' => PMT_PATH_TMP . '/cache')
		);
		Zend_Db_Table_Abstract::setDefaultMetadataCache($cache);
 
		Zend_Registry::set('conf', $conf);
		Zend_Registry::set('db', $db);
		Zend_Registry::set('cache', $cache);
	}
 
	/**
	 * Run Base
	 * This will run the main website and initilize different
	 * items that are strictly for the website usage as well
	 * as handling all of the aspects of the controllers.
	 */
	public static function run() {
		Pmt_Base::init();
		Zend_Loader::loadClass('Zend_Controller_Front');
		Zend_Loader::loadClass('Pmt_Controller_Action');
		Zend_Loader::loadClass('Zend_Auth');
		Zend_Loader::loadClass('Zend_Acl');
		Zend_Loader::loadClass('Zend_Acl_Resource');
		Zend_Loader::loadClass('Zend_Acl_Role');
		Zend_Loader::loadClass('Pmt_Acl');
		Zend_Loader::loadClass('Zend_Session');
		Zend_Loader::loadClass('Zend_Session_Namespace');
 
		Zend_Session::setOptions(array('save_path' => PMT_PATH_TMP . '/session'));
 
		$acl  = new Pmt_Acl();
		$auth = Zend_Auth::getInstance();
		$session = new Zend_Session_Namespace('Default');
 
		Zend_Registry::set('acl', $acl);
		Zend_Registry::set('auth', $auth);
		Zend_Registry::set('session', $session);
 
		$cntl = Zend_Controller_Front::getInstance();
		$cntl->throwExceptions(true)
		     ->setControllerDirectory(array(
				'default'  => PMT_PATH_APP . '/default/cntl'
			 ))
			 ->dispatch();
	}
}

Pmt_Acl

class Pmt_Acl extends Zend_Acl {
	public function __construct() {
		$this->add(new Zend_Acl_Resource('index'));
		$this->add(new Zend_Acl_Resource('dashboard'));
		$this->add(new Zend_Acl_Resource('*****'));
		$this->add(new Zend_Acl_Resource('*****'));
		$this->add(new Zend_Acl_Resource('*****'));
		$this->add(new Zend_Acl_Resource('*****'));
		$this->add(new Zend_Acl_Resource('*****'));
 
        $this->addRole(new Zend_Acl_Role('guest')); 
        $this->addRole(new Zend_Acl_Role('member'), 'guest');
 
        $this->deny('guest');
        $this->allow('guest', 'index');
        $this->allow('member');
	}
}

Pmt_Controller_Action

class Pmt_Controller_Action extends Zend_Controller_Action {
	function preDispatch() {
		$auth = Zend_Registry::get('auth');
		$acl = Zend_Registry::get('acl');
		$role = (!$auth->hasIdentity()) ? 'guest' : 'member';
		$resource = $this->_request->getControllerName();
		$action = $this->_request->getActionName();
		if (!$acl->has($resource)) {
			$resource = null;
		}
 
		if (!$acl->isAllowed($role, $resource, $action)) {
			if (!$auth->hasIdentity()) {
				$this->_redirect('/index/login');
			} else {
				$this->_redirect('/error/privileges');
			}
		}
	}
}

The Login Page

The form class is a custom render for HTML_QuickForms — you can see this file next

	public function loginAction()	{
		Zend_Loader::loadClass('Pmt/Form.php');
		$f = new Pmt_Form('login', 'post');
		$f->addElement('text', 'username', 'Username:', array('maxlength'=>25))
		  ->addRule('userName', 'Please enter a username', 'required')
		  ->addElement('password', 'password', 'Password:', array('maxlength'=>32))
		  ->addRule('password', 'Please enter a password', 'required')
		  ->addElement('submit', 'submit', 'Login');
 
		if ($f->validate() && ($clean = $f->exportValues())) {
			//this is never run inside here.
		}
		$this->view->login_form = $f->render();
	}

Pmt_Form

You may be wondering why I dynamically turn on and off error reporting here. Well HTML_QuickForms isn’t exactly PHP 5 nice and has quite a bit of warnings and/or strict errors. It actually speeds things up since the error reporting is not having to work).

$err = error_reporting(0);
require_once('HTML/QuickForm.php');
require_once('HTML/QuickForm/Renderer/Tableless.php');
error_reporting($err);
unset($err);
 
class Pmt_Form extends HTML_QuickForm
{
	private $_err;
 
	public function __construct($name='form', $method='post', $url='')
	{
		$this->_err = error_reporting(0);
 
		$url = (empty($url)) ? $_SERVER['REQUEST_URI'] : $url;
		parent::HTML_QuickForm($name, $method, $url);
		$this->removeAttribute('name');
		return $this;
	}
 
	public function addRule($element, $message, $type, $format=null, $validation='server', $reset=false, $force=false)
	{
		parent::addRule($element, $message, $type, $format=null, $validation='server', $reset=false, $force=false);
		return $this;
	}
 
	public function &addElement($element)
	{
		$a = func_get_args();
		$as = array();
		for($c=0;$c<count ($a);$c++)
		{
			$as[] = '$a[' . $c . ']'; 
		}
		$as = implode(', ', $as);
		eval('parent::addElement(' . $as . ');');	
		return $this;
	}
 
	public function exportValues($e = null)
	{
		$ret = parent::exportValues($e);
		error_reporting($this->_err);
		return $ret;
	}
 
	public function render()
	{
		$r = new HTML_QuickForm_Renderer_Tableless();
		$this->accept($r);
		$ret = $r->toHtml();
		error_reporting($this->_err);
		return $ret;
	}
}
</count>

From PHP

13 Comments
  1. I’d be curious to see a comparison between the Zend autoloader and the ezc autoloader. Zend uses include and ezc uses require internally.

  2. Kaloyan K. Tsvetkov permalink

    Can you also try a separate test, on which you got all the files compiled with apc_compile_file() in advance ? This should eliminate the drawbacks of conditional includes (the ones that are hard to avoid, like db classes, input filters,views, controllers, etc) when using APC, so it will probably give better results (the only overhead will be the class_ and interface_exists checks in the loader, which in this apc-compiled-in-advance scenario seem useless to me).

  3. @Kaloyan
    I will attempt this during the next test. I plan on doing this later on as I am waiting on Andi to check and see if there is any additional configuration I should test with the Zend Platform before I continue testing on the other layers again.

  4. Kaloyan K. Tsvetkov permalink

    One last comment: the Zend_Loader::loadClass() method should be used only in situations where the classname that is about to be loaded is dynamicly composed, like for example the views, the fitler adapters, the action controllers, etc. However, if the classname is known in advance, like in the bootstrap file above, using Zend_Loader::loadClass() is not recommended, because you do not need the overhead of parsing the classname into a filename – you already know the filename (e.g Zend/Config/Xml.php for Zend_Config_Xml). I think this is in the documentation, but I am not quite sure about that.

    Also … I think everybody knows that, but anyway: using full path includes/requires is faster then relying on include_path, because we are skipping all the extra checks in each “segment” of the include_path to find the file we are looking for. So, for the bootstrap, the best approach to me is to use require_once with full paths for the Zend Framework classes that you load in advance.

    Finally, thank you for the prompt reply ;)

  5. @Kaloyan
    Yeah, I was just simply creating a base application and I never really looked into the performance of the Zend_Loader as I have seen it in a few applications over the last few months. So I decided to do a reality check on using it. I am not sure I saw it in the documentation anywhere but I didn’t really check all that hard.

    I always attempt to use the fullpath includes when I can, that is why I create the constants in the script ahead of time and simply use those constants to find the library anywhere. The include path is just set for the cases where I might forget and during development I am not typically too concerned. Before moving it into a test or production environment I typically remove the include path and ensure that everything is running correctly without it and then put it back in just for those “just in case” opportunities.

  6. Mike, if by “default configurations” for XCache you mean you just added ‘extension = xcache.so’ to your php.ini, opcode caching would not have been enabled. Can you provide more detail about the “default configurations” you used for the various extensions?

  7. @Clay:
    XCache I used as a Zend Extension
    I used the default configuration included with the source (here is the applicable config):

    [xcache]
    xcache.shm_scheme =        "mmap"
    xcache.size  =                0M
    xcache.count =                 1
    xcache.slots =                8K
    xcache.ttl   =                 0
    xcache.gc_interval =           0
    xcache.var_size  =            0M
    xcache.var_count =             1
    xcache.var_slots =            8K
    xcache.var_ttl   =             0
    xcache.var_maxttl   =          0
    xcache.var_gc_interval =     300
    xcache.test =                Off
    xcache.readonly_protection = Off
    xcache.mmap_path =    "/dev/zero"
    xcache.coredump_directory =   ""
    xcache.cacher =               On
    xcache.stat   =               On
    xcache.optimizer =           Off
    

    APC I utilized the default options (when you load the extension and look at phpinfo it will show the defaults)

  8. Kaloyan K. Tsvetkov permalink

    Yeah, I see what you mean. Anyway, the ZF itself relies on the include_path to include the class inside it, so for ZF-based applications we can not rule out the include_path at 100%. Except if they do change the approach and start including the files with a full path prefix ;)

    Several months ago I stumbled upon a site with video tutorials on ZF, and in them they were encouraging in a way the usage of Zend_Loader::loadClass(), so if there are other people that used something similar as a reference for creating their (first) ZF based projects, they are tend to use this approach of loading files too. Probably some kind of discussion should be initiated on best practices with ZF (and PHP in general) – different approaches have advantages and disadvatages depending on the environment (shared hosting or not, multiple servers or not, etc) and the scope of the project. For example the pre-compilation of all the files with apc_compile_file() might be OK for a project like Facebook, but I don’t see how anyone can do it for a WordPress or a Drupal on a shared hosting ;)

  9. @Mike, it is interesting that XCache comes with that config by default. You may want to read http://xcache.lighttpd.net/wiki/XcacheIni … as it indicates that having xcache.size set to 0M is the same as having xcache.cacher = Off.

    I only harp on this because we use XCache under very heavy load and find it to be stellar. It does require a bit of reading up on the settings though to get it to where it’ll do anything for you.

  10. @Kaloyan
    I’ve created several projects using the Zend Framework, mostly before it was version 1.0 and then since then I have a few projects that I have slowly been working on in free time (what’s free time?!). I had seen some of those tutorials that were using it and also telling people to use it, which was part of my reasoning for looking into what the actual handling was going to affect performance wise. I am thinking about creating an extended version of it, such that I can easily make a loader that simply uses a defined path and then just quickly explodes and joins without any checks for the file.

    Example:

    class Pmt_Loader::loadClass($className) {
    	$fileName = explode('_', $className);
    	include_once (implode('/', $fileName) . '.php');
    }

    Now that is obviously an extremely simplistic version that could be done but would certainly skip any checks.

    I completely agree about the pre-compilation of all the files. Also best practices seem to be never followed in many different environments which make for so many applications to have to work around them (register_globals and magic_quotes I am looking at you!).

  11. @Clay
    I will keep that in mind for the next testing round which I am hoping to do later on tonight. I am going to attempt to put the configurations as close as possible to each other based on APC’s defaults since that seemed to work great right out of the box. XCache should probably tweak the configuration as in the install from source didn’t really reference anything about changing those values (I did the Quick Installation).

  12. Kaloyan K. Tsvetkov permalink

    @Clay: Do you have any observations on whether XCache has the same issues as the rest of the opcode caching solutions when it comes to conditional includes ? I found this forum post (http://forum.lighttpd.net/topic/864) and I am not quite sure if what they are are referring to as “possible early/late binding issues” is the same as conditional includes

Trackbacks & Pingbacks

  1. Accelerators Revisited - Mike Willbanks : getting into the mind of a php developer.

Leave a Reply

Note: XHTML is allowed. Your email address will never be published.

Subscribe to this comment feed via RSS