Magento is not the fastest platform as we now. As a result, e-commerce store owners are often asking to improve the websites speed, especially if it is large and full of extensions. Sometimes, it’s confusing to check what part of the page rendering is slowing down the things without using the side services, Magento profiler or even with them. This article describes the code of simple extension which measures the time of each frontend block rendering without significant use of the system resources. Additionally, you can find some useful tips for Magento extension development itself reading information below.
Our extension is called “Blockspeed” and it will use the “Atwix” namespace, but you can surely use your own. First of all, let’s tell Magento about our extension using a config file.
magento_root_dir/app/etc/modules/Atwix_Blockspeed.xml:
<?xml version="1.0"?> <config> <modules> <Atwix_Blockspeed> <active>true</active> <codePool>local</codePool> </Atwix_Blockspeed> </modules> </config>
That was the simplest thing, and next step is our extension config file. We’ll provide the whole xml and will explain the separate snippets when required.
magento_root_dir/app/code/local/Atwix/Blockspeed/etc/config.xml:
<?xml version="1.0"?> <config> <modules> <Atwix_Blockspeed> <version>1.0.0</version> </Atwix_Blockspeed> </modules> <global> <blocks> <atwix_blockspeed> <class>Atwix_Blockspeed_Block</class> </atwix_blockspeed> </blocks> <models> <atwix_blockspeed> <class>Atwix_Blockspeed_Model</class> <resourceModel>atwix_blockspeed_resource</resourceModel> </atwix_blockspeed> <atwix_blockspeed_resource> <class>Atwix_Blockspeed_Model_Resource</class> <deprecatedNode>atwix_blockspeed_eav_mysql4</deprecatedNode> <entities> <rendertime> <table>atwix_blockspeed_rendertime</table> </rendertime> </entities> </atwix_blockspeed_resource> </models> <helpers> <atwix_blockspeed> <class>Atwix_Blockspeed_Helper</class> </atwix_blockspeed> </helpers> <resources> <atwix_blockspeed_setup> <setup> <module>Atwix_Blockspeed</module> </setup> </atwix_blockspeed_setup> </resources> </global> <frontend> <events> <core_block_abstract_to_html_before> <observers> <atwix_blockspeed> <type>singleton</type> <class>atwix_blockspeed/observer</class> <method>blockHtmlStart</method> </atwix_blockspeed> </observers> </core_block_abstract_to_html_before> <core_block_abstract_to_html_after> <observers> <atwix_blockspeed> <type>singleton</type> <class>atwix_blockspeed/observer</class> <method>blockHtmlEnd</method> </atwix_blockspeed> </observers> </core_block_abstract_to_html_after> <controller_action_postdispatch> <observers> <atwix_blockspeed> <type>singleton</type> <class>atwix_blockspeed/observer</class> <method>controllerEnd</method> </atwix_blockspeed> </observers> </controller_action_postdispatch> </events> </frontend> <adminhtml> <acl> <resources> <all> <title>Allow Everything</title> </all> <admin> <children> <system> <children> <config> <children> <atwix_blockspeed> <title>Atwix Block Speed</title> </atwix_blockspeed> </children> </config> </children> </system> </children> </admin> </resources> </acl> </adminhtml> <admin> <routers> <adminhtml> <args> <modules> <blockspeed before="Mage_Adminhtml">Atwix_Blockspeed_Adminhtml</blockspeed> </modules> </args> </adminhtml> </routers> </admin> </config>
The ability to enable and disable our extension via the admin panel is useful as it will also decrease the performance. So, let’s create the settings panel with a single “yes/no” option in it.
magento_root_dir/app/code/local/Atwix/Blockspeed/etc/system.xml :
<?xml version="1.0"?> <config> <tabs> <atwix translate="label"> <label>Atwix Extensions</label> <sort_order>150</sort_order> </atwix> </tabs> <sections> <atwix_blockspeed translate="label" module="atwix_blockspeed"> <class>separator-top</class> <label>Atwix Block Speed</label> <tab>atwix</tab> <sort_order>100</sort_order> <show_in_default>1</show_in_default> <show_in_website>1</show_in_website> <show_in_store>1</show_in_store> <groups> <atwix_blockspeed translate="label comment"> <label>Atwix Block Speed Options</label> <frontend_type>text</frontend_type> <sort_order>1</sort_order> <show_in_default>1</show_in_default> <show_in_website>1</show_in_website> <show_in_store>1</show_in_store> <fields> <enabled translate="label"> <label>Enabled</label> <comment> <![CDATA[<span class="notice">Can decrease performance when enabled</span>]]> </comment> <frontend_type>select</frontend_type> <source_model>adminhtml/system_config_source_yesno</source_model> <sort_order>100</sort_order> <show_in_default>1</show_in_default> <show_in_website>1</show_in_website> <show_in_store>1</show_in_store> </enabled> </fields> </atwix_blockspeed> </groups> </atwix_blockspeed> </sections> </config>
Note that any admin section of a single extension can not run if this extension has no helper. We will also use helper as a temporary information storage about the block speed rendering since each helper instance is created only once per page load, and that is exactly what we need. Using registry or session for that purpose is more inconvenient.
magento_root_dir/app/code/local/Atwix/Blockspeed/Helper/Data.php:
<?php class Atwix_Blockspeed_Helper_Data extends Mage_Core_Helper_Data { public $_blocks = array(); }
The helpers were declared in our config.xml with:
<helpers> <atwix_blockspeed> <class>Atwix_Blockspeed_Helper</class> </atwix_blockspeed> </helpers>
Furthermore, the heart of our extension is an observer – that is catching every Magento block “toHtml” method call with “core_block_abstract_to_html_before” and “core_block_abstract_to_html_after” events. It will save the results after the page controller finishes its operations with the “controller_action_postdispatch” event. It will look like this in config.xml:
<events> <core_block_abstract_to_html_before> <observers> <atwix_blockspeed> <type>singleton</type> <class>atwix_blockspeed/observer</class> <method>blockHtmlStart</method> </atwix_blockspeed> </observers> </core_block_abstract_to_html_before> <core_block_abstract_to_html_after> <observers> <atwix_blockspeed> <type>singleton</type> <class>atwix_blockspeed/observer</class> <method>blockHtmlEnd</method> </atwix_blockspeed> </observers> </core_block_abstract_to_html_after> <controller_action_postdispatch> <observers> <atwix_blockspeed> <type>singleton</type> <class>atwix_blockspeed/observer</class> <method>controllerEnd</method> </atwix_blockspeed> </observers> </controller_action_postdispatch> </events>
The observer itself will have three methods to corresponding events described above.
magento_root_dir/app/code/local/Atwix/Blockspeed/Model/Observer.php:
<?php class Atwix_Blockspeed_Model_Observer { /** * Write block rendering starting time to helper instance * * @param Varien_Event_Observer $observer */ public function blockHtmlStart(Varien_Event_Observer $observer) { if( Mage::getStoreConfig('atwix_blockspeed/atwix_blockspeed/enabled') == 1 ) { Mage::helper('atwix_blockspeed')->_blocks[get_class($observer->getBlock()) . '---' . $observer->getBlock()->getTemplate()][$observer->getBlock()->getNameInLayout()]['start'] = microtime(true); } } /** * Write block rendering ending time to helper instance * * @param Varien_Event_Observer $observer */ public function blockHtmlEnd(Varien_Event_Observer $observer) { if( Mage::getStoreConfig('atwix_blockspeed/atwix_blockspeed/enabled') == 1 ) { Mage::helper('atwix_blockspeed')->_blocks[get_class($observer->getBlock()) . '---' . $observer->getBlock()->getTemplate()][$observer->getBlock()->getNameInLayout()]['end'] = microtime(true); } } /** * Refresh block rendering statistics * * @param Varien_Event_Observer $observer */ public function controllerEnd(Varien_Event_Observer $observer) { if( Mage::getStoreConfig('atwix_blockspeed/atwix_blockspeed/enabled') == 1 ) { Mage::getModel('atwix_blockspeed/rendertime')->updateStatistics(); } } }
Also, you should pay attention to the following three things here:
- Every observer method has a config check on its start.
- We used block class name and template file name as an identifier with the “- – -” separator. Since there can be multiple block instances, we used the ”name_in_layout” param.
- We are calling the custom model to save the results and haven’t declared it yet.
So, let’s prepare Magento and our extension for that model (model declaration is also required for our observer). The “models” node in config.xml is responsible for that:
<models> <atwix_blockspeed> <class>Atwix_Blockspeed_Model</class> <resourceModel>atwix_blockspeed_resource</resourceModel> </atwix_blockspeed> <atwix_blockspeed_resource> <class>Atwix_Blockspeed_Model_Resource</class> <deprecatedNode>atwix_blockspeed_eav_mysql4</deprecatedNode> <entities> <rendertime> <table>atwix_blockspeed_rendertime</table> </rendertime> </entities> </atwix_blockspeed_resource> </models>
The fact is that we will also need the install script to create the table for our model – an appropriate config.xml snippet:
<resources> <atwix_blockspeed_setup> <setup> <module>Atwix_Blockspeed</module> </setup> </atwix_blockspeed_setup> </resources>
In this case, we will use Varien methods for our install script instead of the direct SQL queries.
magento_root_dir/app/code/local/Atwix/Blockspeed/sql/atwix_blockspeed_setup/install-1.0.0.php :
<?php $this->startSetup(); $table = $this->getConnection() ->newTable($this->getTable('atwix_blockspeed/rendertime')) ->addColumn('entity_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array( 'identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true, ), 'Entity ID') ->addColumn('block', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( 'nullable' => false, 'default' => '', ), 'Block name') ->addColumn('template', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array( 'nullable' => false, 'default' => '', ), 'Template name') ->addColumn('time', Varien_Db_Ddl_Table::TYPE_FLOAT, null, array( 'unsigned' => true, 'nullable' => false, ), 'Average rendering time') ->addColumn('hits', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array( 'unsigned' => true, 'nullable' => true, 'default' => '0', ), 'Total hits') ; $this->getConnection()->createTable($table); $this->endSetup();
Our model will require a resource model and collection. The collection is standard and the resource model has a “flush” method to truncate our block rendering speed statistics table.
magento_root_dir/app/code/local/Atwix/Blockspeed/Model/Resource/Rendertime.php :
<?php class Atwix_Blockspeed_Model_Resource_Rendertime extends Mage_Core_Model_Resource_Db_Abstract { protected function _construct() { $this->_init('atwix_blockspeed/rendertime', 'entity_id'); } public function flush() { try{ $this->_getWriteAdapter()->query('TRUNCATE TABLE '.$this->getMainTable()); Mage::getSingleton('adminhtml/session')->addSuccess('Block statistics was successfully flushed'); } catch (Exception $e) { Mage::getSingleton('adminhtml/session')->addError($e->getMessage()); } } }
magento_root_dir/app/code/local/Atwix/Blockspeed/Model/Resource/Rendertime/Collection.php :
<?php class Atwix_Blockspeed_Model_Resource_Rendertime_Collection extends Mage_Core_Model_Resource_Db_Collection_Abstract { protected function _construct() { $this->_init('atwix_blockspeed/rendertime', 'entity_id'); } }
We think that everything is ready for the model itself. It will have an “updateStatistics” method to calculate stored in the helper timestamps in milliseconds rendering time and count an average rendering time for each block-template combination and store it.
magento_root_dir/app/code/local/Atwix/Blockspeed/Model/Rendertime.php :
<?php class Atwix_Blockspeed_Model_Rendertime extends Mage_Core_Model_Abstract { public function _construct() { $this->_init('atwix_blockspeed/rendertime'); } /** * Update block rendering statistics */ public function updateStatistics() { $blocks = Mage::helper('atwix_blockspeed')->_blocks; $collection = $this->getCollection(); //updating existing values if( $collection->count() > 0 ) { $items = $collection->getItems(); foreach($items as $item) { $name = $item->getBlock() . '---' . $item->getTemplate(); if(array_key_exists($name, $blocks)) { $hits = $item->getHits() + count( $blocks[$name] ); $totalTime = $item->getTime() * $item->getHits(); foreach($blocks[$name] as $value) { if( isset($value['end']) && isset($value['start']) ){ $totalTime += $value['end'] - $value['start']; }else{ $hits += -1; } } $avgTime = $totalTime / $hits; $item->setTime($avgTime) ->setHits($hits) ->save() ; unset($blocks[$name]); } } } //adding new values foreach( $blocks as $title => $values ) { $hits = count( $values ); $totalTime = 0; foreach($values as $value) { if( isset($value['end']) && isset($value['start']) ){ $totalTime += $value['end'] - $value['start']; }else{ $hits += -1; } } if($hits > 0){ $titles = explode('---',$title); $avgTime = $totalTime / $hits; $data = array( 'block' => $titles[0], 'template' => $titles[1], 'time' => $avgTime, 'hits' => $hits, ); $this->setData($data)->save(); } } } }
That is all we need to get and store information about the block rendering speed. Also, we can see that information through the DBMS GUI’s of your choice. But, we think it would be nice to have this information in Magento admin panel as well. So, providing that functionality will not take a lot of time – we need to declare a router in config.xml:
<admin> <routers> <adminhtml> <args> <modules> <blockspeed before="Mage_Adminhtml">Atwix_Blockspeed_Adminhtml</blockspeed> </modules> </args> </adminhtml> </routers> </admin>
Let’s place Magento admin panel menu item for our extension to “System”.
magento_root_dir/app/code/local/Atwix/Blockspeed/etc/adminhtml.xml:
<?xml version="1.0"?> <config> <menu> <system> <children> <atwix_blockspeed translate="title" module="atwix_blockspeed"> <title>Atwix Blockspeed</title> <sort_order>55</sort_order> <action>adminhtml/blockspeed/index</action> </atwix_blockspeed> </children> </system> </menu> <acl> <resources> <all> <title>Allow Everything</title> </all> <admin> <children> <system> <atwix_blockspeed> <atwix_blockspeed> <title>Atwix Blockspeed</title> </atwix_blockspeed> </atwix_blockspeed> </system> </children> </admin> </resources> </acl> </config>
Our controller will require only three actions: “index” action for viewing the statistics, “export” action for storing result in CSV somewhere else and “flush” action if you will need to start measuring from the scratch. Also, note that we’re creating content right in the controller to avoid additional layout creation.
magento_root_dir/app/code/local/Atwix/Blockspeed/controllers/Adminhtml/BlockspeedController.php:
<?php class Atwix_Blockspeed_Adminhtml_BlockspeedController extends Mage_Adminhtml_Controller_Action { public function indexAction() { $this->loadLayout(); $this->_setActiveMenu('system/atwix_blockspeed'); $this->_addContent($this->getLayout()->createBlock('atwix_blockspeed/adminhtml_statistics')); $this->renderLayout(); } public function flushAction() { Mage::getResourceModel('atwix_blockspeed/rendertime')->flush(); $this->_redirect('adminhtml/blockspeed'); } public function exportCsvAction() { $fileName = 'block_statistics.csv'; $content = $this->getLayout()->createBlock('atwix_blockspeed/adminhtml_statistics_grid') ->getCsvFile(); $this->_prepareDownloadResponse($fileName, $content); } }
The grid container block is pretty simple, it contains right block naming for grid, “flush” button and the “add” button removal to avoid manual records creation.
magento_root_dir/app/code/local/Atwix/Blockspeed/Block/Adminhtml/Statistics.php:
<?php class Atwix_Blockspeed_Block_Adminhtml_Statistics extends Mage_Adminhtml_Block_Widget_Grid_Container { public function __construct() { $this->_controller = 'adminhtml_statistics'; $this->_blockGroup = 'atwix_blockspeed'; $this->_headerText = Mage::helper('atwix_blockspeed')->__('Block rendering time statistics'); $this->_addButton('module_controller', array( 'label' => $this->__('Flush statistics'), 'onclick' => "confirmSetLocation('This will flush ALL statistics. Are you sure?','{$this->getUrl('*/*/flush')}')", )); parent::__construct(); $this->removeButton('add'); } }
Moreover, the grid block itself is pretty common with its columns declaration.
magento_root_dir/app/code/local/Atwix/Blockspeed/Block/Adminhtml/Statistics/Grid.php:
<?php class Atwix_Blockspeed_Block_Adminhtml_Statistics_Grid extends Mage_Adminhtml_Block_Widget_Grid { public function __construct() { parent::__construct(); $this->setId('blockspeedStatisticsGrid'); $this->setDefaultSort('block'); $this->setDefaultDir('ASC'); } protected function _prepareCollection() { $collection = Mage::getModel('atwix_blockspeed/rendertime')->getCollection(); $this->setCollection($collection); return parent::_prepareCollection(); } protected function _prepareColumns() { $helper = Mage::helper('atwix_blockspeed'); $this->addColumn('entity_id', array( 'header' => $helper->__('ID'), 'align' => 'right', 'width' => '50px', 'type' => 'number', 'index' => 'entity_id', )); $this->addColumn('block', array( 'header' => $helper->__('Block class name'), 'index' => 'block', )); $this->addColumn('template', array( 'header' => $helper->__('Block template name'), 'index' => 'template', )); $this->addColumn('time', array( 'header' => $helper->__('Average rendering time'), 'index' => 'time', 'align' => 'right', 'type' => 'number', 'width' => '50px', 'renderer' => 'Atwix_Blockspeed_Block_Adminhtml_Statistics_Renderer_Seconds', )); $this->addColumn('hits', array( 'header' => $helper->__('Number of hits'), 'index' => 'hits', 'align' => 'right', 'type' => 'number', 'width' => '50px', )); $this->addExportType('*/*/exportCsv', Mage::helper('atwix_blockspeed')->__('CSV')); return parent::_prepareColumns(); } }
The only thing in the grid you may spotted is a renderer, that provides rendering time in the seconds with 2 decimals.
magento_root_dir/app/code/local/Atwix/Blockspeed/Block/Adminhtml/Statistics/Renderer/Seconds.php:
<?php class Atwix_Blockspeed_Block_Adminhtml_Statistics_Renderer_Seconds extends Mage_Adminhtml_Block_Widget_Grid_Column_Renderer_Abstract { /** * Return time in seconds with 2 decimals * * @param Varien_Object $row * @return string */ public function render(Varien_Object $row) { return round($row->getData('time'), 2) . ' s'; } }
The block declaration in config.xml :
<blocks> <atwix_blockspeed> <class>Atwix_Blockspeed_Block</class> </atwix_blockspeed> </blocks>
That’s all, you have the complete extension for the block rendering speed measurement. You can find a repo with extension here. And as always, thanks for reading our blog!