$30 off During Our Annual Pro Sale. View Details »

PHP で PHP のプロファイラをつくろう

sji
April 11, 2022

PHP で PHP のプロファイラをつくろう

PHPerKaigi2022

sji

April 11, 2022
Tweet

More Decks by sji

Other Decks in Programming

Transcript

  1. PHP
    で PHP
    のプロファイラをつくろう
    五十嵐 進士 / sji / sj-i / @sji_ch

    View Slide

  2. 自己紹介
    @sji_ch
    SNS
    上でのアイコンは GitHub
    が自動生成した奴

    View Slide

  3. 生まれも育ちも仙台

    View Slide

  4. PHP
    カンファレンス仙台とかやった

    View Slide

  5. ふつうのサラリーマン


    株式会社インフィニットループ仙台支社所属
    スマホゲーのサーバサイドプログラマ

    View Slide

  6. 二歳児の父
    かわいい
    絵本好き

    View Slide

  7. WEB+DB PRESS
    の現 PHP
    連載担当
    昨年 6
    月から WEB+DB PRESS
    の PHP
    連載
    だいたいわりと真面目な話をしてる

    View Slide

  8. Agenda
    プロファイラとはなにか
    reli (
    旧 php-profiler)
    の紹介
    どんなことができるのか
    内部構造

    View Slide

  9. プロファイラとはなにか

    View Slide

  10. プロファイラってこういうやつ
    性能解析ツール
    実行中のプログラムの挙動を収集
    ボトルネックを見つけるのに使える

    View Slide

  11. ボトルネック
    80:20
    の法則
    ごく一部の処理が実行時間の大部分
    一部を直すだけで大幅に改善しがち
    瓶の首の細さが水の出る速度を決める

    View Slide

  12. 推測するな、計測せよ
    性能に大きく影響するコードはごく一部
    あてずっぽうで直すとハズレが多いということ
    プロファイラのようなツールでの計測が要る

    View Slide

  13. PHP
    にも色々なプロファイラ
    Xdebug
    Xhprof
    Tideways
    Blackfire
    NewRelic
    sample_prof
    phpspy

    View Slide

  14. 俺のプロファイラの話

    View Slide

  15. reliforp/reli-prof
    PHP
    の PHP
    による PHPer
    のためのプロファイラ
    2020
    年頃から半分ギャグで作ってる
    phpspy
    の PHP
    版のパクり
    前は php-profiler
    という名前(後述)

    View Slide

  16. デモ

    View Slide

  17. 最初に rbspy
    があった
    ruby
    で同様のことをやる rbspy
    という rust
    製プログラム
    https://github.com/rbspy/rbspy

    View Slide

  18. パクリで py-spy
    https://github.com/benfred/py-spy

    View Slide

  19. パクリで phpspy
    https://github.com/adsr/phpspy

    View Slide

  20. phpspy
    のパクリで reli
    最初は php-profiler
    という名前だった
    色々あって変わった
    https://github.com/reliforp/reli-prof

    View Slide

  21. ざっくり言うと
    実行中のスクリプトの動作をプロセス外から盗み見る
    計測対象のプログラムは無修正でよい
    拡張を組み込んだりしなくてよい
    計測用コードを仕込んだりしなくてよい

    View Slide

  22. 何に使える?
    スクリプトの性能計測
    手に入れたトレースを何らかの方法で集計すればよい
    トラブルシュート
    「なにか知らんが無応答になった」みたいな時、どこで詰まってるかわかる

    View Slide

  23. プロファイラの方式:
    計測化(instrumentation)

    オーバーヘッドの累積がつらい


    xdebug, xhprof, tideways
    などほとんどの奴
    各関数呼び出しの前後で自動で時間取得
    速く何度も実行される関数に弱い
    計測オーバーヘッドの累積で不正確に
    呼び出し関係の記録は正確

    View Slide

  24. プロファイラの方式:
    サンプリング型
    よくあるプログラムは関数呼び出しの繰り返し


    「今何を実行中か」を大体等間隔でサンプリングして取得
    adsr/phpspy
    、nikic/sample_prof
    など一部の奴
    VM
    の状態を定期的にのぞき見
    速く何度も実行される関数に強い
    呼び出し関係の記録は取りこぼしも
    reli
    はこっち

    View Slide

  25. 子プロセスを起動してトレース
    子プロセスのトレースは権限いらず
    sudo
    なしで使える
    -o でプロファイラの出力だけリダイレクト
    $ ./reli i:trace -- php -r "fgets(STDIN
    0 fgets :-1

    1 :-1



    0 fgets :-1

    1 :-1



    0 fgets :-1

    1 :-1

    View Slide

  26. 他プロセスへアタッチ
    権限があれば PID
    で任意プロセスへアタッチ可能
    実行プロセスに CAP_SYS_PTRACE
    が必要
    単に root
    でもいける
    $ sudo php ./reli i:trace -p 2182685

    View Slide

  27. プロセス名で自動アタッチ
    正規表現でプロセス名を指定
    apache
    のワーカとかプロセス生え変わる奴向き
    デフォルトだと 8
    並列で待受
    ext-parallel
    が有効な環境ではマルチスレッド
    $ sudo php ./reli i:daemon -P "^/usr/sbin/ht

    View Slide

  28. top-like
    モード
    毎秒で top
    コマンドっぽく集計表示
    挙動的には daemon
    と同様
    $ sudo php ./reli i:top -P "^/usr/sbin/ht

    View Slide

  29. 実行中のオペコードを取得
    PHP
    の VM
    命令レベルでトレース
    PHP
    コード自体がボトルネックなら便利
    I/O
    や標準関数がボトルネックなことの方が多い
    ./reli i:trace --template=phpspy_with_opcode -- php mandelbrot.php



    0 ::ZEND_ASSIGN :-1

    1 Mandelbrot::iterate /home/sji/work/test/mandelbrot.php:33:ZEND_ASSIG
    2 Mandelbrot::__construct /home/sji/work/test/mandelbrot.php:12:ZEND_D
    3 /home/sji/work/test/mandelbrot.php:45:ZEND_DO_FCALL

    View Slide

  30. PHP
    テンプレート機能
    出力テンプレートをPHP
    で整形可能
    必要なら関数内の各行を別関数扱いにしたりもできる
    とりあえずJSON
    で吐いて後処理、とかもできる
    call_frames as $frame): ?>

    func: = $frame->getFullyQualifiedFunctionName() . PHP_EOL ?>

    file: = $frame->file_name . PHP_EOL ?>

    line: = $frame->getLineno() . PHP_EOL ?>



    = "\n" ?>

    View Slide

  31. というか全体が PHP

    PHP
    スクリプトの改善をするのはしばしば PHPer
    PHPer
    が好きに挙動をいじれるプロファイラという価値
    かわりに速度は C
    製の phpspy
    より遅い
    JIT
    はいくらか効く

    View Slide

  32. Docker
    から使う
    いくつかのオプション設定で Docker
    からも
    apparmor
    無効化
    CAP_SYS_PTRACE
    のケーパビリティ追加
    --pid=host
    でコンテナ外のプロセスが見える
    Windows
    の Docker Desktop
    で他のコンテナ内 PHP
    にア
    タッチ可能
    $ docker pull reliforp/reli-prof

    $ docker run -it \

    --security-opt="apparmor=unconfined" \

    --cap-add=SYS_PTRACE \

    --pid=host \

    reliforp/reli-prof \

    i:daemon -P "^/usr/sbin/httpd"

    View Slide

  33. 可視化
    Flame Graph Search ic
    P..
    Psalm\I..
    Psalm\In..
    PhpParser\Node..
    Php..
    Ph..
    Psalm\Internal\Provider\StatementsPro..
    P..
    PhpParser..
    PhpParser\Nod..
    Psalm\In..
    Psalm\Internal\Analyzer..
    P..
    P..
    Psalm\Internal\Codebase\Scanner::scanFile
    Ph..
    Psa..
    Psalm\Internal\Provider\StatementsPro..
    Ps..
    PhpParser..
    Ph..
    Ps..
    P..
    PhpPa..
    Psalm\Internal\Cli\Psalm::run
    Psal..

    Psalm\Con..
    PhpPa..
    Psalm\Internal\Scanner\FileScanner::scan
    P..
    Psalm\Internal\Codebase\Scanner::Psalm\Internal\Codebase\{closure}
    PhpParser\NodeTraverser::traverseArray
    PhpParser\ParserAbstract:..
    Psalm\Internal\Analy..
    Psalm\Internal\Provider\Statem..
    Psalm\Cod..
    Psalm\Codebase::scanFiles
    Psalm\Internal\Codebase\Scanner::scanFilePaths
    array_merge
    PhpParser\ParserAbst..
    P..
    Psalm\Internal\Analyzer\ProjectAnalyzer::check
    P..
    Ph..
    Psalm\Internal\Codebase\Scanner::scanFile
    Psalm\Internal\Scanner\FileScanner::scan
    Psalm\Internal\PhpVisito..
    Php..
    P..
    Psalm\Config::visitComposerAutoloadFiles
    Psalm\Internal\Codebase\Scanner::scanFilePaths
    Ph..
    PhpParser\NodeTraverser::traverseNode
    Psalm\Internal\Provider\Statem..
    PhpParser\ParserAbstract..
    Psalm\Internal\Codebase\Scanner::scanFiles
    Ph..
    PhpPa..
    P..
    PhpP..
    PhpParser\NodeTraverser::traverseArray
    PhpParse..
    Psalm\Internal\Codebase\Scanner::scanFiles
    Psa..
    P..
    P..
    Psalm..
    Psalm\Internal\Analyzer..
    Psalm\Internal\Analyzer\ProjectAnalyzer::visitAutoloadFiles
    Ph..
    PhpParser\ParserAbstract::parse
    PhpParser\NodeTraverser::traverse
    Psalm\In..
    Ph..
    Ph..
    Php.. Ph..
    P.. PhpParser\..
    Psal..
    P..
    PhpParser\No..
    Psalm\Internal\PhpVisitor..
    P..

    Ph..
    Ps..
    Ps..
    Psalm\In..
    PhpParser\Nod..
    Psalm\Internal\Codebase\Scanner::Psalm\Internal\Code..
    PhpPa..
    Psalm\Internal\PhpVisito..
    Ph..
    フレームグラフや speedscope
    形式を吐ける
    これ以外の可視化方法にも徐々に対応していきたい
    perfetto
    とか call-grind
    とか

    View Slide

  34. 内部実装

    View Slide

  35. 達成すべきこと
    外部プロセスのメモリ内のどこに何があるかを知る
    外部プロセスのメモリ内容を覗き見る

    View Slide

  36. /proc/
    下の内容を解釈
    /proc//maps =
    対象プロセスのメモリマップ
    正規表現でパース
    PHP
    処理系が居そうな領域を見つける
    % sudo cat /proc/10364/maps

    56187c10d000-56187c20e000 r--p 00000000 08:07 1181323 /usr/sbin/p
    56187c30d000-56187c6a9000 r-xp 00200000 08:07 1181323 /usr/sbin/p
    56187c70d000-56187cef3000 r--p 00600000 08:07 1181323 /usr/sbin/p
    56187d26a000-56187d30d000 r--p 00f5d000 08:07 1181323 /usr/sbin/p
    56187d30d000-56187d314000 rw-p 01000000 08:07 1181323 /usr/sbin/p
    56187d314000-56187d335000 rw-p 00000000 00:00 0

    56187f100000-56187f2c6000 rw-p 00000000 00:00 0 [heap]

    56187f100000-56187f2c6000 rw-p 00000000 00:00 0 [heap]

    7f6b76c00000-7f6b76e00000 rw-p 00000000 00:00 0

    7f6b76eb3000-7f6b76f34000 rw-p 00000000 00:00 0

    View Slide

  37. ELF
    を解釈
    ELF
    ファイルがシンボル情報を持つ

    ELF
    パーサを PHP
    で実装
    PHP
    処理系の VM
    状態はグローバル変数(EG)
    に保持
    EG
    をシンボル解決するとファイル上のオフセット
    オフセット +
    プロセスのベースアドレス =
    目的アドレス
    ベースアドレスは /proc//maps
    から得る

    View Slide

  38. FFI
    で process_vm_readv(2)
    を呼んでメモリのぞき見
    呼び出しプロセスのアドレス空間の指定領域へ PID
    で指定さ
    れたプロセスのアドレス空間の指定領域をコピー
    プロセスの壁を越えてデータがコピーできる
    このために CAP_SYS_PTRACE
    が必要
    PHP 7.4
    で追加された FFI
    で呼び出し

    View Slide

  39. FFI
    で ポインタを ZendEngine
    内の構造体ポインタへキャスト
    PHP
    処理系は C
    で書かれている
    C
    の構造体定義は FFI
    で読める
    process_vm_readv(2)
    で実行中の処理系内部データをコピー
    してきたバッファのポインタを PHP
    構造体へのポインタにキ
    ャスト
    public function readAs(

    string $type,

    CData $cdata

    ): CastedCData {
    //
    処理系のヘッダを読み込み


    $ffi = $this->loadHeader($this->php_version);

    return new CastedCData(

    $cdata,

    $ffi->cast($type, $cdata)

    ?? throw new CannotCastCDataException(

    'cannot cast a C Data'

    ),

    );

    }

    View Slide

  40. 各 C
    言語構造体へのプロクシ
    CData
    は動的すぎて辛い型
    PHP
    のクラス経由で各構造体へアクセスできるように
    型付プロパティのunset
    でマジックメソッドが起動可能
    CData
    へのアクセスは遅いのでマジックメソッドで lazy

    final class ZendExecutorGlobals implements Dereferencable

    {

    /** @var Pointer|null */

    public ?Pointer $current_execute_data;



    /** @param CastedCData $casted_cd
    public function __construct(

    private CastedCData $casted_cdata,

    ) {

    unset($this->current_execute_data);

    }

    View Slide

  41. PHP
    の型としてのポインタ
    リモートプロセスから取得したポインタはリモートプロセス
    の中での仮想アドレスを指している
    直接アクセスすると SEGV
    PHP
    側でポインタ用の型を定義
    process_vm_readv(2)
    を内部で呼び出すデリファレンサ経由
    でしかアクセスできないように
    Psalm
    のジェネリクスでデリファレンス後は中身の型に
    /**

    * @template T of \PhpProfiler\Lib\Process\Pointer\Dereferen
    */

    class Pointer

    {

    /** @param class-string $type */

    public function __construct(

    public string $type,

    public int $address,

    public int $size,

    ) {

    }

    interface Dereferencer

    {

    /**

    * @template T of Dereferencable

    * @param Pointer $pointer

    * @return T

    */

    public function deref(Pointer $pointer): mixed;

    View Slide

  42. 一応解説記事も書いたよ
    『「別プロセスの PHP
    が今何をしているか」を実況するプロ
    グラムを PHP
    で作った』
    https://qiita.com/sj-i/items/a29d54cfd83f230ddc3d

    View Slide

  43. その他のこと

    View Slide

  44. 名前
    元々は php-profiler
    というツール名だった
    最適化にPHP
    ライセンスのコードが必要
    PHP
    ライセンスでは派生プロジェクトがPHP
    と名乗るとダメ
    雑にstrrev(php-profiler)
    するとreliforp-php
    reli for p(hp)
    → Reli

    View Slide

  45. 面白かったらスターを付けてね!
    スターが付くと「試してみようかな」という人も増える
    フィードバックが増えればより便利にできる
    各種のフィードバック待ってます
    https://github.com/reliforp/reli-prof

    View Slide

  46. まとめ

    View Slide

  47. PHP
    ではわりともうなんでもできる

    View Slide

  48. 変なものを作りましょう!

    View Slide

  49. おしまい

    View Slide