Performance fixes for Magento bundled product views

I have always had sort of a love/hate relation ship with Magento.  Its overly complex web of controllers, models and blocks and then throw in a bunch of events, always seemed a bit over done.  But there is that aspect of extensibility and flexibility that is extremely valuable.  I will have to give its creators (Yoav Kutner for one) a bit of credit for the flexibility as, you can really extend it to do what your business needs.  Its just sometimes a bit of a tangled web to get there.

Recently a client of ours was seeing page load time crawl up to around 10 seconds for the loading of bundle product pages.  This came after upgrading to magneto 1.7 and upgrading a few extensions.

So we did a bit of analysis and noticed a few things:

  • Many of the extensions were tapped into event observers such as “DISPATCH EVENT:catalog_product_is_salable_after” and where firing many many times more than the entire number of products in the bundles (around 3 times)
  • Seriously a huge amount of sql queriers are run. I am not joking but over 1,000 queries were run on a single product page.  I am amazed the page even loaded in 10 seconds.
  • The product and the product options were loading at least 2 times on the page (we are still working through a good fix for this)
  • The observers were being tapped into by 3 or 4 extensions, and really getting called a lot (Dispatch Event: is called 2876 times and Observer: 4000 times)

So this is a good case of object oriented and event programing gone  a bit wrong.  In many cases Magento uses either getSingleton or the Mage::registry to persist data throughout an request. Often time the geters and setters (if you can really call them that in Magento) would be responsible for getting an object property if it was not set and then persisting it.

In this case the class Mage_Catalog_Model_Product method isSalabe() (also aliased with isSaleable()) as shown below, does not in any way persist its answer to the question. That is each and every time it is called, you fire 2 events and then return the answer (again and again)

../app/code/core/Mage/Catalog/Model/Product.php
public function isSalable()

{
Mage::dispatchEvent(‘catalog_product_is_salable_before’, array(‘product’ => $this));
$salable = $this->isAvailable();

$object = new Varien_Object(array(
‘product’ => $this,
‘is_salable’ => $salable
));
Mage::dispatchEvent(‘catalog_product_is_salable_after’, array(
‘product’ => $this,
‘salable’ => $object
));
return $object->getIsSalable();
}

Here is how we modified it (of course by moving the code to the local pool)

../app/code/local/Mage/Catalog/Model/Product.php
public function isSalable()
{
//function modified to store answers in the magento registry
if (is_null(Mage::registry('isSaleable'.$this->getId()))) {

Mage::dispatchEvent(‘catalog_product_is_salable_before’, array(
‘product’ => $this
));

$salable = $this->isAvailable();

$object = new Varien_Object(array(
‘product’ =>; $this,
‘is_salable’ =>; $salable
));
Mage::dispatchEvent(‘catalog_product_is_salable_after’, array(
‘product’ => $this,
‘salable’ => $object
));
Mage::register(‘isSaleable’.$this->getId(), $object->getIsSalable() ,true);
}
return Mage::registry(‘isSaleable’.$this->getId());
}

Now after this and a few other things we are closer to 4 seconds. (one was a hardware upgrade so i would estimate 6 seconds down to 4 for this fix)

Hopefully we will find a few more things that I can post later.