Cyrax_02

Cyrax_02

С нами с 04 августа 2013; Место в рейтинге пользователей: #247
Cyrax_02
07 октября 2014, 15:23
0
Нужно свой парсер написать. То что modx сейчас парсит 0.3 сек, с помощью регулярок можно распарсить за 0.01 сек.
Плюс сразу парсить элементы по мере их «поступления» не нужно. Лучше их распарсить в самом конце после полного разворачивания элементов за один раз. так будет гораздо быстрее.
Cyrax_02
07 октября 2014, 15:19
0
С помощью плагина на OnParseDocument можно было бы маскировать modx-теги внутри чанков и внутри возвращаемых сниппетами html-фрагментов (чтобы эти теги не обрабатывались сразу), а перед конечным прогоном парсера их размаскировать, чтобы все теги были обработаны за раз.
Но при создании плагина (даже пустого) время генерации страницы увеличивается на полсекунды из-за большого числа вызовов этого плагина. Т.е. вариант с OnParseDocument отпадает…
Cyrax_02
07 октября 2014, 13:28
0
Проверил. Всё-таки эти 0.3-0.4 секунды уходят на парсинг html-кода, возвращаемого сниппетом (modParser::processElementTags()). А в этом html-коде много тегов modx (150 штук, в основном, cсылки вида [[~...]]):

public function processElementTags($parentTag, & $content, $processUncacheable= false, $removeUnprocessed= false, $prefix= "[[", $suffix= "]]", $tokens= array (), $depth= 0) {
        $_processingTag = $this->_processingTag;
        $_processingUncacheable = $this->_processingUncacheable;
        $_removingUnprocessed = $this->_removingUnprocessed;
        $this->_processingTag = true;
        $this->_processingUncacheable = (boolean) $processUncacheable;
        $this->_removingUnprocessed = (boolean) $removeUnprocessed;
        $depth = $depth > 0 ? $depth - 1 : 0;
        $processed= 0;
        $tags= array ();
        /* invoke OnParseDocument event */
        $this->modx->documentOutput = $content;
        $this->modx->invokeEvent('OnParseDocument', array('content' => &$content));
        $content = $this->modx->documentOutput;
        unset($this->modx->documentOutput);
        if ($collected= $this->collectElementTags($content, $tags, $prefix, $suffix, $tokens)) {
            $tagMap= array ();
            foreach ($tags as $tag) {
                $token= substr($tag[1], 0, 1);
                if (!$processUncacheable && $token === '!') {
                    if ($removeUnprocessed) {
                        $tagMap[$tag[0]]= '';
                    }
                }
                elseif (!empty ($tokens) && !in_array($token, $tokens)) {
                    $collected--;
                    continue;
                }
                if ($tag[0] === $parentTag) {
                    $tagMap[$tag[0]]= '';
                    $processed++;
                    continue;
                }
                $tagOutput= $this->processTag($tag, $processUncacheable);
                if (($tagOutput === null || $tagOutput === false) && $removeUnprocessed) {
                    $tagMap[$tag[0]]= '';
                    $processed++;
                }
                elseif ($tagOutput !== null && $tagOutput !== false) {
                    $tagMap[$tag[0]]= $tagOutput;
                    if ($tag[0] !== $tagOutput) $processed++;
                }
            }
            $this->mergeTagOutput($tagMap, $content);
            if ($depth > 0) {
                $processed+= $this->processElementTags($parentTag, $content, $processUncacheable, $removeUnprocessed, $prefix, $suffix, $tokens, $depth);
            }
        }

        $this->_removingUnprocessed = $_removingUnprocessed;
        $this->_processingUncacheable = $_processingUncacheable;
        $this->_processingTag = $_processingTag;
        return $processed;
    }

Иногда на парсинг этих 150 тегов уходит 0.10-0.15 сек, иногда 0.05 сек, временами 0.3-0.4 сек.
Cyrax_02
06 октября 2014, 22:32
0
Тьфу. Нужно было не modElement::process() смотреть, а modScript::process()
Cyrax_02
06 октября 2014, 21:55
0
Вот мой тест:
В файле /core/model/modx/modparser.class.php внутрь метода processTag (строка 414) ставим 3 заглушки:

public function processTag($tag, $processUncacheable = true) {
    /* STUB: start time */ if($tag[0] == '[[!snp01]]') { $GLOBALS['time0'] = microtime(true); }
    ...
    if ($elementOutput === null) {
        switch ($token) {
        ...
        default:
	    $tagName= substr($tagName, $tokenOffset);
	    if ($element= $this->getElement('modSnippet', $tagName)) {
                $element->set('name', $tagName);
                $element->setTag($outerTag);
                $element->setCacheable($cacheable);
		/* STUB */ if($tag[0] == '[[!snp01]]') { print_r('[before process] '.number_format(microtime(true) - $GLOBALS['time0'], 4), false); }
                $elementOutput= $element->process($tagPropString);
                /* STUB */ if($tag[0] == '[[!snp01]]') { print_r('[after process] '.number_format(microtime(true) - $GLOBALS['time0'], 4), false); }
	    }
        }
    }
    ...
}

Далее в файле /core/model/modx/modelement.class.php внутрь метода process (строка 263) ставим ещё 2 заглушки:

public function process($properties= null, $content= null) {
    /* STUB */ if($this->_tag == '[[!snp01]]') { print_r('[start process] '.number_format(microtime(true) - $GLOBALS['time0'], 4), false); }
    $this->xpdo->getParser();
    $this->xpdo->parser->setProcessingElement(true);
    $this->getProperties($properties);
    $this->getTag();
    if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Processing Element: " . $this->get('name') . ($this->_tag ? "\nTag: {$this->_tag}" : "\n") . "\nProperties: " . print_r($this->_properties, true));
    if ($this->isCacheable() && isset ($this->xpdo->elementCache[$this->_tag])) {
        $this->_output = $this->xpdo->elementCache[$this->_tag];
        $this->_processed = true;
    } else {
        $this->filterInput();
        $this->getContent(is_string($content) ? array('content' => $content) : array());
    }
    /* STUB */ if($this->_tag == '[[!snp01]]') { print_r('[end process] '.number_format(microtime(true) - $GLOBALS['time0'], 4), false); }
    return $this->_result;  // NOTE: [$this->_result] return true
}

Загружаем страницу и получаем цифры:
[before process] 0.0023
[start process] 0.0024
[end process] 0.0032
[after process] 0.0436

Т.е наблюдается большой скачок времени (0.04 сек) между лапками [end process] и [after process]. Этот промежуток времени соответствует возврату из метода modElement::process() обратно в метод modParser::processTag(), откуда он был вызван.
Временной провал локализован и составляет 0.04. Осталось это чудо объяснить.
Cyrax_02
06 октября 2014, 19:32
0
Вот сейчас снова сниппет парсится 0.3-0.4 сек. при этом код сниппета выполняется стабильно 0.01-0.02 сек.
Явно modx что-там делает усиленно. Причём временами эти его действия занимают много времени (0.3 сек), временами — мало (0.03 сек).
Причём ошибок замеров здесь нет, потому что общее время генерации скрипта, выводимое через тег [^t^], также увеличилось на 0.3 сек. Т.е. время парсинга сниппета (без выполнения кода) на самом деле тормозит.

Попробую поковыряться в исходниках…
Cyrax_02
06 октября 2014, 19:13
0
Получается так:
— время выполнения кода сниппета: 0.01-0.02 сек
— время парсинга сниппета, показанное debugParser: 0.07-0.30 сек
— разница между временем парсинга и временем выполнения кода: 0.05-0.28 сек

Т.е. сегодня в течение 3-4 часов время парсинга менялось в довольно широком диапазоне 0.05-0.30 сек, при этом время выполнения сниппета не менялось и всё время составляло 0.01-0.02 сек.

Но даже если взять минимальное время парсинга (0.04 сек), то всё равно чисто на парсинг (без выполнения) уходит слишком много времени — 0.05 сек. При этом чтение include-файла сниппета core/cache/includes/elements/modsnippet/xxx.include.cache.php выполняется за тысячные доли секунды.
Cyrax_02
06 октября 2014, 18:58
0
Если в плагине синтаксическая ошибка, в логах php об этом должно быть указано.
А «виснуть» админка будет только при вызове этого плагина. Если при загрузке некоторой страницы сайта или админки плагин с синтаксической ошибкой не вызывается, то и проблем никаких не будет.
Cyrax_02
06 октября 2014, 17:36
0
А чтобы разница была ощутимее, сделайте не 3500 строк кода, а 10000 строк кода.
И сравните время парсинга сниппета с этими 10000 строками кода и без них.

А код возьмите полегче, чтобы быстро выполнялся, например:
$var = array_key_exists('ttt', $modx->placeholders) || array_key_exists('yyy', $modx->placeholders);
Cyrax_02
06 октября 2014, 17:30
0
Сниппет с 3500 строками кода парсится (без выполнения): 0.0311 — 0.0076 = 0.0235 сек
Сниппет без кода парсится: 0.0052 сек.

А теперь вопрос: куда подевались 0.02 сек?
Чтение include-файла сниппета с 3500 строками кода занимает тысячные (у Вас — десятитысячные) доли секунды.

Т.е. в случае с 3500 строками кода чисто парсинг (без выполнения) длится на 0.02 сек дольше, чем парсинг пустого (или почти пустого) сниппета.
Cyrax_02
06 октября 2014, 17:21
0
Ну вот. У Вас уже чисто на парсинг уходит 0.02 сек.
Учитывая, что у меня сервер слабый (работает в 20-40 раз медленнее, чем тот сервер, на котором работает Василий), можете умножить Ваши цифры на 10 и получите тот же масштаб цифр (0.3 сек и 0.075 сек соответственно)

А теперь уберите все 3500 строк и посмотрите, сколько будет парситься почти пустой сниппет.
Cyrax_02
06 октября 2014, 17:16
0
Да, действительно. У меня в одном чанке остался вызов [[+landing]], тогда как simpleSearch я уже давно как снёс (руки всё никак не доходили):
community.modx-cms.ru/blog/questions/199388.html#comment77053

Но на скорость загрузки страницы это никак не повлияло (разве что на 0.05 сек, не более). И тем более, никак не повлияло на сабжевую проблему.
Cyrax_02
06 октября 2014, 16:45
0
<?php
$time0 = microtime(true);
... // код сниппета: в $output набирается html-код
print_r(number_format(microtime(true) - $time0, 4).' сек');
return $output;

Наберите в сниппете любой левый код на 3000 строк (который выполняется быстро) и запустите debugParser. Получите приличное время для этого сниппета.
Cyrax_02
06 октября 2014, 15:49
0
Можно было бы предположить, что причина в объёме html-кода, который возвращает этот сниппет (объём немалый) — этот возвращаемый html-код modx может долго парсить.

Но если в сниппете snp01 весь код оставить, но вместо
return $output;  // возвращается большой объём html-кода
поставить
return '000';
то время парсинга сниппета нисколько не уменьшается — также 0.3 сек

Следовательно, 0.3 сек уходит НЕ на парсинг возвращаемого сниппептом html-кода.

P.S. Т.е. время парсинга некэшируемого сниппета приблизительно пропорционально текстовому объёму кода этого сниппета и не зависит от возвращаемого сниппетом значения. И коэффициент пропорциональности имеет приличную величину.
Cyrax_02
06 октября 2014, 15:26
0
В сниппет snp01 поместил минимальный код:
return '000';
В итоге время парсинга сниппета уменьшилось с 0.3 сек до 0.002 сек

Как такое может быть? Напрашивается вывод, что modx очень тщательно парсит код сниппета. Но чего там парсить? Ведь в сниппетах нет modx-тегов — только php-код и всё.
Единственной разницей во времени парсинга должна быть разница между чтением с диска маленького файла и большого — а это тысячные и сотые доли секунды соответственно. Де факто получаем разницу в 0.3 сек.
Cyrax_02
03 октября 2014, 20:45
0
В идеале компонент не мешает «научить» выполнять рефакторинг кода.
В частности, переименовывать элементы сейчас — жуть. Вручную по всем шаблонам, чанкам, сниппетам и ctrl+F. А можно было бы за секунду.
Cyrax_02
30 сентября 2014, 14:43
1
+1
Поскольку reflection позволяет извне получить доступ к защищённым/закрытым полям объекта, то через reflection сабжевая задача решается гораздо проще:

abstract class xPDOQueryMemento {
    static $reflectionProperties;
    
    static protected function getReflectionProperties() {
        global $modx;
        
        if(!isset(xPDOQueryMemento::$reflectionProperties)) {
            $reflection = new ReflectionClass('xPDOQuery_'.$modx->config['dbtype']);
            
            $properties = array();
            foreach($reflection->getProperties() as $property) {
                $properties[$property->getName()] = $property;
            }
            xPDOQueryMemento::$reflectionProperties = $properties;
        }
        return xPDOQueryMemento::$reflectionProperties;
    }

    // memento pattern save
    static public function save($query) {
        global $modx;
        if(is_null($query)) { return ''; }
        
        $data = array();
        $properties = xPDOQueryMemento::getReflectionProperties();  // $reflection = new ReflectionObject($query);
        
        foreach($properties as $property) {
            $property->setAccessible(true);
            
            $value = $property->getValue($query);
            if(!is_object($value) || is_null($value)) {
                $data[$property->getName()] = $value;
            }
            if (!$property->isPublic()) { $property->setAccessible(false); }
        }
        return serialize($data);
    }
    
    // memento pattern restore
    static public function restore($data) {
        global $modx;
        if(($data == '') || is_null($data)) { return null; }
        
        $query = $modx->newQuery('modResource');
        $properties = xPDOQueryMemento::getReflectionProperties();  // $reflection = new ReflectionObject($query);
        
        foreach(unserialize($data) as $name => $value) {
            $property = $properties[$name];
            
            $property->setAccessible(true);
            $property->setValue($query, $value);
            if (!$property->isPublic()) { $property->setAccessible(false); }
        }
        return $query;
    }
}

Далее в коде делаем так (кэшируем состояние объекта xPDOQuery):
$key = md5('Ключ кэша с состоянием объекта xPDOQuery');
$query = xPDOQueryMemento::restore($modx->cacheManager->get($key));
if(is_null($query)) {
    $query = $modx->newQuery('modResource');
    ... // Сложные операции с query (подключение TV, условия и пр.)
    cacheManager::cacheSet($key, xPDOQueryMemento::save($query));
}
... // Часто меняющиеся операции с query. Например, указание LIMIT/OFFSET при пагинации

Тест (1 TV и 2 простых условия):
$time0 = microtime(true);
$key = md5('key125');
$query = xPDOQueryMemento::restore($modx->cacheManager->get($key));
if(is_null($query)) {
    $query = $modx->newQuery('modResource');
    $query->leftJoin('modTemplateVarResource', 'tv', array('tv.contentid = modResource.id', 'tv.tmplvarid = 1239');
    $query->where(array('modResource.id:=' => 1000, 'tv.value:>' => 25), xPDOQuery::SQL_OR);
    
    cacheManager::cacheSet($key, xPDOQueryMemento::save($query));
}
print_r(number_format(microtime(true) - $time0, 4).' сек');

При первом выполнении (запрос строится с нуля): 0.005-0.007 сек
При последующих выполнениях (объект xPDOQuery восстанавливается из файлового кэша): 0.0007 сек (из них 0.0005 сек — чтение кэш-файла с состоянием xPDOQuery)
Разница: в 7-10 раз. И это только на простом запросе (1 tv и 2 условия).

Мой запрос с 30 tv-ками и кучей сложных условий готовится (без выполнения) 0.1 сек.
С кэшированием запроса вплоть до пагинации (до добавления limit/offset) — 0.0007-0.0010 сек
Разница: в 70-100 раз.

P.S. Кэширование запроса xPDOQuery совместно с его выполнением в режиме pdo может придать ему реактивные свойства. И переписывать запрос на чистый pdo не потребуется.
Cyrax_02
28 сентября 2014, 02:50
0
Имеем такую цепочку наследования:
xPDOQuery_mysql extends xPDOQuery extends xPDOCriteria

Самый простой вариант — засериализовать все поля объекта xPDOQuery_mySQL. Проблема в том, что открытыми являются только поля класса xPDOCriteria:
class xPDOCriteria {
    public $sql= '';
    public $stmt= null;
    public $bindings= array ();
    public $cacheFlag= false;
...
Собственные же поля класса xPDOQuery являются защищёнными.
Соответственно, единственным способом вручную засериализовать поля объекта xPDOQuery (xPDOQuery_mysql) остаётся наследование. Что-то вроде такого:
class xPDOQueryManager_mysql extends xPDOQuery_mysql {
    
    public function __construct(& $xpdo, $class, $criteria= null) {
        parent::__construct($xpdo, $class, $criteria);
    }
    // реализация паттерна memento
    public function save() { ... }
    public function restore() { ... }    
}
Но в этом случае также потребуется заставить метод xpdo->newQuery вместо xPDOQuery_mysql создавать объект класса xPDOQueryManager_mysql. Опять-таки заставить можно, только перегрузив класс modx и метод newQuery. И это не всё. Нужно ещё заставить modx в процессе инициализации вместо modx использовать перегруженный класс modxManager. Но это уже попахивает извращением.

В голову лезет ещё вариант с reflection. Может, к утру что-нибудь придумаю…
Cyrax_02
26 августа 2014, 18:46
0
Есть текущие (актуальные) данные и закэшированные данные. Третьего не дано. Если мы работаем с кэшируемыми данными (кэшируемые ресурсы, кэшируемые элементы), то при загрузке веб-страниц отображаются закэшированные данные. Если мы работает с НЕкэшируемыми данными, то при загрузке веб-страниц должны отображаться всегда актуальные данные.

В остальное время есть кеш и/или готовая к инклюду функция.

Отделяем мух от котлет:
Если по замыслу разработчиков include-код имеет отношение к кэшу элемента (т.е. является кэшем include-кода), то этот кэш include-кода вообще НЕ должен использоваться при вызове НЕкэшируемого элемента.
Если же по замыслу разработчиков include-код является include-вариантом текущего кода элемента, то он всегда должен поддерживаться в актуальном состоянии (т.е. обновляться при обновлении исходного кода элемента).

Сейчас же мы наблюдаем смешанную логику:
В ходе обработки НЕкэшируемого элемента парсером modx этот файл выступает в качестве include-варианта текущего кода элемента. А при изменении исходного кода сниппета — в качестве кэша include-кода.
Такого быть не должно. Логика должна быть однозначной — либо это кэш-side, либо не кэш-side.

Предвосхищая вопросы «Зачем это нужно»
Например, сниппет будет возвращать другое значение (не true|false, а количество элементов). А чанки с вызовом сниппета я не могу менять (прав нет), этим занят фронтенд-разработчик. А я изменил код сниппета. Завтра второй разработчик доделает чанки и обновит кеш, и все будет хорошо.

Ну ведь очевидно, что разработчики modx реализовали именно такой вариант работы парсера (не eval'ом из БД, а include'ом из внешнего файла) не для того, чтобы синхронизировать раздельную работу разработчиков.
Ваш пример всего лишь показывает, как с пользой можно использовать эту «особенность» modx.

Специально, чтобы не было нарушения целостности и стоит галочка «Обновлять кеш при сохранении».

Всё-таки цель этой галки несколько иная. Не обеспечение целостности данных (целостность данных должна обеспечиваться всегда, везде и в безусловном порядке, а если нужны варианты данных на разные моменты времени, здесь уже вводим категории снимков данных, кэша, версий и пр.), а актуализация кэша (в силу реализации новые кэш-данные физически будут созданы при первом вызове, но смысл именно такой).