Оптимизация PHP: профилирование как основа

Раздел: Разработка на 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]

Первая колонка – время от начала, вторая – потребление памяти в байтах, затем сама функция и файл с номером строки.

Этот режим подходит для поиска неожиданных вызовов функций или проверки последовательности, но даёт меньше статистики по каждому вызову, чем профилирование.

Профилирование PHP-кода - comments

En
Php profiling (php)