?
Собственно говоря, сейчас еще можно увидеть 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
верни статью на этот сайт
ОК. Завтра сделаю.
Вернули :-)
Получилось еще сократить время загрузки главной страницы с одной секунды до 0.4-0.6
Оказывается, забыли про phpthumbof, который постоянно читает директорию кеша. В настройках выставил phpthumb_cache_maxage, phpthumb_cache_maxfiles и phpthumb_cache_maxsize в ноль, и стразу все зашуршало.
Я эти три параметра уже изнасиловал, но так ничего и не ускорилось. Насилую дальше.
для справедливости должен сообщить, что у меня был включенным xdebug, который я успешно забыл. ну и тормоза конечно. рука-лицо.
эти параметры насиловать не надо. Если значения не нулевые, то будет выполняться подсчет значений. А это зверское чтение файловой системы.
Я уже где-то читал и понял о нулях, но ничего не выходило. Спасибо за напоминание.
И вот как я сделал в итоге: поставил нули в трех параметрах совершенно без надежды, отключил xdebug (сам по себе уже был отключен, но я вырубил модуль полностью), включил memcached на 1 гиг для общего кеша сайта и просто для понта, удалил вручную кеш modx, ребутнул Apache2, ребутнул memcached — запустил…
Ускорение где-то в пределах от 1,5 до 6 раз. (диапазон ранее был 1-3,5 секунды для тяжелой морды и для залогиненного юзера теперь снизился до 0.68-0,8 при нулевом изменении кода сайта при равных условиях нагрузки).
А для анонимусов там вообще другая заварка.
?
Опишите, пожалуйста, параметры хостинга и версию минишопа в оригинале на сайте.
Минишоп 1.6.2-rc
Параметры хостинга запросил, но пока ответа нет. Как только будет (если будет), отпишу. Знаю только что это как минимум VPS, а не простой шаредхостинг, и память и т.п. ему поднимали, ибо нет мочи.
Но для сравнения скажу, что на другом моем выделенном облачном сервере, где было 16 ядер и 1,5-2 гига памяти, оригинальный сайт отрабатывал в среднем за 8-10 секунд. А уже с изменениями сайт отрабатывал за эти 0,5-1 сек. К сожалению, с отключенной проверкой phpthumbOf я не успел проверить, сейчас этого облака уже нет. Да и скоро самого Galaxy и не останется.
Спасибо, интересно читать подробные кейсы, понимаешь ограничения и места возможного искривления рук в процессе разработки)
Конкретно в данном случае на хостинг вообще не получается грешить. Здесь проблема именно из-за использования модифицированного getResources. Посмотрите формируемый запрос, и если хоть какой-то опыт с MySQL имеете, то поймете, что он весьма не слабый: gist.github.com/Fi1osof/c3aafeb797c20f370b73
Но проблема и не столько в самом запросе, сколько в процессе формирования этого запроса. Много циклов, много запросов к БД и т.п.
Хороший инструмент для убийства, так понимаю модификация сниппета была значительная?
Нет, сниппет совершенно не трогали. Здесь в принципе не использовались механизмы минишопа для выборки товаров из каталога. Здесь использовались list-процессоры из shopModx-а. А в остальном все работает на самом минишопе (точнее должно работать, так как где-то при переносе на облако что-то поломалось, скорее всего из-за коротких <?, которые не воспринимаются modxcloud-ом. Но мне пока не до этих мелочей. Задача стояла только в оптимизации вывода каталога).
То есть минишоп справлялся со всеми задачами на сайте, кроме выборки товаров из этого не маленького каталога.
Спасибо за развернутый ответ.
Пожалуйста
Сегодня основательно перелопатил боевой сайт. Очень подробный топик здесь: modxclub.ru/blog/modx-club-portfolio/152.html
Добрый день!
Насколько сложно мигрировать магазин с minishop 1 версии на shopmodx? Нет никакой универсальной последовательности действий? Замучался я с минишопом уже.
Добрый день!
Я не могу дать универсальный рецепт по переносу, так как каждый проект на MODX-е все-таки выполняется индивидуально, и 10 сайтов на минишопе могут быть в итоге абсолютно по разному разработаны. Тем не менее мы уже довольно много магазинов перенесли как с минишопа, так и с шопкипера (да и просто с самописок), так что задача вполне решаемая. Киньте в личку ссылку на свой магазин, а так же вкратце напишите про самые сложные модули и задачи, которые там были введены или хотелось бы ввести, но не получается. Я тогда оценю сложность или не сложность переезда в целом.