PHP 7.4 で xhprof/xhgui プロファイリング

だいぶ前に xhgui 使ったときは、アプリ側にも xhgui のソースを入れて xhgui/external/header.php みたいなファイルを auto_prepend_file とかに設定していたと思うのですが、最新版だとだいぶ変わっていました。

xhprof

とりあえず tideways_xhprof 拡張 が必要です。Docker なら次のような感じでインストールできます。

RUN mkdir -p /tmp/tideways_xhprof &&\
    curl -fsSL https://github.com/tideways/php-xhprof-extension/archive/v5.0.2.tar.gz |\
        tar xzf - --strip-components=1 -C /tmp/tideways_xhprof &&\
    docker-php-ext-install /tmp/tideways_xhprof &&\
    rm -fr /tmp/tideways_xhprof

と思ったら xhprof 拡張も PHP 7 対応でメンテ続いていたんですね。こっちでも良いかも。

RUN apk add --no-cache --virtual .build-deps autoconf gcc g++ make &&\
    pecl install xhprof &&\
    apk del .build-deps &&\
    rm -fr /tmp/pear &&\
    docker-php-ext-enable xhprof

php-profiler

アプリ側には xhgui は必要無く、代わりに perftools/php-profiler が必要です。プロファイル結果をアプリ側から MongoDB に直接保存しようとすると xhgui-collector も必要です。

php-profiler の設定の save.handlerSAVER_UPLOAD を指定すればプロファイル結果を HTTP で xhgui へポストするようになるので楽ちんです。アプリ側に mongodb 拡張も必要ありません。

アプリケーションの index.php とか composer.json の autoload.files とかあるいは auto_prepend_file とかで次のようにプロファイラを開始します。

<?php
$config = [
    'profiler.enable' => function () {
        return true;
    },

    // xhprof や tideways_xhprof で有効にするフラグ
    'profiler.flags' => [
        // 実行時間以外に収集するメトリクス
        \Xhgui\Profiler\ProfilingFlags::CPU,
        \Xhgui\Profiler\ProfilingFlags::MEMORY,

        // ビルトイン関数をプロファイル結果煮含めない
        \Xhgui\Profiler\ProfilingFlags::NO_BUILTINS,

        // xhprof や tideways_xhprof では無意味(tideways 拡張ではサポートされているらしい)
        \Xhgui\Profiler\ProfilingFlags::NO_SPANS,
    ],

    // プロファイル結果の保存ハンドラ
    'save.handler' => \Xhgui\Profiler\Profiler::SAVER_UPLOAD,

    // プロファイル結果のアップロード先
    'save.handler.upload' => [
        // xhgui の URL を指定する
        'uri' => 'http://xhgui/run/import',
    ],
];

$profiler = new \Xhgui\Profiler\Profiler($config);
$profiler->start();

$profiler->start() でプロファイルが開始されつつ、プロファイルを停止して結果を保存するためのシャットダウンハンドラが登録されます。

シャットダウン関数の中で fastcgi_finish_request でリクエストを終了させたうえで保存ハンドラを呼ぶため、保存に時間が掛かったとしてもページの表示が遅延することはありません。ただ、別の用途でシャットダウンハンドラを利用していてシャットダウンハンドラからレスポンスを返している場合、それが機能しなくなります。その場合、$profiler->start(false) のようにプロファイルを開始すればシャットダウン関数で fastcgi_finish_request は実行されなくなります。

xhgui

xhgui は edyan/xhgui がオールインワンの Docker イメージなので楽です。docker-compose ならこれだけです。

version: '3.7'
services:
  xhgui:
    image: edyan/xhgui
    ports:
      - '8142:80'

xhgui/xhgui というイメージもありますが、これは nginx や mongodb を別に用意する必要があります。あとなぜか /var/www/xhgui/config に謎のコンフィグファイルが置かれているため、別にコンフィルファイルを用意してマウントするか、あるいは削除しないと環境変数で設定を指定できません。。。

xhgui: document to insert contains invalid key: keys cannot contain "."

追記 2020-12-16 アップストリームで 0.16 で対応されたものの edyan/xhgui は 0.14.0 なのでまだ必要

xhprof 拡張を使ったときだけ下記の問題で xhgui で mongodb への保存が失敗するようになりました。

tideways_xhprof 拡張では発生しないようです。たまたまかもしれません。

プロファイル結果のオブジェクトキーに "." が含まれていることが原因のようですが・・次のようにシャットダウン関数で修正して保存すればとりあえず大丈夫です。

<?php
$profiler = new \Xhgui\Profiler\Profiler([
    'profiler.enable' => function () {
        return true;
    },

    'profiler.flags' => [
        \Xhgui\Profiler\ProfilingFlags::CPU,
        \Xhgui\Profiler\ProfilingFlags::MEMORY,
        \Xhgui\Profiler\ProfilingFlags::NO_BUILTINS,
        \Xhgui\Profiler\ProfilingFlags::NO_SPANS,
    ],

    'save.handler' => \Xhgui\Profiler\Profiler::SAVER_UPLOAD,

    'save.handler.upload' => [
        'uri' => 'http://xhgui/run/import',
    ],
]);

$profiler->enable();

register_shutdown_function(function () use ($profiler) {
    ignore_user_abort(true);
    session_write_close();
    flush();
    fastcgi_finish_request();

    $data = $profiler->disable();
    $profile = [];
    foreach($data['profile'] as $key => $value) {
        $profile[strtr($key, ['.' => '_'])] = $value;
    }
    $data['profile'] = $profile;
    $profiler->save($data);
});

さいごに

どうやら xhgui に最近になってかなり大きな変更が入っているようです。そのため、まだちょいちょいおかしなところがあるようです。プロファイル結果のストレージに mongodb 以外に PDO も指定できるようになっているようなのですが PDO だとまともに動作しない・・とか。

ただ、以前のアプリ側にも mongodb 拡張が必要、とかと比べるとだいぶ仕込みやすくなっていると思います。