Fi1osof 14 июля 2013 1 20


Собственно говоря, сейчас еще можно увидеть hamster-fox.ru на чистом minishop + Revolution. Обязательно покликайте новинки, разделы и т.п. (отдельные страницы до 10 МИНУТ выполняются). Грустно все… Если у кого сомнения по поводу того, что это не вина минишопа и т.п., лучше просто оставьте при себе свое мнение:-) Модифицированный getResources — не лучший инструмент для многоуровневой выборки из 5000 документов с TV-шками и т.п.

А вот это немного измененный магазин: hamster-fox-ru.fi1osof.modxcloud.com/
На самом деле почти ничего и не сделано, просто использованы процессоры shopModx и связка phpTemplates+modxSmarty. Каталог не из кеша, полностью на лету, даже еще и постраничность прикрутил (на старом сайте от нее отказались еще год назад, так как VDS просто умирает). А этот как вы видите, на modxcloud.com крутится.

Обновил все сегодня за день. То есть сам магазин, корзина и т.п. — это все на minishop осталось, а каталог через shopModx работает.

Но результатом все равно не доволен. 0,5-2 сек на страницу — много. Могу точно сказать, что система изначально не удачно разработана.

UPD: Про корень зла и примеры кодов.
Почему так тормозит минишоп?
Основная причина в том, что в минишопе используется для выборки товаров модифицированный getResources. А всем известно, что данный компонент просто не создан для того. чтобы делать выборки из большого количества ресурсов, особенно когда много родителей и уровней вложенности больше, чем один. Он проходится рекурсивно по всем уровням и собирает ID-шники всех родительских документов. При чем для всех выборок использует getCollection(). Но в данном магазине это вообще жесть, так как разделов очень много (284, если быть точным). В итоге вот такой запрос складывается: gist.github.com/Fi1osof/c3aafeb797c20f370b73 (и это еще только часть запроса). А если попробовать перейти в раздел Новинки, к примеру, то еще и поиск по TV-полю включается, и в таком случае был зафиксирован рекорд — 10 минут выполнение на VDS (сейчас этот раздел вообще где-то в конце третьей минуты разваливается критической ошибкой нехватки ресурсов и времени на выполнение). Плюс мне вообще не понятно зачем все завязано на шаблонах? Если у нас есть жесткая связь с ModGoods (innerJoin), и эта связь — только для товаров, то зачем еще поиск по шаблону вести? Это утяжеляет запрос.

В общем, для решения этой проблемы я и использовал модифицированный getData-процессор из shopModx. Вот конечный код:
<?php

/*
 * Получаем данные каталога
 */

if($this instanceof Modxsite){
    $modxsite = & $this;
}
else{
    $modxsite = & $this->modxsite;
}
$modxsite->loadProcessor('web.getdata', 'shopmodx');

class modWebCatalogGetdataProcessor extends ShopmodxWebGetDataProcessor{
    public $defaultSortField = 'good.id';
    public $defaultSortDirection = 'DESC';


    public function initialize() {
        $this->setDefaultProperties(array(
            'limit' => 12,
        ));
        if(!empty($_REQUEST['page']) AND $page = (int)$_REQUEST['page'] AND $page > 1 AND $this->getProperty('limit', 0)){
            $this->setProperty('start', ($page-1) * $this->getProperty('limit'));
        }
        return parent::initialize();
    }
    
    public function prepareQueryBeforeCount(xPDOQuery $c) {
        $c = parent::prepareQueryBeforeCount($c);
        $c->innerJoin('ModGoods', 'good', "good.gid={$this->classKey}.id");
        return $c;
    }
    
    protected function prepareCountQuery(xPDOQuery &$query) {
        $query = parent::prepareCountQuery($query);
        
        $type = $this->getProperty('type', 'all');
        if($type != 'all'){
            switch($type){
                // Новинки
                case 'novelty':
                    $query->innerJoin('modTemplateVarResource', 'novelty', 
                            "novelty.contentid={$this->classKey}.id AND novelty.tmplvarid=11 AND novelty.value='1'");
                    break;
                // Хиты продаж
                case 'top':
                    $query->innerJoin('modTemplateVarResource', 'top', 
                            "top.contentid={$this->classKey}.id AND top.tmplvarid=6 AND top.value='1'");
                    break;
                // Скоро в продаже
                case 'soon':
                    $query->innerJoin('modTemplateVarResource', 'soon', 
                            "soon.contentid={$this->classKey}.id AND soon.tmplvarid=12 AND soon.value='1'");
                    break;
                default:;
            }
        }
        
        $query->where(array(
            'published' => 1,
            'deleted'   => 0,
            'hidemenu'   => 0,
        ));
        
        return $query;
    }
    
    public function setSelection(xPDOQuery $c) {
        $c = parent::setSelection($c);
        $c->select(array(
            'good.*',
        ));
        return $c;
    }
    
    public function outputArray(array $array, $count = false) {
        $this->modx->setPlaceholder('total', $count);
        $this->modx->runSnippet('getPage@getPage', array(
            'limit' => $this->getProperty('limit'),
        ));
        return parent::outputArray($array, $count);
    }
}

return 'modWebCatalogGetdataProcessor';

То есть здесь и выборка товаров, и сортировка, и постраничность, и условия поиска новинок, топов и т.п. Как видите, код совсем не большой. При чем в родительский процессор можно вообще не лезть. Просто знайте, что здесь будет массив данных товаров вместе со всеми TV-шками. При чем это через чистые PDO-запросы без всяких лишних пакетов и т.п.

А вот расширяющий процессор, который делает выборки товаров только в категории и подкатегориях:
<?php

/*
 * Получаем данные каталога
 */

require_once dirname(dirname(__FILE__)).'/getdata.class.php';

class modWebCatalogCategoryGetdataProcessor extends modWebCatalogGetdataProcessor{
    protected $sectionsIDs = array();   // Разделы
    
    public function beforeQuery() {
        $can = parent::beforeQuery();
        if($can !== true){
            return $can;
        }
        
        $this->getSectionsIDs($this->getSectionsCondition());
        if(!$this->sectionsIDs){
            return "Не были получены разделы";
        }
        return true;
    }

    protected function getSectionsCondition(){
        return array(
            'id' => $this->modx->resource->get('id'),
        );
    }
    
    // Получаем ID-шники разделов
    protected function getSectionsIDs($where){
        if(!$where){
            return;
        }
        $query = $this->modx->newQuery('modResource');
        $query->select(array(
            "DISTINCT {$this->classKey}.id",     
        ));
        $query->where(array(
            'deleted'   => 0,
            'published' => 1,
            'isfolder'  => 1,
            'hidemenu'  => 0,
            'template'  => 2,
        ));
        $query->where($where);
        
        if($query->prepare() && $query->stmt->execute() && $rows = $query->stmt->fetchAll(PDO::FETCH_ASSOC)){
            $result = array();
            foreach($rows as $row){
                $result[] = $row['id'];
            }
            $this->sectionsIDs = array_unique(array_merge($this->sectionsIDs, $result));
            return $this->getSectionsIDs(array(
                "parent:IN" => $result,
            ));
        } 
        return;
    }

    public function prepareCountQuery(xPDOQuery &$query) {
        $query = parent::prepareCountQuery($query);
        $query->where(array(
            "{$this->classKey}.parent:IN" => $this->sectionsIDs,
        ));
        return $query;
    }     
}

return 'modWebCatalogCategoryGetdataProcessor';


Далее результат набиваем сами, как хотим, хоть в чанки, хоть еще куда-нибудь. Я в смарти набиваю. Кстати, есть с чем сравнить. Вот чанк, который использовался раньше:
<ins class="row show-grid">
  
  <div class="r [[+tv.novice_good:gt=`0`:then=`novice`]] 
[[+tv.top_buyed:gt=`0`:then=`top_buyed`]] 
[[+remains:equalto=`0`:then=`[[+tv.expected_qty:gt=`0`:then=`expected_qty`]]`]]">

        <div class="label [[+tv.novice_good:gt=`0`:then=`novice`]] 
[[+tv.top_buyed:gt=`0`:then=`top_buyed`]] 
[[+remains:equalto=`0`:then=`[[+tv.expected_qty:gt=`0`:then=`expected_qty`]]`]]"></div>

        <div class="picture">
            <img src="[[!If? &subject=`[[+img]]` 
&operator=`!empty` &then=`[[+img:phpthumbof=`w=170`]]` &else=`/assets/hamster/css/images/no_photo.png`]]" />
        </div>

        <div class="info">

        <a href="[[~[[+id]]]]" class="title">[[+pagetitle]]</a>

        <span class="sku">Арт. [[+article]]</span><br />

        <div class="buy"><span class="price">[[+price]] 
<span class="currency">[[+currency:default=`Р`]]</span></span> 
<a href='#' class="addToCartLink" data-gid="[[+id]]">[[+tv.expected_qty:gt=`0`:then=`Заказать`:else=`В корзину`]]
</a>
</div>

        <div class="descr">[[+introtext]]</div>

        </div>

    </div>
  
</ins>


А вот он же, но на Smarty:
<ins class="row show-grid">
    {assign var=block_class value=""}
    
 {if !empty($product.tvs.novice_good.value) && $product.tvs.novice_good.value == 1}
     {assign var=block_class value="{$block_class} novice"}
 {/if} 
 {if !empty($product.tvs.top_buyed.value) && $product.tvs.top_buyed.value == 1}
     {assign var=block_class value="{$block_class} top_buyed"}
 {/if} 
 {if !empty($product.tvs.expected_qty.value) && $product.tvs.expected_qty.value == 1}
     {assign var=block_class value="{$block_class} expected_qty"}
     {assign var=basket_label value="В корзину"}
 {else} 
     {assign var=basket_label value="Заказать"}
 {/if} 
  <div class="r {$block_class}">

        <div class="label {$block_class}"></div>

        <div class="picture">
            <img src="{if !empty($product.img)}{snippet name="phpthumbof" 
params="input=`{$product.img}`&options=`w=170`"}{else}/assets/hamster/css/images/no_photo.png{/if}" />
        </div>

        <div class="info">

        <a href="{link id=$product.object_id}" class="title">{$product.pagetitle}</a>

        <span class="sku">Арт. {$product.article}</span><br />

        <div class="buy"><span class="price">{$product.price} <span class="currency">Р</span></span> 
            <a href='#' class="addToCartLink" data-gid="{$product.object_id}">{$basket_label}</a>
        </div>

        <div class="descr">{$product.introtext}</div>

        </div>

    </div>
  
</ins>

На самом деле почти тоже самое, но с той разницей, что в Смарти это скомпиллированный PHP-шаблон, с полной поддержкой PHP и выполнением всего в одном месте, а в чанке все это — куча MODX-тегов, которые будут парситься MODX-ом, инициироваться куча новых объектов и т.п. Могу точно сказать, что разница в производительности очень существенная.
Вторая проблема — меню каталога
Как я говорил выше, меню каталога очень большое — 284 раздела. И работало это традиционно на Wayfinder. Я удалял из шаблона вообще все, оставлял только один Wayfinder, результат — почти 3 секунды. И это вообще не удивительно. Меню я тоже перевел на процессор, и теперь меню формируется за 0,2-0,3 секунды, и то только потому что в цикле приходится все элементы меню набивать в Smarty-шаблончике. Можно конечно вообще шаблончики эти перенести в сам процессор, чтобы инклюдов не выполнялось, тогда вообще мгновенно будет формироваться меню, но это уже не стал пока заморачиваться, так как это выполняется только при первом заходе на страницу, а дальше это уже просто HTML документа. Еще плюс этого процессора в том, что он не выполняет запросов к БД каждый раз. После полной очистки кеша он один раз набивает все элементы в массив, и кеширует их. А далее он формирует конечное меню уже из этого массива без запросов к БД. Вот код процессора:
<?php

class modWebSidebarMenuIndexProcessor extends modObjectGetListProcessor{
    
    protected $IDs = array();
    
    public function initialize() {
        $this->setDefaultProperties(array(
            'startId'   => $this->modx->getOption('shopmodx.catalog_id', null, 0),
            'depth'     => 3,
            'levelClass' => 'level',
            'outerTpl'  => 'inc/menu/catalog/outer.tpl',
            'rowTpl'    => 'inc/menu/catalog/row.tpl',
            'sortby'    => 'pagetitle',
            'sortdir'   => 'ASC',
        ));
        return parent::initialize();
    }


    public function process() {
        $output = '';
        // get current doc id
        if($pid = $this->modx->resource->parent){
            $this->IDs[] = $this->modx->resource->id;
            while($doc = $this->modx->getObject('modResource', $pid)){
                $this->IDs[] = $doc->id;
                $pid = $doc->parent;
            }
        }
        
        if(!$items = $this->getMenu()){
            return $this->failure('');
        }
        
        $output = $this->fetchMenu($items);
        return $this->success($output);
    }
    
    protected function fetchMenu(array $items, $level=0){
        $level++;
        $outer = '';
        $rows = '';
        $levelClass = $this->getProperty('levelClass');
        foreach($items as $item){
            $this->count++;
            $wraper  = '';
            $cls = array();
            if($levelClass){
                $cls[] = "{$levelClass}{$level}";
            }
            if(in_array($item['id'], $this->IDs)){
                $cls[] = 'active';
            } 
            $item['cls'] = $cls;
            
            if(!empty($item['childs'])){
                $wraper = $this->fetchMenu($item['childs'], $level);
            }
            
            $this->modx->smarty->assign('wraper', $wraper);
            $this->modx->smarty->assign('item', array(
                'link' => $item['uri'],
                'title' => $item['menutitle'] ? $item['menutitle'] : $item['pagetitle'],
                'cls' => implode(" ", $item['cls']),
            ));
            $rows .= $this->modx->smarty->fetch($this->getProperty('rowTpl'));
        }
        $this->modx->smarty->assign('wraper', $rows);
        $output = $this->modx->smarty->fetch($this->getProperty('outerTpl'));
        return $output;
    }
    
    
    public function getMenu(){
        $key = "{$modx->context->key}/catalog_menu";
        if(!$items = $this->modx->cacheManager->get($key)){
            $startId = $this->getProperty('startId', 0);
            $depth = $this->getProperty('depth', 1);

            if($items = $this->_getMenu($startId, $depth)){
                $this->modx->cacheManager->set($key, $items);
            }
        }
        return $items;
    }
      


    protected function _getMenu($id, $depth){
        $depth--;
        $items = array();
        $q = $this->modx->newQuery('modResource', array(
            'parent' => $id,
            'deleted' => 0,
            'published' => 1,
            'hidemenu' => 0,
            'template'  => 2,
        ));
        $q->select(array(
            'id', 'parent', 'uri', 'alias', 'pagetitle', 'menutitle',
        ));
        if($sortby = $this->getProperty('sortby')){
            $q->sortby($sortby, $this->getProperty('sortdir', 'ASC'));
        }
        if($q->prepare() && $q->stmt->execute()){
            while($row = $q->stmt->fetch(PDO::FETCH_ASSOC)){
                $row['childs'] = array();
                if($depth>0){
                    $row['childs'] = $this->_getMenu($row['id'], $depth);
                }
                $items[$row['id']] = $row;
            }
        }
        return $items;
    }
}
return 'modWebSidebarMenuIndexProcessor';

Выполняю его в Smarty так:
{processor ns=modxsite action="web/sidebar/menu/index" assign=menu}
{$menu.message}


Только надо учитывать, что этот массив не учитывает права доступов к документам, так что если у вас есть какие-то приватные разделы в каталоге, то он в чистом виде не годится, придется подправлять. Хотя если разделов не много, то само собой и WF достаточно.
Заключение
Вот, собственно, и вся оптимизация. Но здесь есть еще к чему стремиться, и самое главное — это надо сделать оптимизацию базы данных. Многие пытаются выполнить оптимизацию кода MODX-а, но забывают, что на уровне запросов единственное что можно и нужно оптимизировать — это база данных. На производительность сложных запросов очень сильно влияют первичные и вторичные ключи. Вот у нас здесь выборка из трех таблиц идет (документы, товары, TV-шки), и их надо между собой связать с настройкой вторичных ключей. Подробно об этом я писал здесь.

UPD 2: подробный кейс: modxclub.ru/blog/modx-club-portfolio/152.html
20 комментариев
den991
den99 12 июля 2013г в 22:58 #
верни статью на этот сайт
Fi1osof1
Fi1osof 13 июля 2013г в 01:33 #
ОК. Завтра сделаю.
Fi1osof1
Fi1osof 14 июля 2013г в 15:53 #
Вернули :-)
Fi1osof1
Fi1osof 14 июля 2013г в 16:14 #
Получилось еще сократить время загрузки главной страницы с одной секунды до 0.4-0.6
Оказывается, забыли про phpthumbof, который постоянно читает директорию кеша. В настройках выставил phpthumb_cache_maxage, phpthumb_cache_maxfiles и phpthumb_cache_maxsize в ноль, и стразу все зашуршало.
den991
den99 15 июля 2013г в 05:19 #
Я эти три параметра уже изнасиловал, но так ничего и не ускорилось. Насилую дальше.
den991
den99 15 июля 2013г в 12:21 #
для справедливости должен сообщить, что у меня был включенным xdebug, который я успешно забыл. ну и тормоза конечно. рука-лицо.
Fi1osof1
Fi1osof 15 июля 2013г в 13:55 #
эти параметры насиловать не надо. Если значения не нулевые, то будет выполняться подсчет значений. А это зверское чтение файловой системы.
den991
den99 15 июля 2013г в 22:00 #
Я уже где-то читал и понял о нулях, но ничего не выходило. Спасибо за напоминание.

И вот как я сделал в итоге: поставил нули в трех параметрах совершенно без надежды, отключил xdebug (сам по себе уже был отключен, но я вырубил модуль полностью), включил memcached на 1 гиг для общего кеша сайта и просто для понта, удалил вручную кеш modx, ребутнул Apache2, ребутнул memcached — запустил…

Ускорение где-то в пределах от 1,5 до 6 раз. (диапазон ранее был 1-3,5 секунды для тяжелой морды и для залогиненного юзера теперь снизился до 0.68-0,8 при нулевом изменении кода сайта при равных условиях нагрузки).

А для анонимусов там вообще другая заварка.

Fi1osof1
Fi1osof 15 июля 2013г в 23:13 #
:-)
d
dem 20 июля 2013г в 21:55 #
Опишите, пожалуйста, параметры хостинга и версию минишопа в оригинале на сайте.
Fi1osof1
Fi1osof 20 июля 2013г в 22:08 #
Минишоп 1.6.2-rc
Параметры хостинга запросил, но пока ответа нет. Как только будет (если будет), отпишу. Знаю только что это как минимум VPS, а не простой шаредхостинг, и память и т.п. ему поднимали, ибо нет мочи.

Но для сравнения скажу, что на другом моем выделенном облачном сервере, где было 16 ядер и 1,5-2 гига памяти, оригинальный сайт отрабатывал в среднем за 8-10 секунд. А уже с изменениями сайт отрабатывал за эти 0,5-1 сек. К сожалению, с отключенной проверкой phpthumbOf я не успел проверить, сейчас этого облака уже нет. Да и скоро самого Galaxy и не останется.
d
dem 20 июля 2013г в 22:11 #
Спасибо, интересно читать подробные кейсы, понимаешь ограничения и места возможного искривления рук в процессе разработки)
Fi1osof1
Fi1osof 20 июля 2013г в 22:23 #
Конкретно в данном случае на хостинг вообще не получается грешить. Здесь проблема именно из-за использования модифицированного getResources. Посмотрите формируемый запрос, и если хоть какой-то опыт с MySQL имеете, то поймете, что он весьма не слабый: gist.github.com/Fi1osof/c3aafeb797c20f370b73
Но проблема и не столько в самом запросе, сколько в процессе формирования этого запроса. Много циклов, много запросов к БД и т.п.
d
dem 20 июля 2013г в 22:38 #
Хороший инструмент для убийства, так понимаю модификация сниппета была значительная?
Fi1osof1
Fi1osof 20 июля 2013г в 22:45 #
Нет, сниппет совершенно не трогали. Здесь в принципе не использовались механизмы минишопа для выборки товаров из каталога. Здесь использовались list-процессоры из shopModx-а. А в остальном все работает на самом минишопе (точнее должно работать, так как где-то при переносе на облако что-то поломалось, скорее всего из-за коротких <?, которые не воспринимаются modxcloud-ом. Но мне пока не до этих мелочей. Задача стояла только в оптимизации вывода каталога).
То есть минишоп справлялся со всеми задачами на сайте, кроме выборки товаров из этого не маленького каталога.
d
dem 20 июля 2013г в 23:10 #
Спасибо за развернутый ответ.
Fi1osof1
Fi1osof 20 июля 2013г в 23:23 #
Пожалуйста
Fi1osof1
Fi1osof 24 июля 2013г в 12:29 #
Сегодня основательно перелопатил боевой сайт. Очень подробный топик здесь: modxclub.ru/blog/modx-club-portfolio/152.html
b
blackomb 16 марта 2014г в 14:00 #
Добрый день!
Насколько сложно мигрировать магазин с minishop 1 версии на shopmodx? Нет никакой универсальной последовательности действий? Замучался я с минишопом уже.
Fi1osof1
Fi1osof 16 марта 2014г в 14:09 #
Добрый день!
Я не могу дать универсальный рецепт по переносу, так как каждый проект на MODX-е все-таки выполняется индивидуально, и 10 сайтов на минишопе могут быть в итоге абсолютно по разному разработаны. Тем не менее мы уже довольно много магазинов перенесли как с минишопа, так и с шопкипера (да и просто с самописок), так что задача вполне решаемая. Киньте в личку ссылку на свой магазин, а так же вкратце напишите про самые сложные модули и задачи, которые там были введены или хотелось бы ввести, но не получается. Я тогда оценю сложность или не сложность переезда в целом.
Авторизуйтесь или зарегистрируйтесь (можно через соцсети ), чтобы оставлять комментарии.