Оптимизация PHP: профилирование как основа
Профилирование PHP кода: поиск узких мест
Наиболее эффективным решением для профилирования PHP кода является использование расширения Xdebug в сочетании с визуализатором QCacheGrind (KCacheGrind). Этот подход позволяет получить детальную информацию о времени выполнения каждой функции, количестве вызовов и потреблении памяти без внесения изменений в сам код.
Настройка Xdebug для профилирования
; php.ini
zend_extension = xdebug.so
xdebug.mode = profile
xdebug.output_dir = /tmp/profiling
xdebug.start_with_request = default
; По желанию можно ограничить профилирование по cookie или параметру:
; xdebug.start_with_request = trigger
; xdebug.trigger_value = PROFILE
Как включить профилирование только для конкретного запроса, чтобы не замедлять все страницы?
Установите xdebug.start_with_request = trigger. После этого профилирование начнётся, если в запросе присутствует GET/POST параметр XDEBUG_PROFILE или cookie с именем XDEBUG_PROFILE. Значение cookie/параметра должно совпадать с xdebug.trigger_value (по умолчанию пусто – триггер срабатывает при любом значении).
Генерация и анализ cachegrind файлов
# Выполняем обычный HTTP запрос с триггером:
curl -X GET 'http://example.com/page?XDEBUG_PROFILE=1' > /dev/null
# В /tmp/profiling появится файл cachegrind.out.XXXXX
Откройте этот файл в QCacheGrind (Windows/Linux) или KCacheGrind (Linux). Программа покажет дерево вызовов, время выполнения каждой функции (собственное и с дочерними), количество вызовов и графическое представление.
Типичная ошибка: после включения профилирования сайт работает медленно, объём файлов профилирования слишком велик.
Решение: никогда не включайте xdebug.mode = profile на production сервере постоянно. Используйте триггерный механизм или профилируйте только на staging/локальной среде. Также можно исключить определённые пути с помощью xdebug.profiler_ignore_paths.
Другая проблема: кеширование opcache может искажать результаты (первый запрос медленнее).
Решение: выполняйте запрос дважды, анализируйте второй. Либо прогрейте кеш перед профилированием.
Как измерить время выполнения участков кода без внешних расширений?
Используйте встроенную функцию microtime(true) для ручного профилирования. Создайте простой класс таймера:
class SimpleProfiler {
private static array $marks = [];
public static function start(string $label): void {
self::$marks[$label] = microtime(true);
}
public static function end(string $label): void {
if (!isset(self::$marks[$label])) return;
$elapsed = (microtime(true) - self::$marks[$label]) * 1000;
echo "{$label}: {$elapsed} ms\n";
unset(self::$marks[$label]);
}
}
// Использование
SimpleProfiler::start('database_query');
// выполнение запроса
SimpleProfiler::end('database_query');
Метод подходит для быстрого поиска узких мест, но не даёт информации о вложенных вызовах и требует ручного добавления меток в код.
Как профилировать PHP на production сервере с минимальным оверхедом?
Используйте расширение XHProf (или его форк Tideways). XHProf значительно легче Xdebug и предназначен для использования в production. Установка:
pecl install xhprof
# или для современных PHP (например, с использованием php-xhprof из PECL)
Пример запуска профилирования с сохранением результата в файл:
<?php
xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY);
// весь ваш код
$xhprof_data = xhprof_disable();
include_once '/path/to/xhprof_lib/utils/xhprof_lib.php';
include_once '/path/to/xhprof_lib/utils/xhprof_runs.php';
$runs = new XHProfRuns_Default();
$run_id = $runs->save_run($xhprof_data, 'my_app');
echo "Profile ID: {$run_id}\n";
?>
Результаты можно просмотреть через встроенный веб-интерфейс XHProf (скрипт xhprof_html/index.php).
Проблема: XHProf не обновлялся долгое время, могут быть сложности с установкой на PHP 8+.
Решение: используйте форк Tideways (pecl install tideways) или Blackfire (рекомендуется для современных проектов).
Как профилировать PHP с помощью облачного сервиса и наглядной визуализацией?
Blackfire (blackfire.io) предлагает готовое решение с низким оверхедом, детальными отчётами и интеграцией с CI/CD. Установка агента и клиента:
# Установка Blackfire на сервер (пример для Ubuntu)
curl -s https://packages.blackfire.io/gpg.key | sudo apt-key add -
echo "deb http://packages.blackfire.io/debian any main" | sudo tee /etc/apt/sources.list.d/blackfire.list
sudo apt-get update
sudo apt-get install blackfire-agent blackfire-php
# Запуск агента
sudo blackfire-agent --register --server-id=ваш-id --server-token=ваш-токен
# Профилирование запроса из командной строки
blackfire curl http://example.com/page
Результат сразу открывается в веб-интерфейсе Blackfire с flame-графиками, рекомендациями по оптимизации и сравнением с предыдущими прогонами.
Ошибка: Blackfire требует регистрации и может быть платным для больших команд.
Решение: для небольших проектов достаточно бесплатного тарифа с ограничениями. Альтернатива – Tidweays + XHGui (самостоятельный хостинг).
Расширенные примеры профилирования
Пример 1. Детальный анализ cachegrind файла
После профилирования Xdebug файл cachegrind.out содержит записи в формате:
# /tmp/profiling/cachegrind.out.12345
version: 1
creator: xdebug 3.2.0
cmd: /var/www/index.php
part: 1
events: Time
fl=php:internal
fn=php::count
5 20
fl=/var/www/app/helpers.php
fn=App\Helpers\formatPrice
10 200
1 30
Расшифровка: для каждой функции указывается количество вызовов и затраченное время (в микросекундах). Первая строка после fn= – это число вызовов, вторая – общее время (включая дочерние).
Пример визуализации в QCacheGrind:
- Self Cost – время, затраченное непосредственно в теле функции (без учёта вызовов).
- Inclusive Cost – общее время с учетом всех вложенных вызовов.
- Calls Count – количество вызовов функции.
Самые узкие места обычно находятся в функциях с высоким Self Cost и большим количеством вызовов (например, многократные повторные запросы к БД).
Пример 2. Ручное профилирование с микроразметкой и выводом в лог
function profile(string $label, callable $fn): mixed {
$start = microtime(true);
$result = $fn();
$elapsed = (microtime(true) - $start) * 1000;
error_log("PROFILER: {$label} = {$elapsed} ms");
return $result;
}
// Использование
$data = profile('fetch_users', function () use ($db) {
return $db->query('SELECT * FROM users WHERE active = 1')->fetchAll();
});
Результат в логе:
[2025-03-25 12:34:56] PROFILER: fetch_users = 12.3456 ms
Пример 3. XHProf с сохранением в БД и построением графиков
Установите XHGui (веб-интерфейс для XHProf/Tideways):
git clone https://github.com/perftools/xhgui.git
cd xhgui
composer install
cp config/default.php config/config.php
# настройте подключение к MongoDB (XHGui использует MongoDB для хранения)
Включите профилирование в точке входа (например, в index.php):
<?php
// Autoload
require 'path/to/xhgui/vendor/autoload.php';
use XHGui\Profiler\Profiler;
use XHGui\Saver\MongoSaver;
$saver = new MongoSaver('mongodb://localhost:27017/xhgui');
$profiler = new Profiler($saver);
$profiler->start();
// … ваш код …
$profiler->stop();
?>
Перейдите в браузере на http://your-xhgui-install/ для просмотра собранных профилей. Можно сортировать по времени, памяти, количеству вызовов.
Пример 4. Blackfire с автоматической профилировкой тестов в CI
Используйте Blackfire в GitLab CI:
# .gitlab-ci.yml
blackfire:
stage: test
script:
- blackfire run php vendor/bin/phpunit
- blackfire --output blackfire_report.json run php -r 'echo "done";'
artifacts:
paths:
- blackfire_report.json
Отчеты можно сохранять и сравнивать между коммитами, выявляя регрессии производительности.
Пример 5. Использование Xdebug без QCacheGrind – анализ через текстовый вывод
Xdebug может генерировать не только cachegrind, но и человекочитаемый трейс (xdebug.mode = trace). Режим trace покажет последовательность вызовов функций с временными метками:
xdebug.mode = trace
xdebug.trace_output_name = trace.%p
xdebug.trace_output_dir = /tmp
Пример содержимого файла trace.12345.xt:
TRACE START [2025-03-25 12:34:56.789]
0.0010 123456 -> {main}() /var/www/index.php:0
0.0015 124000 -> require_once() /var/www/index.php:10
0.0020 125000 -> App\Router::route() /var/www/router.php:25
0.0050 130000 -> App\Controller::index() /var/www/router.php:30
0.0080 135000 -> App\Model::fetchData() /var/www/Controller.php:50
0.0120 140000 -> PDO::query() /var/www/Model.php:30
TRACE END [2025-03-25 12:34:56.812]
Первая колонка – время от начала, вторая – потребление памяти в байтах, затем сама функция и файл с номером строки.
Этот режим подходит для поиска неожиданных вызовов функций или проверки последовательности, но даёт меньше статистики по каждому вызову, чем профилирование.