Куда девается память?
Я попытался разобраться с тем, куда уходит память в целеполагании и прикинуть варианты, что тут можно сделать.
Для профилирования я использовал xdebug, просмотр дампов делался через qcachegrind074-x86.
Исходная проблема:
Открытие главной страницы целеполагания (других страниц — в меньшей степени) может закончиться ошибкой нехватки памяти. После нескольких обновлений, страница, как правило, загружается. Глобальный лимит памяти, выделяемой процессу, составляет 128M.
Данные для анализа:
1) Каждая цель выводится через TargetWidget. Внутри виджета собирается всевозможная информация (запросами к БД через ActiveQuery), форматируется, подключаются ассеты. Количество информации и уровень её анализа зависит от множества факторов (состояние цели, её история и т.д), и, в целом это ОЧЕНЬ затратно как по времени исполнения, так и по памяти.
2) Для экономии ресурсов, TargetWidget обёрнут в кеширование (через наследование от CachedWidget). Таким образом, виджет рассчитывается при первом обращении и сохраняется в кеше (изменение связанной цели вызывает сброс соответствующей записи).
3) Главная страница целеполагания представляет собой произвольный GridView, в ячейки которого выводятся сгенерированные виджеты целей. Количество одновременно отображаемых виджетов зависит от текущих пользовательсикх настроек и фильтров, но ничем не ограничено.
4) В Yii может быть включён внутренний отладчик (включается при определении YII_ENV_DEV).
Первичные гипотезы для проверки:
?) Кривой механизм кеширования: виджеты каждый раз пересчитываются заново.
?) Включение внутреннего отладчика Yii добавляет серьёзный оверхед по памяти.
Для проверки откроем одну и ту же страницу (главную целеполагания) с одними и теми же настройками (вывод 50 строк максимум, все колонки включены) но разными конфигурациями среды и проанализируем профили выполнения.
Конфигурации будут следующие:
а) Кеширование Yii отключено ($config['cache']['class'] => DummyCache::class), внутренний отладчик Yii отключён.
б) Кеширование Yii включено ($config['cache']['class'] => FileCache::class), кеш не прогрет, внутренний отладчик Yii отключён.
в) Кеширование Yii включено ($config['cache']['class'] => FileCache::class), кеш прогрет, внутренний отладчик Yii отключён.
г) Кеширование Yii включено ($config['cache']['class'] => FileCache::class), кеш прогрет, внутренний отладчик Yii включён.
Смотрим:
Вариант а, TMC 276Mb.
Наиболее "жирный" вызов — обращение к "кешу", роль которого выполняет DummyCache. В "кеше" нужных данных, естественно, нет, и виджеты просчитываются заново, на что уходит 85 мегабайт суммарно. На втором месте (64 мегабайта) — вызов PDOStatement->execute, условно — весь скоп взаимодействия с БД (запросы, ответы, буферизация, связанные данные). На третьем месте (45 мегабайт) — ob_start => включение отдельного буфера под вывод рендеринга любого виджета (не только TargetWidget, но каждого потомка Widget::class).
Далее следуют также довольно "жирные" вызовы, связанные с построением самого грида, остальные вызовы уже копеечные.
Вариант б, TMC 204Mb.
За счёт включения кеширования часть вызовов уже не выполняется. Речь не только о вызовах, целеполагания непосредственно: кешироваться начинают справочники, некоторые подзапросы и т.д. За счёт этого почти вдвое (до 38 мегабайт) падает стоимость обращения к БД. Часть виджетов не пересчитывается, это видно по десятимегабайтной экономии в вызовах
php::call_user_func:{C:\LIFE\openserver\domains\pmo\vendor\yiisoft\yii2\caching\Cache.php:597}
и семимегабайтной экономии на вызовах ob_start. При этом стоимость генерации колонок грида также падает до 31 мегабайта (против 36 в предыдущем варианте), что также может быть объяснено попаданием в кеш.
Вариант в, TMC 92Mb.
С прогретым кешем ререндеринг виджетов и обращения к БД практически не выполняются, все данные, очевидно, получены в готовом виде из кеша. Самая затратная по памяти операция — хранение данных виджетов в буфере (23 мегабайта).
Этот эксперимент исключает гипотезу о кривом кешировании.
Вариант г, TMC 130Mb.
Измерения в целом аналогичны предыдущему эксперименту, за исключением появления вызова debug_backtrace, занимающего суммарно 22 мегабайта памяти. Можно заметить слегка выросшую стоимость spl_autoload_call (как раз за счёт подключения класса отладчика).
Гипотеза о оверхеде отладчика подтверждается.
Файлы профилей для самостоятельного анализа: https://cloud.mail.ru/public/2d5U/RWGHFZo2s
Выводы:
- Виджеты целей — "жадные" в расчётах, но с этим ничего поделать нельзя, по крайней мере — пока.
- Кеширование нивелирует эту жадность. Если кеш будет набираться заблаговременно, то не должно быть проблем даже при первом вызове страницы (реализовано в коммите a9db28ff6ca59b9c4f99f9b6d2fd69d2af6f4e56).
- Забытая на боевом сервере отладка добавляет оверхед, которого может оказаться достаточно для исчерпания лимита памяти.
- Вывод данных из виджетов в Yii дорогостоящий сам по себе. Большое количество "дешёвых" в генерации виджетов всё равно может потребовать большого количества памяти (видимо, для буферизации, для дальнейшего изучения вопроса нужно изучать код фреймворка). Т.е. мы упираемся в лимит памяти, необходимый для непосредственного вывода данных.
Варианты решения:
- На бою не использовать отладчик.
- Убрать ограничение по памяти процесса (или, хотя бы, увеличить его).
- Запретить вывод большого количества элементов TargetWidget => фактически, лимитировать количество строк в GridView стандартными 20.
- Ещё как-то упростить вывод. Например, по умолчанию выводить только название цели, а сопутствующую информацию (статусы/состояния/связи) — во всплывающем блоке, получаемом в фоне.
- Как-то добавить в GridView динамическую подгрузку данных по мере прокрутки.